Expand description
Learn about Runtime Upgrades and best practices for writing Migrations.
§Runtime Upgrades
At their core, blockchain logic consists of
- on-chain state,
- a state transition function.
In Substrate-based blockchains, state transition functions are referred to as runtimes.
Traditionally, before Substrate, upgrading state transition functions required node operators to download new software and restart their nodes in a process called forking.
Substrate-based blockchains do not require forking, and instead upgrade runtimes in a process called “Runtime Upgrades”.
Forkless runtime upgrades are a defining feature of the Substrate framework. Updating the runtime logic without forking the code base enables your blockchain to seamlessly evolve over time in a deterministic, rules-based manner. It also removes ambiguity for node operators and other participants in the network about what is the canonical runtime.
This capability is possible due to the runtime of a blockchain existing in on-chain storage.
§Performing a Runtime Upgrade
To upgrade a runtime, an Origin
with the necessary permissions
(usually via governance) changes the :code
storage. Usually, this is performed via a call to
set_code
(or set_code_without_checks
) with the desired new runtime blob, scheduled
using pallet_scheduler
.
Prior to building the new runtime, don’t forget to update the
RuntimeVersion
.
§Migrations
It is often desirable to define logic to execute immediately after runtime upgrades (see this diagram).
Self-contained pieces of logic that execute after a runtime upgrade are called “Migrations”.
The typical use case of a migration is to ‘migrate’ pallet storage from one layout to another, for example when the encoding of a storage item is changed. However, they can also execute arbitrary logic such as:
- Calling arbitrary pallet methods.
- Mutating arbitrary on-chain state.
- Cleaning up some old storage items that are no longer needed.
§Single Block Migrations
- Execute immediately and entirely at the beginning of the block following a runtime upgrade.
- Are suitable for migrations which are guaranteed to not exceed the block weight.
- Are simply implementations of
OnRuntimeUpgrade
.
To learn best practices for writing single block pallet storage migrations, see the Single Block Migration Example Pallet.
§Scheduling the Single Block Migrations to Run Next Runtime Upgrade
Schedule migrations to run next runtime upgrade passing them as a generic parameter to your
Executive
pallet:
/// Tuple of migrations (structs that implement `OnRuntimeUpgrade`)
type Migrations = (
pallet_example_storage_migration::migrations::v1::versioned::MigrateV0ToV1,
MyCustomMigration,
// ...more migrations here
);
pub type Executive = frame_executive::Executive<
Runtime,
Block,
frame_system::ChainContext<Runtime>,
Runtime,
AllPalletsWithSystem,
Migrations, // <-- pass your migrations to Executive here
>;
§Ensuring Single Block Migration Safety
“My migration unit tests pass, so it should be safe to deploy right?”
No! Unit tests execute the migration in a very simple test environment, and cannot account for the complexities of a real runtime or real on-chain state.
Prior to deploying migrations, it is critical to perform additional checks to ensure that when run in our real runtime they will not brick the chain due to:
- Panicking.
- Touching too many storage keys and resulting in an excessively large PoV.
- Taking too long to execute.
try-runtime-cli
has a sub-command
on-runtime-upgrade
which is designed to help with exactly this.
Developers MUST run this command before deploying migrations to ensure they will not inadvertently result in a bricked chain.
It is recommended to run as part of your CI pipeline. See the polkadot-sdk check-runtime-migration job for an example of how to configure this.
§Note on the Manipulability of PoV Size and Execution Time
While try-runtime-cli
can help ensure with
very high certainty that a migration will succeed given existing on-chain state, it cannot
prevent a malicious actor from manipulating state in a way that will cause the migration to take
longer or produce a PoV much larger than previously measured.
Therefore, it is important to write migrations in such a way that the execution time or PoV size it adds to the block cannot be easily manipulated. e.g., do not iterate over storage that can quickly or cheaply be bloated.
If writing your migration in such a way is not possible, a multi block migration should be used instead.
§Other useful tools
Chopsticks
is another tool in the Substrate
ecosystem which developers may find useful to use in addition to try-runtime-cli
when testing
their single block migrations.
§Multi Block Migrations
Safely and easily execute long-running migrations across multiple blocks.
Suitable for migrations which could use arbitrary amounts of block weight.
See the multi-block-migrations example for reference.