Initialise on-chain StorageVersion for pallets added after genesis (#1297)

Original PR https://github.com/paritytech/substrate/pull/14641

---

Closes https://github.com/paritytech/polkadot-sdk/issues/109

### Problem
Quoting from the above issue:

> When adding a pallet to chain after genesis we currently don't set the
StorageVersion. So, when calling on_chain_storage_version it returns 0
while the pallet is maybe already at storage version 9 when it was added
to the chain. This could lead to issues when running migrations.

### Solution

- Create a new trait `BeforeAllRuntimeMigrations` with a single method
`fn before_all_runtime_migrations() -> Weight` trait with a noop default
implementation
- Modify `Executive` to call
`BeforeAllRuntimeMigrations::before_all_runtime_migrations` for all
pallets before running any other hooks
- Implement `BeforeAllRuntimeMigrations` in the pallet proc macro to
initialize the on-chain version to the current pallet version if the
pallet has no storage set (indicating it has been recently added to the
runtime and needs to have its version initialised).

### Other changes in this PR

- Abstracted repeated boilerplate to access the `pallet_name` in the
pallet expand proc macro.

### FAQ

#### Why create a new hook instead of adding this logic to the pallet
`pre_upgrade`?

`Executive` currently runs `COnRuntimeUpgrade` (custom migrations)
before `AllPalletsWithSystem` migrations. We need versions to be
initialized before the `COnRuntimeUpgrade` migrations are run, because
`COnRuntimeUpgrade` migrations may use the on-chain version for critical
logic. e.g. `VersionedRuntimeUpgrade` uses it to decide whether or not
to execute.

We cannot reorder `COnRuntimeUpgrade` and `AllPalletsWithSystem` so
`AllPalletsWithSystem` runs first, because `AllPalletsWithSystem` have
some logic in their `post_upgrade` hooks to verify that the on-chain
version and current pallet version match. A common use case of
`COnRuntimeUpgrade` migrations is to perform a migration which will
result in the versions matching, so if they were reordered these
`post_upgrade` checks would fail.

#### Why init the on-chain version for pallets without a current storage
version?

We must init the on-chain version for pallets even if they don't have a
defined storage version so if there is a future version bump, the
on-chain version is not automatically set to that new version without a
proper migration.

e.g. bad scenario:

1. A pallet with no 'current version' is added to the runtime
2. Later, the pallet is upgraded with the 'current version' getting set
to 1 and a migration is added to Executive Migrations to migrate the
storage from 0 to 1
    a. Runtime upgrade occurs
    b. `before_all` hook initializes the on-chain version to 1
c. `on_runtime_upgrade` of the migration executes, and sees the on-chain
version is already 1 therefore think storage is already migrated and
does not execute the storage migration
Now, on-chain version is 1 but storage is still at version 0.

By always initializing the on-chain version when the pallet is added to
the runtime we avoid that scenario.

---------

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-authored-by: Bastian Köcher <git@kchr.de>
This commit is contained in:
Liam Aharon
2023-11-07 15:12:40 +11:00
committed by GitHub
parent 32a974088c
commit c4211b6533
8 changed files with 182 additions and 41 deletions
+57 -3
View File
@@ -21,7 +21,7 @@ use frame_support::{
dispatch_context::with_context,
pallet_prelude::{StorageInfoTrait, ValueQuery},
parameter_types,
storage::unhashed,
storage::{unhashed, unhashed::contains_prefixed_key},
traits::{
ConstU32, GetCallIndex, GetCallName, GetStorageVersion, OnFinalize, OnGenesis,
OnInitialize, OnRuntimeUpgrade, PalletError, PalletInfoAccess, StorageVersion,
@@ -2330,6 +2330,58 @@ fn test_storage_alias() {
})
}
#[test]
fn pallet_on_chain_storage_version_initializes_correctly() {
type Executive = frame_executive::Executive<
Runtime,
Block,
frame_system::ChainContext<Runtime>,
Runtime,
AllPalletsWithSystem,
>;
// Simple example of a pallet with current version 10 being added to the runtime for the first
// time.
TestExternalities::default().execute_with(|| {
let current_version = Example::current_storage_version();
// Check the pallet has no storage items set.
let pallet_hashed_prefix = twox_128(Example::name().as_bytes());
let exists = contains_prefixed_key(&pallet_hashed_prefix);
assert_eq!(exists, false);
// [`frame_support::traits::BeforeAllRuntimeMigrations`] hook should initialize the storage
// version.
Executive::execute_on_runtime_upgrade();
// Check that the storage version was initialized to the current version
let on_chain_version_after = StorageVersion::get::<Example>();
assert_eq!(on_chain_version_after, current_version);
});
// Pallet with no current storage version should have the on-chain version initialized to 0.
TestExternalities::default().execute_with(|| {
// Example4 current_storage_version is NoStorageVersionSet.
// Check the pallet has no storage items set.
let pallet_hashed_prefix = twox_128(Example4::name().as_bytes());
let exists = contains_prefixed_key(&pallet_hashed_prefix);
assert_eq!(exists, false);
// Confirm the storage version is implicitly 0.
let on_chain_version_before = StorageVersion::get::<Example4>();
assert_eq!(on_chain_version_before, StorageVersion::new(0));
// [`frame_support::traits::BeforeAllRuntimeMigrations`] initializes the storage version.
Executive::execute_on_runtime_upgrade();
// Check that the storage version now exists and was initialized to 0.
let on_chain_version_after = StorageVersion::get::<Example4>();
assert_eq!(StorageVersion::exists::<Example4>(), true);
assert_eq!(on_chain_version_after, StorageVersion::new(0));
});
}
#[cfg(feature = "try-runtime")]
#[test]
fn post_runtime_upgrade_detects_storage_version_issues() {
@@ -2382,8 +2434,10 @@ fn post_runtime_upgrade_detects_storage_version_issues() {
>;
TestExternalities::default().execute_with(|| {
// Call `on_genesis` to put the storage version of `Example` into the storage.
Example::on_genesis();
// Set the on-chain version to one less than the current version for `Example`, simulating a
// forgotten migration
StorageVersion::new(9).put::<Example2>();
// The version isn't changed, we should detect it.
assert!(
Executive::try_runtime_upgrade(UpgradeCheckSelect::PreAndPost).unwrap_err() ==