Basic Storage Migration
Goal
Write a storage migration for a pallet that adds an additional Vec<u8>
storage item to runtime storage.
Use Cases
A pallet that adds a single storage item and needs to be included in a runtime upgrade.
Overview
This guide will step through a storage migration on FRAME's Nick's pallet. It shows how to modify a storage map to provide an optional field that includes a last name, and how to write the migration function ready to be triggered upon a runtime upgrade. This guide can equally be used in other contexts which require a simple storage migration that modifies a storage map in a runtime.
Steps
1. Create a storage struct and utility type
Write a struct to manage the previous and new storage items, first and last:
pub struct Nickname {
first: Vec<u8>,
last: Option<Vec<u8>>, // handles empty storage
}
Write a utility type enum to keep track of the storage versions:
#[derive(codec::Encode, codec::Decode, Clone, frame_support::RuntimeDebug, PartialEq)]
pub enum StorageVersion {
V1Bytes,
V2Struct,
}
2. Update your storage items
The Nicks pallet only keeps track of a lookup table in storage, but we also need to add PalletVersion
to
declare the current version in storage. To update these items, use the Nickname
struct in the NameOf
item and add the new storage item PalletVersion
:
decl_storage! {
trait Store for Module<T: Trait> as MyNicks {
/// The lookup table for names.
NameOf: map hasher(twox_64_concat) T::AccountId => Option<(Nickname, BalanceOf<T>)>;
/// The current version of the pallet.
PalletVersion: StorageVersion = StorageVersion::V1Bytes;
}
}
3. Update all functions
All of the Nicks pallet functions need to account for the new last: Option<Vec<u8>>
storage item. Update each function by adding it as a parameter, for example:
//--snip
fn force_name(origin,
target: <T::Lookup as StaticLookup>::Source,
first: Vec<u8>,
last: Option<Vec<u8>>) {
//--snip
}
In addition, update all storage writes with the Nickname
struct:
<NameOf<T>>::insert(&sender, (Nickname { first, last }, deposit));
4. Declare a migration module
The migration module should contain two parts:
- A module indicating the deprecated storage to migrate from.
- The migration function which returns a weight.
The scaffolding of this module looks like this:
pub mod migration {
use super::*;
pub mod v1 {...} // only contains V1 storage format
pub fn migrate_to_v2<T: Config>() -> frame_support::weights::Weight {...} // contains checks and transforms storage to V2 format
}
5. Write migrate_to_v2
Here's an overview of what this function needs to do:
- Check the storage version to make sure a migration is needed (good practice)
- Transform the storage values into the new storage format
- Update the storage version
- Return the weight consumed by the migration
Check the storage version
Construct the migrate_to_v2
logic around the check. If the storage migration doesn't need to happen, return 0:
if PalletVersion::get() == StorageVersion::V1Bytes {
// migrate to v2
} else {
frame_support::debug::info!(" >>> Unused migration!");
0
}
Transform storage values
Using the translate storage method
,
transform the storage values to the new format. Since the existing nick
value in storage can be made of a string separated by a
space, split it at the ' '
and place anything after that into the new last
storage item. If it isn't, last
takes the None
value:
NameOf::<T>::translate::<(Vec<u8>, BalanceOf<T>), _>(
|k: T::AccountId, (nick, deposit): (Vec<u8>, BalanceOf<T>)| {
// We split the nick at ' ' (<space>).
match nick.iter().rposition(|&x| x == b" "[0]) {
Some(ndx) => Some((Nickname {
first: nick[0..ndx].to_vec(),
last: Some(nick[ndx + 1..].to_vec())
}, deposit)),
None => Some((Nickname { first: nick, last: None }, deposit))
}
}
);
Remove 'Option' wrapping to make sure decoding works properly.
Return the consumed weight
To do this, count the number of storage reads and writes and return the corresponding weight:
let count = NameOf::<T>::iter().count();
T::DbWeight::get().reads_writes(count as Weight + 1, count as Weight + 1)
Use migrate_to_v2
in on_runtime_upgrade
Go back to the pallet's functions and specify the migrate_to_v2
function in on_runtime_upgrade
:
fn on_runtime_upgrade() -> frame_support::weights::Weight {
migration::migrate_to_v2::<T>()
}
6. Create a types.json
file
Put the new storage types in a types.json
which you will need to trigger the migration using a UI. Our new types in JSON are:
{
"Nickname": {
"first": "Vec<u8>",
"last": "Option<Vec<u8>>"
},
"StorageVersion": {
"_enum": [
"V1Bytes",
"V2Struct"
]
}
}
Examples
Resources
Rust docs
Option
in Rustframe_support::storage::migration
utility docs