Migrations and Try Runtime
How to use the slides - Full screen (new tab)
Migrations and Try Runtime
Runtime upgrades...
and how to survive them
At the end of this lecture, you will be able to:
- Justify when runtime migrations are needed.
- Write a the full a runtime upgrade that includes migrations, end-to-end.
- Test runtime upgrades before executing on a network using
try-runtime
andremote-externalities
.
When is a Migration Required?
---v
When is a Migration Required?
- In a typical runtime upgrade, you typically only replace
:code:
. This is Runtime Upgrade. - If you change the storage layout, then this is also a Runtime Migration.
Anything that changes encoding is a migration!
---v
When is a Migration Required?
#![allow(unused)] fn main() { #[pallet::storage] pub type FooValue = StorageValue<_, Foo>; }
#![allow(unused)] fn main() { // old pub struct Foo(u32) // new pub struct Foo(u64) }
- A clear migration.
---v
When is a Migration Required?
#![allow(unused)] fn main() { #[pallet::storage] pub type FooValue = StorageValue<_, Foo>; }
#![allow(unused)] fn main() { // old pub struct Foo(u32) // new pub struct Foo(i32) // or pub struct Foo(u16, u16) }
- The data still fits, but the interpretations is almost certainly different!
---v
When is a Migration Required?
#![allow(unused)] fn main() { #[pallet::storage] pub type FooValue = StorageValue<_, Foo>; }
#![allow(unused)] fn main() { // old pub struct Foo { a: u32, b: u32 } // new pub struct Foo { a: u32, b: u32, c: u32 } }
- This is still a migration, because
Foo
's decoding changed.
---v
When is a Migration Required?
#![allow(unused)] fn main() { #[pallet::storage] pub type FooValue = StorageValue<_, Foo>; }
#![allow(unused)] fn main() { // old pub struct Foo { a: u32, b: u32 } // new pub struct Foo { a: u32, b: u32, c: PhantomData<_> } }
- If for whatever reason
c
has a type that its encoding is like()
, then this would work.
---v
When is a Migration Required?
#![allow(unused)] fn main() { #[pallet::storage] pub type FooValue = StorageValue<_, Foo>; }
#![allow(unused)] fn main() { // old pub enum Foo { A(u32), B(u32) } // new pub enum Foo { A(u32), B(u32), C(u128) } }
- Extending an enum is even more interesting, because if you add the variant to the end, no migration is needed.
- Assuming that no value is initialized with
C
, this is not a migration.
---v
When is a Migration Required?
#![allow(unused)] fn main() { #[pallet::storage] pub type FooValue = StorageValue<_, Foo>; }
#![allow(unused)] fn main() { // old pub enum Foo { A(u32), B(u32) } // new pub enum Foo { A(u32), C(u128), B(u32) } }
- You probably never want to do this, but it is a migration.
---v
🦀 Rust Recall 🦀
Enums are encoded as the variant enum, followed by the inner data:
- The order matters! Both in
struct
andenum
. - Enums that implement
Encode
cannot have more than 255 variants.
---v
When is a Migration Required?
#![allow(unused)] fn main() { #[pallet::storage] pub type FooValue = StorageValue<_, u32>; }
#![allow(unused)] fn main() { // new #[pallet::storage] pub type BarValue = StorageValue<_, u32>; }
- So far everything is changing the value format.
- The key changing is also a migration!
---v
When is a Migration Required?
#![allow(unused)] fn main() { #[pallet::storage] pub type FooValue = StorageValue<_, u32>; }
#![allow(unused)] fn main() { // new #[pallet::storage_prefix = "FooValue"] #[pallet::storage] pub type I_can_NOW_BE_renamEd_hahAA = StorageValue<_, u32>; }
- Handy macro if you must rename a storage type.
- This does not require a migration.
Writing Runtime Migrations
- Now that we know how to detect if a storage change is a migration, let's see how we write one.
---v
Writing Runtime Migrations
- Once you upgrade a runtime, the code is expecting the data to be in a new format.
- Any
on_initialize
or transaction might fail decoding data, and potentiallypanic!
---v
Writing Runtime Migrations
- We need a hook that is executed ONCE as a part of the new runtime...
- But before ANY other code (on_initialize, any transaction) with the new runtime is migrated.
This is
OnRuntimeUpgrade
.
---v
Writing Runtime Migrations
- Optional activity: Go into
executive
andsystem
, and find out howOnRuntimeUpgrade
is called only when the code changes!
Pallet Internal Migrations
---v
Pallet Internal Migrations
One way to write a migration is to write it inside the pallet.
#![allow(unused)] fn main() { #[pallet::hooks] impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { fn on_runtime_upgrade() -> Weight { migrate_stuff_and_things_here_and_there<T>(); } } }
This approach is likely to be deprecated and is no longer practiced within Parity either.
---v
Pallet Internal Migrations
#![allow(unused)] fn main() { #[pallet::hooks] impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { fn on_runtime_upgrade() -> Weight { if guard_that_stuff_has_not_been_migrated() { migrate_stuff_and_things_here_and_there<T>(); } else { // nada } } } }
- If you execute
migrate_stuff_and_things_here_and_there
twice as well, then you are doomed 😫.
---v
Pallet Internal Migrations
Historically, something like this was used:
#![allow(unused)] fn main() { #[derive(Encode, Decode, ...)] enum StorageVersion { V1, V2, V3, // add a new variant with each version } #[pallet::storage] pub type Version = StorageValue<_, StorageVersion>; #[pallet::hooks] impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { fn on_runtime_upgrade() -> Weight { if let StorageVersion::V2 = Version::<T>::get() { // do migration Version::<T>::put(StorageVersion::V3); } else { // nada } } } }
---v
Pallet Internal Migrations
- FRAME introduced macros to manage migrations:
#[pallet::storage_version]
.
#![allow(unused)] fn main() { // your current storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet<T>(_); }
- This adds two function to the
Pallet<_>
struct:
#![allow(unused)] fn main() { // read the current version, encoded in the code. let current = Pallet::<T>::current_storage_version(); // read the version encoded onchain. Pallet::<T>::on_chain_storage_version(); // synchronize the two. current.put::<Pallet<T>>(); }
---v
Pallet Internal Migrations
#![allow(unused)] fn main() { #[pallet::hooks] impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { fn on_runtime_upgrade() -> Weight { let current = Pallet::<T>::current_storage_version(); let onchain = Pallet::<T>::on_chain_storage_version(); if current == 1 && onchain == 0 { // do stuff current.put::<Pallet<T>>(); } else { } } } }
Stores the version as u16 in twox(pallet_name) ++ twox(:__STORAGE_VERSION__:)
.
External Migrations
---v
External Migrations
- Managing migrations within a pallet could be hard.
- Especially for those that want to use external pallets.
Alternative:
- Every runtime can explicitly pass anything that implements
OnRuntimeUpgrade
toExecutive
. - End of the day, Executive does:
<(COnRuntimeUpgrade, AllPalletsWithSystem) as OnRuntimeUpgrade>::on_runtime_upgrade()
.
---v
External Migrations
- The main point of external migrations is making it more clear:
- "What migrations did exactly execute on upgrade to spec_version xxx"
---v
External Migrations
- Expose your migration as a standalone function or struct implementing
OnRuntimeUpgrade
inside apub mod v<version_number>
.
#![allow(unused)] fn main() { pub mod v3 { pub struct Migration; impl OnRuntimeUpgrade for Migration { fn on_runtime_upgrade() -> Weight { // do stuff } } } }
---v
External Migrations
- Guard the code of the migration with
pallet::storage_version
- Don't forget to write the new version!
#![allow(unused)] fn main() { pub mod v3 { pub struct Migration; impl OnRuntimeUpgrade for Migration { fn on_runtime_upgrade() -> Weight { let current = Pallet::<T>::current_storage_version(); let onchain = Pallet::<T>::on_chain_storage_version(); if current == 1 && onchain == 0 { // do stuff current.put::<Pallet<T>>(); } else { } } } } }
---v
External Migrations
- Pass it to the runtime per-release.
#![allow(unused)] fn main() { pub type Executive = Executive< _, _, _, _, _, (v3::Migration, ...) >; }
---v
External Migrations
- Discussion: Can the runtime upgrade scripts live forever? Or should they be removed after a few releases?
Notes:
Short answer is, yes, but it is a LOT of work. See here: https://github.com/paritytech/polkadot-sdk/issues/296
Utilities in frame-support
.
translate
methods:- For
StorageValue
,StorageMap
, etc.
- For
- https://paritytech.github.io/substrate/master/frame_support/storage/migration/index.html
#[storage_alias]
macro to create storage types for removed for those that are being removed.
Notes:
Imagine you want to remove a storage map and in a migration you want to iterate it and delete all items. You want to remove this storage item, but it would be handy to be able to access it one last time in the migration code. This is where #[storage_alias]
comes into play.
Case Studies
- The day we destroyed all balances in Polkadot.
- First ever migration (
pallet-elections-phragmen
). - Fairly independent migrations in
pallet-elections-phragmen
.
Testing Upgrades
---v
Testing Upgrades
-
try-runtime
+RemoteExternalities
allow you to examine and test a runtime in detail with a high degree of control over the environment. -
It is meant to try things out, and inspired by traits like
TryFrom
, the nameTryRuntime
was chosen.
---v
Testing Upgrades
Recall:
- The runtime communicates with the client via host functions.
- Moreover, the client communicates with the runtime via runtime APIs.
- An environment that provides these host functions is called
Externalities
. - One example of which is
TestExternalities
, which you have already seen.
---v
Testing Upgrades: remote-externalities
remote-externalities
ia a builder pattern that loads the state of a live chain inside TestExternalities
.
#![allow(unused)] fn main() { let mut ext = Builder::<Block>::new() .mode(Mode::Online(OnlineConfig { transport: "wss://rpc.polkadot.io", pallets: vec!["PalletA", "PalletB", "PalletC", "RandomPrefix"], ..Default::default() })) .build() .await .unwrap(); }
Reading all this data over RPC can be slow!
---v
Testing Upgrades: remote-externalities
remote-externalities
supports:
- Custom prefixes -> Read a specific pallet
- Injecting custom keys -> Read
:code:
as well. - Injecting custom key-values -> Overwrite
:code:
with0x00
! - Reading child-tree data -> Relevant for crowdloan pallet etc.
- Caching everything in disk for repeated use.
---v
Testing Upgrades: remote-externalities
remote-externalities
is in itself a very useful tool to:
- Go back in time and re-running some code.
- Write unit tests that work on the real-chain's state.
Testing Upgrades: try-runtime
-
try-runtime
is a CLI and a set of custom runtime APIs integrated in substrate that allows you to do detailed testing.. -
.. including running
OnRuntimeUpgrade
code of a new runtime, on top of a real chain's data.
---v
Testing Upgrades: try-runtime
- A lot can be said about it, the best resource is the rust-docs.
---v
Testing Upgrades: try-runtime
- You might find some code in your runtime that is featured gated with
#[cfg(feature = "try-runtime")]
. These are always for testing. pre_upgrade
andpost_upgrade
: Hooks executed before and afteron_runtime_upgrade
.try_state
: called in various other places, used to check the invariants the pallet.
---v
Testing Upgrades: try-runtime
: Live Demo.
- Let's craft a migration on top of poor node-template 😈..
- and migrate the balance type from u128 to u64.
Additional Resources 😋
Check speaker notes (click "s" 😉)
Notes:
- Additional work on automatic version upgrades: https://github.com/paritytech/substrate/issues/13107
- A Great talk about try-runtime and further testing of your runtime: https://www.youtube.com/watch?v=a_u3KMG-n-I
Reference material:
Notes:
FIXME: docs.google.com/presentation/d/1hr3fiqOI0JlXw0ISs8uV9BXiDQ5mGOQLc3b_yWK6cxU/edit#slide=id.g43d9ae013f_0_82
was listed here as a reference but is not public!
Exercise ideas:
- Find the storage version of nomination pools pallet in Kusama.
- Give them a poorly written migration code, and try and fix it. Things they need to fix:
- The migration depends on
<T: Config>
- Does not manage version properly
- is hardcoded in the pallet.
- The migration depends on
- Re-execute the block at which the runtime went OOM in May 25th 2021 Polkadot.