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 weightpost_upgrade
succeeds when given the bytes returned bypre_upgrade
- Pallet storage is in the expected state after the migration
Re-exports§
pub use pallet::*;
Modules§
- The
pallet
module in each FRAME pallet hosts the most important items needed to construct this pallet.