migrations: prevent accidentally using unversioned migrations instead of VersionedMigration (#3835)

closes #1324 

#### Problem
Currently, it is possible to accidentally use inner unversioned
migration instead of `VersionedMigration` since both implement
`OnRuntimeUpgrade`.

#### Solution

With this change, we make it clear that value of `Inner` is not intended
to be used directly. It is achieved by bounding `Inner` to new trait
`UncheckedOnRuntimeUpgrade`, which has the same interface (except
`unchecked_` prefix) as `OnRuntimeUpgrade`.

#### `try-runtime` functions

Since developers can implement `try-runtime` for `Inner` value in
`VersionedMigration` and have custom logic for it, I added the same
`try-runtime` functions to `UncheckedOnRuntimeUpgrade`. I looked for a
ways to not duplicate functions, but couldn't find anything that doesn't
significantly change the codebase. So I would appreciate If you have any
suggestions to improve this

cc @liamaharon @xlc 

polkadot address: 16FqwPZ8GRC5U5D4Fu7W33nA55ZXzXGWHwmbnE1eT6pxuqcT

---------

Co-authored-by: Liam Aharon <liam.aharon@hotmail.com>
This commit is contained in:
Dastan
2024-04-02 15:43:09 +02:00
committed by GitHub
parent 8e95a3e1aa
commit e54279699b
22 changed files with 255 additions and 187 deletions
@@ -89,7 +89,7 @@
//!
//! See the migration source code for detailed comments.
//!
//! To keep the migration logic organised, it is split across additional modules:
//! Here's a brief overview of modules and types defined in `v1.rs`:
//!
//! ### `mod v0`
//!
@@ -98,28 +98,29 @@
//!
//! This allows reading the old v0 value from storage during the migration.
//!
//! ### `mod version_unchecked`
//! ### `InnerMigrateV0ToV1`
//!
//! Here we define our raw migration logic,
//! `version_unchecked::MigrateV0ToV1` which implements the [`OnRuntimeUpgrade`] trait.
//! `InnerMigrateV0ToV1` which implements the [`UncheckedOnRuntimeUpgrade`] trait.
//!
//! Importantly, it is kept in a private module so that it cannot be accidentally used in a runtime.
//! #### Why [`UncheckedOnRuntimeUpgrade`]?
//!
//! Private modules cannot be referenced in docs, so please read the code directly.
//! 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
//! [`OnRuntimeUpgrade`], rather than implementing the
//! [`UncheckedOnRuntimeUpgrade`], rather than implementing the
//! [`Hooks::on_runtime_upgrade`](frame_support::traits::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.
//!
//! ### `pub mod versioned`
//! ### `MigrateV0ToV1`
//!
//! Here, `version_unchecked::MigrateV0ToV1` is wrapped in a
//! Here, `InnerMigrateV0ToV1` is wrapped in a
//! [`VersionedMigration`] to define
//! [`versioned::MigrateV0ToV1`](crate::migrations::v1::versioned::MigrateV0ToV1), which may be used
//! [`MigrateV0ToV1`](crate::migrations::v1::MigrateV0ToV1), which may be used
//! in runtimes.
//!
//! Using [`VersionedMigration`] ensures that
@@ -128,8 +129,6 @@
//! - Reads and writes from checking and setting the on-chain storage version are accounted for in
//! the final [`Weight`](frame_support::weights::Weight)
//!
//! This is the only public module exported from `v1`.
//!
//! ### `mod test`
//!
//! Here basic unit tests are defined for the migration.
@@ -142,7 +141,8 @@
//! [`VersionedMigration`]: frame_support::migrations::VersionedMigration
//! [`GetStorageVersion`]: frame_support::traits::GetStorageVersion
//! [`OnRuntimeUpgrade`]: frame_support::traits::OnRuntimeUpgrade
//! [`MigrateV0ToV1`]: crate::migrations::v1::versioned::MigrationV0ToV1
//! [`UncheckedOnRuntimeUpgrade`]: frame_support::traits::UncheckedOnRuntimeUpgrade
//! [`MigrateV0ToV1`]: crate::migrations::v1::MigrateV0ToV1
// We make sure this pallet uses `no_std` for compiling to Wasm.
#![cfg_attr(not(feature = "std"), no_std)]
@@ -17,7 +17,7 @@
use frame_support::{
storage_alias,
traits::{Get, OnRuntimeUpgrade},
traits::{Get, UncheckedOnRuntimeUpgrade},
};
#[cfg(feature = "try-runtime")]
@@ -34,118 +34,92 @@ mod v0 {
pub type Value<T: crate::Config> = StorageValue<crate::Pallet<T>, u32>;
}
/// Private module containing *version unchecked* migration logic.
/// Implements [`UncheckedOnRuntimeUpgrade`], migrating the state of this pallet from V0 to V1.
///
/// Should only be used by the [`VersionedMigration`](frame_support::migrations::VersionedMigration)
/// type in this module to create something to export.
/// In V0 of the template [`crate::Value`] is just a `u32`. In V1, it has been upgraded to
/// contain the struct [`crate::CurrentAndPreviousValue`].
///
/// The unversioned migration should be kept private so the unversioned migration cannot
/// accidentally be used in any runtimes.
///
/// For more about this pattern of keeping items private, see
/// - <https://github.com/rust-lang/rust/issues/30905>
/// - <https://internals.rust-lang.org/t/lang-team-minutes-private-in-public-rules/4504/40>
mod version_unchecked {
use super::*;
/// In this migration, update the on-chain storage for the pallet to reflect the new storage
/// layout.
pub struct InnerMigrateV0ToV1<T: crate::Config>(sp_std::marker::PhantomData<T>);
/// Implements [`OnRuntimeUpgrade`], migrating the state of this pallet from V0 to V1.
impl<T: crate::Config> UncheckedOnRuntimeUpgrade for InnerMigrateV0ToV1<T> {
/// Return the existing [`crate::Value`] so we can check that it was correctly set in
/// `InnerMigrateV0ToV1::post_upgrade`.
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
use codec::Encode;
// Access the old value using the `storage_alias` type
let old_value = v0::Value::<T>::get();
// Return it as an encoded `Vec<u8>`
Ok(old_value.encode())
}
/// Migrate the storage from V0 to V1.
///
/// In V0 of the template [`crate::Value`] is just a `u32`. In V1, it has been upgraded to
/// contain the struct [`crate::CurrentAndPreviousValue`].
/// - If the value doesn't exist, there is nothing to do.
/// - If the value exists, it is read and then written back to storage inside a
/// [`crate::CurrentAndPreviousValue`].
fn on_runtime_upgrade() -> frame_support::weights::Weight {
// Read the old value from storage
if let Some(old_value) = v0::Value::<T>::take() {
// Write the new value to storage
let new = crate::CurrentAndPreviousValue { current: old_value, previous: None };
crate::Value::<T>::put(new);
// One read for the old value, one write for the new value
T::DbWeight::get().reads_writes(1, 1)
} else {
// One read for trying to access the old value
T::DbWeight::get().reads(1)
}
}
/// Verifies the storage was migrated correctly.
///
/// In this migration, update the on-chain storage for the pallet to reflect the new storage
/// layout.
pub struct MigrateV0ToV1<T: crate::Config>(sp_std::marker::PhantomData<T>);
/// - If there was no old value, the new value should not be set.
/// - If there was an old value, the new value should be a [`crate::CurrentAndPreviousValue`].
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
use codec::Decode;
use frame_support::ensure;
impl<T: crate::Config> OnRuntimeUpgrade for MigrateV0ToV1<T> {
/// Return the existing [`crate::Value`] so we can check that it was correctly set in
/// `version_unchecked::MigrateV0ToV1::post_upgrade`.
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
use codec::Encode;
let maybe_old_value = Option::<u32>::decode(&mut &state[..]).map_err(|_| {
sp_runtime::TryRuntimeError::Other("Failed to decode old value from storage")
})?;
// Access the old value using the `storage_alias` type
let old_value = v0::Value::<T>::get();
// Return it as an encoded `Vec<u8>`
Ok(old_value.encode())
}
match maybe_old_value {
Some(old_value) => {
let expected_new_value =
crate::CurrentAndPreviousValue { current: old_value, previous: None };
let actual_new_value = crate::Value::<T>::get();
/// Migrate the storage from V0 to V1.
///
/// - If the value doesn't exist, there is nothing to do.
/// - If the value exists, it is read and then written back to storage inside a
/// [`crate::CurrentAndPreviousValue`].
fn on_runtime_upgrade() -> frame_support::weights::Weight {
// Read the old value from storage
if let Some(old_value) = v0::Value::<T>::take() {
// Write the new value to storage
let new = crate::CurrentAndPreviousValue { current: old_value, previous: None };
crate::Value::<T>::put(new);
// One read for the old value, one write for the new value
T::DbWeight::get().reads_writes(1, 1)
} else {
// One read for trying to access the old value
T::DbWeight::get().reads(1)
}
}
/// Verifies the storage was migrated correctly.
///
/// - If there was no old value, the new value should not be set.
/// - If there was an old value, the new value should be a
/// [`crate::CurrentAndPreviousValue`].
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
use codec::Decode;
use frame_support::ensure;
let maybe_old_value = Option::<u32>::decode(&mut &state[..]).map_err(|_| {
sp_runtime::TryRuntimeError::Other("Failed to decode old value from storage")
})?;
match maybe_old_value {
Some(old_value) => {
let expected_new_value =
crate::CurrentAndPreviousValue { current: old_value, previous: None };
let actual_new_value = crate::Value::<T>::get();
ensure!(actual_new_value.is_some(), "New value not set");
ensure!(
actual_new_value == Some(expected_new_value),
"New value not set correctly"
);
},
None => {
ensure!(crate::Value::<T>::get().is_none(), "New value unexpectedly set");
},
};
Ok(())
}
ensure!(actual_new_value.is_some(), "New value not set");
ensure!(
actual_new_value == Some(expected_new_value),
"New value not set correctly"
);
},
None => {
ensure!(crate::Value::<T>::get().is_none(), "New value unexpectedly set");
},
};
Ok(())
}
}
/// Public module containing *version checked* migration logic.
///
/// This is the only module that should be exported from this module.
///
/// See [`VersionedMigration`](frame_support::migrations::VersionedMigration) docs for more about
/// how it works.
pub mod versioned {
use super::*;
/// `version_unchecked::MigrateV0ToV1` wrapped in a
/// [`VersionedMigration`](frame_support::migrations::VersionedMigration), which 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/Writes from checking/settings the on-chain storage version are accounted for
pub type MigrateV0ToV1<T> = frame_support::migrations::VersionedMigration<
0, // The migration will only execute when the on-chain storage version is 0
1, // The on-chain storage version will be set to 1 after the migration is complete
version_unchecked::MigrateV0ToV1<T>,
crate::pallet::Pallet<T>,
<T as frame_system::Config>::DbWeight,
>;
}
/// [`UncheckedOnRuntimeUpgrade`] implementation [`InnerMigrateV0ToV1`] wrapped in a
/// [`VersionedMigration`](frame_support::migrations::VersionedMigration), which 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/Writes from checking/settings the on-chain storage version are accounted for
pub type MigrateV0ToV1<T> = frame_support::migrations::VersionedMigration<
0, // The migration will only execute when the on-chain storage version is 0
1, // The on-chain storage version will be set to 1 after the migration is complete
InnerMigrateV0ToV1<T>,
crate::pallet::Pallet<T>,
<T as frame_system::Config>::DbWeight,
>;
/// Tests for our migration.
///
@@ -155,10 +129,10 @@ pub mod versioned {
/// 3. The storage is in the expected state after the migration
#[cfg(any(all(feature = "try-runtime", test), doc))]
mod test {
use self::InnerMigrateV0ToV1;
use super::*;
use crate::mock::{new_test_ext, MockRuntime};
use frame_support::assert_ok;
use version_unchecked::MigrateV0ToV1;
#[test]
fn handles_no_existing_value() {
@@ -168,16 +142,16 @@ mod test {
assert!(v0::Value::<MockRuntime>::get().is_none());
// Get the pre_upgrade bytes
let bytes = match MigrateV0ToV1::<MockRuntime>::pre_upgrade() {
let bytes = match InnerMigrateV0ToV1::<MockRuntime>::pre_upgrade() {
Ok(bytes) => bytes,
Err(e) => panic!("pre_upgrade failed: {:?}", e),
};
// Execute the migration
let weight = MigrateV0ToV1::<MockRuntime>::on_runtime_upgrade();
let weight = InnerMigrateV0ToV1::<MockRuntime>::on_runtime_upgrade();
// Verify post_upgrade succeeds
assert_ok!(MigrateV0ToV1::<MockRuntime>::post_upgrade(bytes));
assert_ok!(InnerMigrateV0ToV1::<MockRuntime>::post_upgrade(bytes));
// The weight should be just 1 read for trying to access the old value.
assert_eq!(weight, <MockRuntime as frame_system::Config>::DbWeight::get().reads(1));
@@ -195,16 +169,16 @@ mod test {
v0::Value::<MockRuntime>::put(initial_value);
// Get the pre_upgrade bytes
let bytes = match MigrateV0ToV1::<MockRuntime>::pre_upgrade() {
let bytes = match InnerMigrateV0ToV1::<MockRuntime>::pre_upgrade() {
Ok(bytes) => bytes,
Err(e) => panic!("pre_upgrade failed: {:?}", e),
};
// Execute the migration
let weight = MigrateV0ToV1::<MockRuntime>::on_runtime_upgrade();
let weight = InnerMigrateV0ToV1::<MockRuntime>::on_runtime_upgrade();
// Verify post_upgrade succeeds
assert_ok!(MigrateV0ToV1::<MockRuntime>::post_upgrade(bytes));
assert_ok!(InnerMigrateV0ToV1::<MockRuntime>::post_upgrade(bytes));
// The weight used should be 1 read for the old value, and 1 write for the new
// value.