Skip to content

Commit

Permalink
contracts: Multi block migrations (paritytech#14045)
Browse files Browse the repository at this point in the history
* Frame Add translate_next

This works similarly to to `translate` but only translate a single entry.
This function will be useful in the context of multi-block migration.

* Move to lazy migration

* Updates

* simplify MockMigration

* wip

* wip

* add bench

* add bench

* fmt

* fix bench

* add .

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_contracts

* Apply suggestions from code review

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* Scalfold v10 / v11 fix tests

* PR comment

* tweak pub use

* wip

* wip

* wip

* misc merge master

* misc merge master

* wip

* rm tmp stuff

* wip

* wip

* wip

* wip

* wip

* fixes

* add state

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix

* fixed compilation

* clean up logs

* wip

* Revert "Frame Add translate_next"

This reverts commit b878662.

* Fix v10 logic

* Apply suggestions from code review

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* wip

* fixes

* exercise del_queue

* bump sample size

* fmt

* wip

* blank line

* fix lint

* fix rustdoc job lint

* PR comment do not use dangerous into()

* Ad macros for updating mod visibility

* Add doc

* Add max_weight to integrity_test

* fix compilation

* Add no migration tests

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_contracts

* fix clippy

* PR review

* Update frame/contracts/src/lib.rs

Co-authored-by: Sasha Gryaznov <hi@agryaznov.com>

* Fix master merge

* fix merge 2

* fix tryruntime

* fix lint

---------

Co-authored-by: Alexander Theißen <alex.theissen@me.com>
Co-authored-by: command-bot <>
Co-authored-by: Sasha Gryaznov <hi@agryaznov.com>
  • Loading branch information
3 people authored and nathanwhit committed Jul 19, 2023
1 parent 73c4f38 commit 06ecbbe
Show file tree
Hide file tree
Showing 10 changed files with 2,803 additions and 1,444 deletions.
2 changes: 2 additions & 0 deletions frame/contracts/primitives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ pub enum ContractAccessError {
DoesntExist,
/// Storage key cannot be decoded from the provided input data.
KeyDecodingFailed,
/// Storage is migrating. Try again later.
MigrationInProgress,
}

bitflags! {
Expand Down
92 changes: 90 additions & 2 deletions frame/contracts/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

mod code;
mod sandbox;

use self::{
code::{
body::{self, DynInstr::*},
Expand All @@ -31,12 +30,13 @@ use self::{
};
use crate::{
exec::{AccountIdOf, Key},
migration::{v10, v11, v9, Migrate},
wasm::CallFlags,
Pallet as Contracts, *,
};
use codec::{Encode, MaxEncodedLen};
use frame_benchmarking::v1::{account, benchmarks, whitelisted_caller};
use frame_support::weights::Weight;
use frame_support::{pallet_prelude::StorageVersion, weights::Weight};
use frame_system::RawOrigin;
use sp_runtime::{
traits::{Bounded, Hash},
Expand Down Expand Up @@ -234,6 +234,94 @@ benchmarks! {
Contracts::<T>::reinstrument_module(&mut module, &schedule)?;
}

// 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());
v9::store_old_dummy_code::<T>(c as usize);
let mut m = v9::Migration::<T>::default();
}: {
m.step();
}

// This benchmarks the v10 migration step. (use dedicated deposit_account)
#[pov_mode = Measured]
v10_migration_step {
let contract = <Contract<T>>::with_caller(
whitelisted_caller(), WasmModule::dummy(), vec![],
)?;

v10::store_old_contrat_info::<T>(contract.account_id.clone(), contract.info()?);
let mut m = v10::Migration::<T>::default();
}: {
m.step();
}

// This benchmarks the v11 migration step.
#[pov_mode = Measured]
v11_migration_step {
let k in 0 .. 1024;
v11::fill_old_queue::<T>(k as usize);
let mut m = v11::Migration::<T>::default();
}: {
m.step();
}

// This benchmarks the weight of executing Migration::migrate to execute a noop migration.
#[pov_mode = Measured]
migration_noop {
assert_eq!(StorageVersion::get::<Pallet<T>>(), 2);
}: {
Migration::<T>::migrate(Weight::MAX)
} verify {
assert_eq!(StorageVersion::get::<Pallet<T>>(), 2);
}

// This benchmarks the weight of executing Migration::migrate when there are no migration in progress.
#[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 {
assert_eq!(StorageVersion::get::<Pallet<T>>(), 1);
}

// This benchmarks the weight of running on_runtime_upgrade when there are no migration in progress.
#[pov_mode = Measured]
on_runtime_upgrade_noop {
assert_eq!(StorageVersion::get::<Pallet<T>>(), 2);
}: {
<Migration::<T> as frame_support::traits::OnRuntimeUpgrade>::on_runtime_upgrade()
} verify {
assert!(MigrationInProgress::<T>::get().is_none());
}

// This benchmarks the weight of running on_runtime_upgrade when there is a migration in progress.
#[pov_mode = Measured]
on_runtime_upgrade_in_progress {
StorageVersion::new(0).put::<Pallet<T>>();
let v = vec![42u8].try_into().ok();
MigrationInProgress::<T>::set(v.clone());
}: {
<Migration::<T> as frame_support::traits::OnRuntimeUpgrade>::on_runtime_upgrade()
} verify {
assert!(MigrationInProgress::<T>::get().is_some());
assert_eq!(MigrationInProgress::<T>::get(), v);
}

// This benchmarks the weight of running on_runtime_upgrade when there is a migration to process.
#[pov_mode = Measured]
on_runtime_upgrade {
StorageVersion::new(0).put::<Pallet<T>>();
}: {
<Migration::<T> as frame_support::traits::OnRuntimeUpgrade>::on_runtime_upgrade()
} verify {
assert!(MigrationInProgress::<T>::get().is_some());
}

// This benchmarks the overhead of loading a code of size `c` byte from storage and into
// the sandbox. This does **not** include the actual execution for which the gas meter
// is responsible. This is achieved by generating all code to the `deploy` function
Expand Down
95 changes: 89 additions & 6 deletions frame/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ use crate::{
use codec::{Codec, Decode, Encode, HasCompact};
use environmental::*;
use frame_support::{
dispatch::{DispatchError, Dispatchable, GetDispatchInfo, Pays, PostDispatchInfo, RawOrigin},
dispatch::{
DispatchError, Dispatchable, GetDispatchInfo, Pays, PostDispatchInfo, RawOrigin,
WithPostDispatchInfo,
},
ensure,
error::BadOrigin,
traits::{
Expand All @@ -120,18 +123,18 @@ use frame_support::{
use frame_system::{ensure_signed, pallet_prelude::OriginFor, EventRecord, Pallet as System};
use pallet_contracts_primitives::{
Code, CodeUploadResult, CodeUploadReturnValue, ContractAccessError, ContractExecResult,
ContractInstantiateResult, ExecReturnValue, GetStorageResult, InstantiateReturnValue,
StorageDeposit,
ContractInstantiateResult, ContractResult, ExecReturnValue, GetStorageResult,
InstantiateReturnValue, StorageDeposit,
};
use scale_info::TypeInfo;
use smallvec::Array;
use sp_runtime::traits::{Convert, Hash, Saturating, StaticLookup};
use sp_runtime::traits::{Convert, Hash, Saturating, StaticLookup, Zero};
use sp_std::{fmt::Debug, marker::PhantomData, prelude::*};

pub use crate::{
address::{AddressGenerator, DefaultAddressGenerator},
exec::Frame,
migration::Migration,
migration::{MigrateSequence, Migration, NoopMigration},
pallet::*,
schedule::{HostFnWeights, InstructionWeights, Limits, Schedule},
wasm::Determinism,
Expand Down Expand Up @@ -179,7 +182,12 @@ pub mod pallet {
use frame_system::pallet_prelude::*;

/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(9);
#[cfg(not(any(test, feature = "runtime-benchmarks")))]
const STORAGE_VERSION: StorageVersion = StorageVersion::new(11);

/// Hard coded storage version for running tests that depend on the current storage version.
#[cfg(any(test, feature = "runtime-benchmarks"))]
const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
Expand Down Expand Up @@ -316,11 +324,22 @@ pub mod pallet {
#[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
}

ContractInfo::<T>::process_deletion_queue_batch(remaining_weight)
.saturating_add(T::WeightInfo::on_process_deletion_queue_batch())
}

fn integrity_test() {
Migration::<T>::integrity_test();

// Total runtime memory limit
let max_runtime_mem: u32 = T::Schedule::get().limits.runtime_memory;
// Memory limits for a single contract:
Expand Down Expand Up @@ -499,6 +518,7 @@ pub mod pallet {
storage_deposit_limit: Option<<BalanceOf<T> as codec::HasCompact>::Type>,
determinism: Determinism,
) -> DispatchResult {
Migration::<T>::ensure_migrated()?;
let origin = ensure_signed(origin)?;
Self::bare_upload_code(origin, code, storage_deposit_limit.map(Into::into), determinism)
.map(|_| ())
Expand All @@ -514,6 +534,7 @@ pub mod pallet {
origin: OriginFor<T>,
code_hash: CodeHash<T>,
) -> DispatchResultWithPostInfo {
Migration::<T>::ensure_migrated()?;
let origin = ensure_signed(origin)?;
<PrefabWasmModule<T>>::remove(&origin, code_hash)?;
// we waive the fee because removing unused code is beneficial
Expand All @@ -537,6 +558,7 @@ pub mod pallet {
dest: AccountIdLookupOf<T>,
code_hash: CodeHash<T>,
) -> DispatchResult {
Migration::<T>::ensure_migrated()?;
ensure_root(origin)?;
let dest = T::Lookup::lookup(dest)?;
<ContractInfoOf<T>>::try_mutate(&dest, |contract| {
Expand Down Expand Up @@ -586,6 +608,7 @@ pub mod pallet {
storage_deposit_limit: Option<<BalanceOf<T> as codec::HasCompact>::Type>,
data: Vec<u8>,
) -> DispatchResultWithPostInfo {
Migration::<T>::ensure_migrated()?;
let common = CommonInput {
origin: Origin::from_runtime_origin(origin)?,
value,
Expand Down Expand Up @@ -645,6 +668,7 @@ pub mod pallet {
data: Vec<u8>,
salt: Vec<u8>,
) -> DispatchResultWithPostInfo {
Migration::<T>::ensure_migrated()?;
let code_len = code.len() as u32;
let data_len = data.len() as u32;
let salt_len = salt.len() as u32;
Expand Down Expand Up @@ -687,6 +711,7 @@ pub mod pallet {
data: Vec<u8>,
salt: Vec<u8>,
) -> DispatchResultWithPostInfo {
Migration::<T>::ensure_migrated()?;
let data_len = data.len() as u32;
let salt_len = salt.len() as u32;
let common = CommonInput {
Expand All @@ -709,6 +734,33 @@ pub mod pallet {
T::WeightInfo::instantiate(data_len, salt_len),
)
}

/// When a migration is in progress, this dispatchable can be used to run migration steps.
/// Calls that contribute to advancing the migration have their fees waived, as it's helpful
/// for the chain. Note that while the migration is in progress, the pallet will also
/// leverage the `on_idle` hooks to run migration steps.
#[pallet::call_index(9)]
#[pallet::weight(T::WeightInfo::migrate().saturating_add(*weight_limit))]
pub fn migrate(origin: OriginFor<T>, weight_limit: Weight) -> DispatchResultWithPostInfo {
use migration::MigrateResult::*;
ensure_signed(origin)?;

let weight_limit = weight_limit.saturating_add(T::WeightInfo::migrate());
let (result, weight) = Migration::<T>::migrate(weight_limit);

match result {
Completed =>
Ok(PostDispatchInfo { actual_weight: Some(weight), pays_fee: Pays::No }),
InProgress { steps_done, .. } if steps_done > 0 =>
Ok(PostDispatchInfo { actual_weight: Some(weight), pays_fee: Pays::No }),
InProgress { .. } =>
Ok(PostDispatchInfo { actual_weight: Some(weight), pays_fee: Pays::Yes }),
NoMigrationInProgress | NoMigrationPerformed => {
let err: DispatchError = <Error<T>>::NoMigrationPerformed.into();
Err(err.with_weight(T::WeightInfo::migrate()))
},
}
}
}

#[pallet::event]
Expand Down Expand Up @@ -861,6 +913,10 @@ pub mod pallet {
CodeRejected,
/// An indetermistic code was used in a context where this is not permitted.
Indeterministic,
/// A pending migration needs to complete before the extrinsic can be called.
MigrationInProgress,
/// Migrate dispatch call was attempted but no migration was performed.
NoMigrationPerformed,
}

/// A mapping from an original code hash to the original code, untouched by instrumentation.
Expand Down Expand Up @@ -920,6 +976,10 @@ pub mod pallet {
#[pallet::storage]
pub(crate) type DeletionQueueCounter<T: Config> =
StorageValue<_, DeletionQueueManager<T>, ValueQuery>;

#[pallet::storage]
pub(crate) type MigrationInProgress<T: Config> =
StorageValue<_, migration::Cursor, OptionQuery>;
}

/// The type of origins supported by the contracts pallet.
Expand Down Expand Up @@ -1210,6 +1270,21 @@ impl<T: Config> Invokable<T> for InstantiateInput<T> {
}
}

macro_rules! ensure_no_migration_in_progress {
() => {
if Migration::<T>::in_progress() {
return ContractResult {
gas_consumed: Zero::zero(),
gas_required: Zero::zero(),
storage_deposit: Default::default(),
debug_message: Vec::new(),
result: Err(Error::<T>::MigrationInProgress.into()),
events: None,
}
}
};
}

impl<T: Config> Pallet<T> {
/// Perform a call to a specified contract.
///
Expand All @@ -1234,6 +1309,8 @@ impl<T: Config> Pallet<T> {
collect_events: CollectEvents,
determinism: Determinism,
) -> ContractExecResult<BalanceOf<T>, EventRecordOf<T>> {
ensure_no_migration_in_progress!();

let mut debug_message = if matches!(debug, DebugInfo::UnsafeDebug) {
Some(DebugBufferVec::<T>::default())
} else {
Expand Down Expand Up @@ -1290,6 +1367,8 @@ impl<T: Config> Pallet<T> {
debug: DebugInfo,
collect_events: CollectEvents,
) -> ContractInstantiateResult<T::AccountId, BalanceOf<T>, EventRecordOf<T>> {
ensure_no_migration_in_progress!();

let mut debug_message = if debug == DebugInfo::UnsafeDebug {
Some(DebugBufferVec::<T>::default())
} else {
Expand Down Expand Up @@ -1333,6 +1412,7 @@ impl<T: Config> Pallet<T> {
storage_deposit_limit: Option<BalanceOf<T>>,
determinism: Determinism,
) -> CodeUploadResult<CodeHash<T>, BalanceOf<T>> {
Migration::<T>::ensure_migrated()?;
let schedule = T::Schedule::get();
let module = PrefabWasmModule::from_code(
code,
Expand All @@ -1353,6 +1433,9 @@ impl<T: Config> Pallet<T> {

/// Query storage of a specified contract under a specified key.
pub fn get_storage(address: T::AccountId, key: Vec<u8>) -> GetStorageResult {
if Migration::<T>::in_progress() {
return Err(ContractAccessError::MigrationInProgress)
}
let contract_info =
ContractInfoOf::<T>::get(&address).ok_or(ContractAccessError::DoesntExist)?;

Expand Down
Loading

0 comments on commit 06ecbbe

Please sign in to comment.