Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Update contract multi-block migration #14313

Merged
merged 45 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
5c14529
move migrate sequence to config
juangirini Jun 6, 2023
e8c493c
remove commented out code
juangirini Jun 6, 2023
0e62930
Update frame/contracts/src/lib.rs
juangirini Jun 6, 2023
14a7175
remove Migrations generic
juangirini Jun 6, 2023
2601d79
make runtime use noop migrations
juangirini Jun 6, 2023
e88854a
restrict is_upgrade_supported
juangirini Jun 6, 2023
a8caaf2
Update contract multi-block migration
pgherveou Jun 6, 2023
1400ab0
undo is_upgrade_supported change
juangirini Jun 7, 2023
cc7609c
Update bin/node/runtime/src/lib.rs
juangirini Jun 7, 2023
92360cd
wip
pgherveou Jun 7, 2023
137ecc8
fix comment (#14316)
pgherveou Jun 7, 2023
cfaf7e8
Merge branch 'jg/remove-default-migrate-sequence' into pg/more_migrat…
pgherveou Jun 7, 2023
27e5cf8
fix test
pgherveou Jun 7, 2023
2935e99
fix
pgherveou Jun 7, 2023
a4bb302
Update frame/contracts/src/migration.rs
pgherveou Jun 7, 2023
f3e81ce
fix test doc
pgherveou Jun 7, 2023
f10f5bd
Apply suggestions from code review
pgherveou Jun 8, 2023
dc556df
Fix compilation with feature runtime-benchmarks
pgherveou Jun 8, 2023
a7ef997
fix example
pgherveou Jun 8, 2023
6a89133
fix cargo doc --document-private-items
pgherveou Jun 9, 2023
86e6696
private links
pgherveou Jun 9, 2023
6417dde
Remove dup comment
pgherveou Jun 9, 2023
e6bb63d
add doc for MigrationInProgress
pgherveou Jun 9, 2023
1841f23
PR review remove duplicate asserts
pgherveou Jun 9, 2023
0236ca0
simplify upper bound
pgherveou Jun 9, 2023
4686c03
fix link
pgherveou Jun 9, 2023
5e029e1
typo
pgherveou Jun 9, 2023
c7846be
typo
pgherveou Jun 9, 2023
d4349fb
no unwrap()
pgherveou Jun 9, 2023
7511bac
correct log message
pgherveou Jun 9, 2023
e4e5ba1
Merge branch 'master' into pg/more_migratinons_per_block
pgherveou Jun 9, 2023
3545f04
missing
pgherveou Jun 9, 2023
08f84fb
fix typo
pgherveou Jun 12, 2023
85f334f
PR comment
pgherveou Jun 12, 2023
103d50c
Add example with single element tuple
pgherveou Jun 13, 2023
dbb20d3
Improve migration message
pgherveou Jun 13, 2023
3b3c179
Update frame/contracts/src/benchmarking/mod.rs
pgherveou Jun 15, 2023
fc977cb
Update frame/contracts/src/migration.rs
pgherveou Jun 15, 2023
d45d570
Update frame/contracts/src/migration.rs
pgherveou Jun 15, 2023
9d5b6b8
use saturating_accrue instead of +=
pgherveou Jun 15, 2023
fe412a7
add more doc
pgherveou Jun 15, 2023
921dc43
Merge branch 'master' into pg/more_migratinons_per_block
pgherveou Jun 19, 2023
8643fcd
Contracts: Better migration types (#14418)
pgherveou Jun 20, 2023
7a2c272
Add explicit error, if try-runtime runs a noop migration
pgherveou Jun 20, 2023
030554d
use mut remaining_weight
pgherveou Jun 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions frame/contracts/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use self::{
};
use crate::{
exec::{AccountIdOf, Key},
migration::{v10, v11, v9, Migrate},
migration::{v10, v11, v9, MigrationStep},
wasm::CallFlags,
Pallet as Contracts, *,
};
Expand Down Expand Up @@ -237,7 +237,7 @@ benchmarks! {
// This benchmarks the v9 migration step. (update codeStorage)
#[pov_mode = Measured]
v9_migration_step {
let c in 0 .. Perbill::from_percent(49).mul_ceil(T::MaxCodeLen::get());
let c in 0 .. T::MaxCodeLen::get();
v9::store_old_dummy_code::<T>(c as usize);
let mut m = v9::Migration::<T>::default();
}: {
Expand All @@ -251,7 +251,7 @@ benchmarks! {
whitelisted_caller(), WasmModule::dummy(), vec![],
)?;

v10::store_old_contrat_info::<T>(contract.account_id.clone(), contract.info()?);
v10::store_old_contract_info::<T>(contract.account_id.clone(), contract.info()?);
let mut m = v10::Migration::<T>::default();
}: {
m.step();
Expand All @@ -277,15 +277,15 @@ benchmarks! {
assert_eq!(StorageVersion::get::<Pallet<T>>(), 2);
}

// This benchmarks the weight of executing Migration::migrate when there are no migration in progress.
// This benchmarks the weight of dispatching migrate to executing 1 `NoopMigraton`
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
#[pov_mode = Measured]
migrate {
StorageVersion::new(0).put::<Pallet<T>>();
<Migration::<T> as frame_support::traits::OnRuntimeUpgrade>::on_runtime_upgrade();
let origin: RawOrigin<<T as frame_system::Config>::AccountId> = RawOrigin::Signed(whitelisted_caller());
}: {
<Contracts<T>>::migrate(origin.into(), Weight::MAX).unwrap()
} verify {
let caller: T::AccountId = whitelisted_caller();
let origin = RawOrigin::Signed(caller.clone());
}: _(origin, Weight::MAX)
verify {
assert_eq!(StorageVersion::get::<Pallet<T>>(), 1);
}

Expand Down
32 changes: 26 additions & 6 deletions frame/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
//! an [`eDSL`](https://wiki.haskell.org/Embedded_domain_specific_language) that enables writing
//! WebAssembly based smart contracts in the Rust programming language.

#![allow(rustdoc::private_intra_doc_links)]
#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(feature = "runtime-benchmarks", recursion_limit = "1024")]

Expand Down Expand Up @@ -328,19 +329,36 @@ pub mod pallet {
/// # struct Runtime {};
/// type Migrations = (v9::Migration<Runtime>, v10::Migration<Runtime>, v11::Migration<Runtime>);
/// ```
///
/// If you have a single migration step, you can use a tuple with a single element:
/// ```
/// use pallet_contracts::migration::v9;
/// # struct Runtime {};
/// type Migrations = (v9::Migration<Runtime>,);
/// ```
type Migrations: MigrateSequence;
}

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_idle(_block: T::BlockNumber, remaining_weight: Weight) -> Weight {
use migration::MigrateResult::*;

let (result, weight) = Migration::<T>::migrate(remaining_weight);
let remaining_weight = remaining_weight.saturating_sub(weight);

if !matches!(result, Completed | NoMigrationInProgress) {
return weight
let mut remaining_weight = remaining_weight;
pgherveou marked this conversation as resolved.
Show resolved Hide resolved

loop {
let (result, weight) = Migration::<T>::migrate(remaining_weight);
remaining_weight.saturating_reduce(weight);

match result {
// There is not enough weight to perform a migration, or make any progress, we
// just return the remaining weight.
NoMigrationPerformed | InProgress { steps_done: 0 } => return remaining_weight,
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that the same as just break?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well this case means that there might be a migration in progress, but we did not have enough gas to make any kind of progress, so you want to exit early here, are doing any work in this function is unsafe is there an actual migration going on.

// Migration is still in progress, we can start the next step.
InProgress { .. } => continue,
// Either no migration is in progress, or we are done with all migrations, we
// can do some more other work with the remaining weight.
Completed | NoMigrationInProgress => break,
}
}

ContractInfo::<T>::process_deletion_queue_batch(remaining_weight)
Expand Down Expand Up @@ -987,6 +1005,8 @@ pub mod pallet {
pub(crate) type DeletionQueueCounter<T: Config> =
StorageValue<_, DeletionQueueManager<T>, ValueQuery>;

/// A migration can span across multiple blocks. This storage defines a cursor to track the
/// progress of the migration, enabling us to resume from the last completed position.
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
#[pallet::storage]
pub(crate) type MigrationInProgress<T: Config> =
StorageValue<_, migration::Cursor, OptionQuery>;
Expand Down
100 changes: 54 additions & 46 deletions frame/contracts/src/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,46 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//! Migration framework for pallets.
//! Multi-block Migration framework for pallet-contracts.
//!
//! This module allows us to define a migration as a sequence of [`MigrationStep`]s that can be
//! executed across multiple blocks.
//!
//! # Usage
//!
//! A migration step is defined under `src/migration/vX.rs`, where `X` is the version number.
//! For example, `vX.rs` defines a migration from version `X - 1` to version `X`.
//!
//! ## Example:
//!
//! To configure a migration to `v11` for a runtime using `v8` of pallet-contracts on the chain, you
//! would set the `Migrations` type as follows:
//!
//! ```
//! use pallet_contracts::migration::{v9, v10, v11};
//! # pub enum Runtime {};
//! type Migrations = (v9::Migration<Runtime>, v10::Migration<Runtime>, v11::Migration<Runtime>);
//! ```
//!
//! ## Notes:
//!
//! - Migrations should always be tested with `try-runtime` before being deployed.
//! - By testing with `try-runtime` against a live network, you ensure that all migration steps work
//! and that you have included the required steps.
//!
//! ## Low Level / Implementation Details
//!
//! When a migration starts and [`OnRuntimeUpgrade::on_runtime_upgrade`] is called, instead of
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
//! performing the actual migration, we set a custom storage item [`MigrationInProgress`].
//! This storage item defines a [`Cursor`] for the current migration step.
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
//!
//! If the [`MigrationInProgress`] storage item exists, it means a migration is in progress, and its
//! value holds a cursor for the current migration step. These migration steps are executed during
//! [`Hooks<BlockNumber>::on_idle`] or when the [`Pallet::migrate`] dispatchable is
//! called.
//!
//! While the migration is in progress, all dispatchables except `migrate`, are blocked, and returns
//! a `MigrationInProgress` error.

pub mod v10;
pub mod v11;
Expand Down Expand Up @@ -57,7 +96,7 @@ pub enum IsFinished {
///
/// The migration is done in steps. The migration is finished when
/// `step()` returns `IsFinished::Yes`.
pub trait Migrate: Codec + MaxEncodedLen + Default {
pub trait MigrationStep: Codec + MaxEncodedLen + Default {
/// Returns the version of the migration.
const VERSION: u16;

Expand Down Expand Up @@ -110,7 +149,7 @@ pub trait Migrate: Codec + MaxEncodedLen + Default {
#[derive(frame_support::DefaultNoBound, Encode, Decode, MaxEncodedLen)]
pub struct NoopMigration<const N: u16>;

impl<const N: u16> Migrate for NoopMigration<N> {
impl<const N: u16> MigrationStep for NoopMigration<N> {
const VERSION: u16 = N;
fn max_step_weight() -> Weight {
Weight::zero()
Expand All @@ -122,10 +161,10 @@ impl<const N: u16> Migrate for NoopMigration<N> {
}

mod private {
use crate::migration::Migrate;
use crate::migration::MigrationStep;
pub trait Sealed {}
#[impl_trait_for_tuples::impl_for_tuples(10)]
#[tuple_types_custom_trait_bound(Migrate)]
#[tuple_types_custom_trait_bound(MigrationStep)]
impl Sealed for Tuple {}
}

Expand All @@ -136,9 +175,9 @@ mod private {
pub trait MigrateSequence: private::Sealed {
/// Returns the range of versions that this migration can handle.
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
/// Migrations must be ordered by their versions with no gaps.
/// The following code will fail to compile:
///
/// The following code will fail to compile:
///
/// ```compile_fail
/// # use pallet_contracts::{NoopMigration, MigrateSequence};
/// let _ = <(NoopMigration<1>, NoopMigration<3>)>::VERSION_RANGE;
Expand Down Expand Up @@ -172,21 +211,10 @@ pub trait MigrateSequence: private::Sealed {

/// Returns whether migrating from `in_storage` to `target` is supported.
///
/// A migration is supported if (in_storage + 1, target) is contained by `VERSION_RANGE`.
/// A migration is supported if `VERSION_RANGE` is (in_storage + 1, target).
fn is_upgrade_supported(in_storage: StorageVersion, target: StorageVersion) -> bool {
if in_storage == target {
return true
}
if in_storage > target {
return false
}

let (low, high) = Self::VERSION_RANGE;
let Some(first_supported) = low.checked_sub(1) else {
return false
};

in_storage >= first_supported && target == high
target == high && in_storage + 1 == low
Comment on lines -177 to +219
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if no migration is needed and also none is provided? In this case it would return false and pre_upgrade will fail even though migrations will be fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then you should probably not include it at all in the runtime executive

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as we can catch the case when somebody forgets to put it back in again this is fine. I suppose try-runtime will catch this missing migration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to be lean and embed as little code as possible, we probably want to enforce it, I would be for leaving it like that and adding an explicit message in the pre_upgrade if storage_version == target_version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does indeed, last time I tried, will test it again here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is also only invoked in try_runtime, you will get away with just a warning if you were to run such a migration

if storage_version == latest_version {
log::warn!(
target: LOG_TARGET,
"{name}: No Migration performed storage_version = latest_version = {:?}",
&storage_version
);
return T::WeightInfo::on_runtime_upgrade_noop()
}

}
}

Expand Down Expand Up @@ -306,7 +334,8 @@ pub enum StepResult {
}

impl<T: Config> Migration<T> {
/// Verify that each migration's step of the [`T::Migrations`] sequence fits into `Cursor`.
/// Verify that each migration's step of the [`Config::Migrations`] sequence fits into
/// `Cursor`.
pub(crate) fn integrity_test() {
let max_weight = <T as frame_system::Config>::BlockWeights::get().max_block;
T::Migrations::integrity_test(max_weight)
Expand Down Expand Up @@ -387,7 +416,7 @@ impl<T: Config> Migration<T> {
}

#[impl_trait_for_tuples::impl_for_tuples(10)]
#[tuple_types_custom_trait_bound(Migrate)]
#[tuple_types_custom_trait_bound(MigrationStep)]
impl MigrateSequence for Tuple {
const VERSION_RANGE: (u16, u16) = {
let mut versions: (u16, u16) = (0, 0);
Expand Down Expand Up @@ -487,7 +516,7 @@ mod test {
count: u16,
}

impl<const N: u16> Migrate for MockMigration<N> {
impl<const N: u16> MigrationStep for MockMigration<N> {
const VERSION: u16 = N;
fn max_step_weight() -> Weight {
Weight::from_all(1)
Expand All @@ -512,30 +541,9 @@ mod test {
#[test]
fn is_upgrade_supported_works() {
type Migrations = (MockMigration<9>, MockMigration<10>, MockMigration<11>);

[(1, 1), (8, 11), (9, 11)].into_iter().for_each(|(from, to)| {
assert!(
Migrations::is_upgrade_supported(
StorageVersion::new(from),
StorageVersion::new(to)
),
"{} -> {} is supported",
from,
to
)
});

[(1, 0), (0, 3), (7, 11), (8, 10)].into_iter().for_each(|(from, to)| {
assert!(
!Migrations::is_upgrade_supported(
StorageVersion::new(from),
StorageVersion::new(to)
),
"{} -> {} is not supported",
from,
to
)
});
assert!(Migrations::is_upgrade_supported(StorageVersion::new(8), StorageVersion::new(11)));
assert!(!Migrations::is_upgrade_supported(StorageVersion::new(9), StorageVersion::new(11)));
assert!(!Migrations::is_upgrade_supported(StorageVersion::new(8), StorageVersion::new(12)));
}

#[test]
Expand Down
18 changes: 9 additions & 9 deletions frame/contracts/src/migration/v10.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
// limitations under the License.

//! Don't rely on reserved balances keeping an account alive
//! See <https://github.com/paritytech/substrate/pull/13370>.
//! See <https://github.com/paritytech/substrate/pull/13369>.

use crate::{
address::AddressGenerator,
exec::AccountIdOf,
migration::{IsFinished, Migrate},
migration::{IsFinished, MigrationStep},
weights::WeightInfo,
BalanceOf, CodeHash, Config, Pallet, TrieId, Weight, LOG_TARGET,
};
Expand Down Expand Up @@ -69,7 +69,7 @@ mod old {
}

#[cfg(feature = "runtime-benchmarks")]
pub fn store_old_contrat_info<T: Config>(account: T::AccountId, info: crate::ContractInfo<T>) {
pub fn store_old_contract_info<T: Config>(account: T::AccountId, info: crate::ContractInfo<T>) {
let info = old::ContractInfo {
trie_id: info.trie_id,
code_hash: info.code_hash,
Expand Down Expand Up @@ -117,7 +117,7 @@ pub struct Migration<T: Config> {
type ContractInfoOf<T: Config> =
StorageMap<Pallet<T>, Twox64Concat, <T as frame_system::Config>::AccountId, ContractInfo<T>>;

impl<T: Config> Migrate for Migration<T> {
impl<T: Config> MigrationStep for Migration<T> {
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
const VERSION: u16 = 10;

fn max_step_weight() -> Weight {
Expand Down Expand Up @@ -181,14 +181,14 @@ impl<T: Config> Migrate for Migration<T> {
})
// If it fails we fallback to minting the ED.
.unwrap_or_else(|err| {
log::error!(target: LOG_TARGET, "Failed to transfer ED, reason: {:?}", err);
log::error!(target: LOG_TARGET, "Failed to transfer the base deposit, reason: {:?}", err);
T::Currency::deposit_creating(&deposit_account, min_balance);
min_balance
});

// Calculate the new base_deposit to store in the contract:
// Ideally: it should be the same as the old one
// Ideally, it should be at least 2xED (for the contract and deposit account).
// Ideally, it should be the same as the old one
// Ideally, it should be at least 2xED (for the contract and deposit accounts).
// It can't be more than the `new_deposit`.
let new_base_deposit = min(
max(contract.storage_base_deposit, min_balance.saturating_add(min_balance)),
Expand Down Expand Up @@ -240,8 +240,8 @@ impl<T: Config> Migrate for Migration<T> {

#[cfg(feature = "try-runtime")]
fn post_upgrade_step(state: Vec<u8>) -> Result<(), TryRuntimeError> {
let sample =
<Vec<(T::AccountId, old::ContractInfo<T>)> as Decode>::decode(&mut &state[..]).unwrap();
let sample = <Vec<(T::AccountId, old::ContractInfo<T>)> as Decode>::decode(&mut &state[..])
.expect("pre_upgrade_step provides a valid state; qed");

log::debug!(target: LOG_TARGET, "Validating sample of {} contracts", sample.len());
for (account, old_contract) in sample {
Expand Down
7 changes: 4 additions & 3 deletions frame/contracts/src/migration/v11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
//! See <https://github.com/paritytech/substrate/pull/13702>.

use crate::{
migration::{IsFinished, Migrate},
migration::{IsFinished, MigrationStep},
weights::WeightInfo,
Config, Pallet, TrieId, Weight, LOG_TARGET,
};
Expand Down Expand Up @@ -69,7 +69,7 @@ pub struct Migration<T: Config> {
_phantom: PhantomData<T>,
}

impl<T: Config> Migrate for Migration<T> {
impl<T: Config> MigrationStep for Migration<T> {
const VERSION: u16 = 11;

// It would be more correct to make our use the now removed [DeletionQueueDepth](https://github.com/paritytech/substrate/pull/13702/files#diff-70e9723e9db62816e35f6f885b6770a8449c75a6c2733e9fa7a245fe52c4656c)
Expand Down Expand Up @@ -121,7 +121,8 @@ impl<T: Config> Migrate for Migration<T> {

#[cfg(feature = "try-runtime")]
fn post_upgrade_step(state: Vec<u8>) -> Result<(), TryRuntimeError> {
let len = <u32 as Decode>::decode(&mut &state[..]).unwrap();
let len = <u32 as Decode>::decode(&mut &state[..])
.expect("pre_upgrade_step provides a valid state; qed");
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
let counter = <DeletionQueueCounter<T>>::get();
ensure!(counter.insert_counter == len, "invalid insert counter");
ensure!(counter.delete_counter == 0, "invalid delete counter");
Expand Down
Loading