referrerpolicy=no-referrer-when-downgrade
Expand description

§Single Block Migration Example Pallet

An example pallet demonstrating best-practices for writing single-block migrations in the context of upgrading pallet storage.

§Forewarning

Single block migrations MUST execute in a single block, therefore when executed on a parachain are only appropriate when guaranteed to not exceed block weight limits. If a parachain submits a block that exceeds the block weight limit it will brick the chain!

If weight is a concern or you are not sure which type of migration to use, you should probably use a multi-block migration.

TODO: Link above to multi-block migration example.

§Pallet Overview

This example pallet contains a single storage item Value, which may be set by any signed origin by calling the set_value extrinsic.

For the purposes of this exercise, we imagine that in [StorageVersion] V0 of this pallet Value is a u32, and this what is currently stored on-chain.

// (Old) Storage Version V0 representation of `Value`
#[pallet::storage]
pub type Value<T: Config> = StorageValue<_, u32>;

In [StorageVersion] V1 of the pallet a new struct CurrentAndPreviousValue is introduced:

#[derive(
	Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen,
)]
pub struct CurrentAndPreviousValue {
	/// The most recently set value.
	pub current: u32,
	/// The previous value, if one existed.
	pub previous: Option<u32>,
}

and Value is updated to store this new struct instead of a u32:

#[pallet::storage]
pub type Value<T: Config> = StorageValue<_, CurrentAndPreviousValue>;

In StorageVersion V1 of the pallet when set_value is called, the new value is stored in the current field of CurrentAndPreviousValue, and the previous value (if it exists) is stored in the previous field.

#[pallet::call]
impl<T: Config> Pallet<T> {
	pub fn set_value(origin: OriginFor<T>, value: u32) -> DispatchResult {
		ensure_signed(origin)?;

		let previous = Value::<T>::get().map(|v| v.current);
		let new_struct = CurrentAndPreviousValue { current: value, previous };
		<Value<T>>::put(new_struct);

		Ok(())
	}
}

§Why a migration is necessary

Without a migration, there will be a discrepancy between the on-chain storage for Value (in V0 it is a u32) and the current storage for Value (in V1 it was changed to a CurrentAndPreviousValue struct).

The on-chain storage for Value would be a u32 but the runtime would try to read it as a CurrentAndPreviousValue. This would result in unacceptable undefined behavior.

§Adding a migration module

Writing a pallets migrations in a separate module is strongly recommended.

Here’s how the migration module is defined for this pallet:

substrate/frame/examples/single-block-migrations/src/
├── lib.rs       <-- pallet definition
├── Cargo.toml   <-- pallet manifest
└── migrations/
   ├── mod.rs    <-- migrations module definition
   └── v1.rs     <-- migration logic for the V0 to V1 transition

This structure allows keeping migration logic separate from the pallet logic and easily adding new migrations in the future.

§Writing the Migration

All code related to the migration can be found under v1.rs.

See the migration source code for detailed comments.

Here’s a brief overview of modules and types defined in v1.rs:

§mod v0

Here we define a storage_alias for the old v0 Value format.

This allows reading the old v0 value from storage during the migration.

§InnerMigrateV0ToV1

Here we define our raw migration logic, InnerMigrateV0ToV1 which implements the UncheckedOnRuntimeUpgrade trait.

§Why UncheckedOnRuntimeUpgrade?

Otherwise, we would have two implementations of OnRuntimeUpgrade which could be confusing, and may lead to accidentally using the wrong one.

§Standalone Struct or Pallet Hook?

Note that the storage migration logic is attached to a standalone struct implementing UncheckedOnRuntimeUpgrade, rather than implementing the Hooks::on_runtime_upgrade hook directly on the pallet. The pallet hook is better suited for special types of logic that need to execute on every runtime upgrade, but not so much for one-off storage migrations.

§MigrateV0ToV1

Here, InnerMigrateV0ToV1 is wrapped in a VersionedMigration to define MigrateV0ToV1, which may be used in runtimes.

Using VersionedMigration ensures that

  • The migration only runs once when the on-chain storage version is 0
  • The on-chain storage version is updated to 1 after the migration executes
  • Reads and writes from checking and setting the on-chain storage version are accounted for in the final Weight

§mod test

Here basic unit tests are defined for the migration.

When writing migration tests, don’t forget to check:

  • on_runtime_upgrade returns the expected weight
  • post_upgrade succeeds when given the bytes returned by pre_upgrade
  • Pallet storage is in the expected state after the migration

Re-exports§

Modules§

  • The pallet module in each FRAME pallet hosts the most important items needed to construct this pallet.

Structs§