diff --git a/frame/contracts/src/benchmarking/mod.rs b/frame/contracts/src/benchmarking/mod.rs index 6ebfb18509b1c..252daa4c2e1fb 100644 --- a/frame/contracts/src/benchmarking/mod.rs +++ b/frame/contracts/src/benchmarking/mod.rs @@ -234,6 +234,16 @@ benchmarks! { Contracts::::reinstrument_module(&mut module, &schedule)?; } + // This benchmarks the v9 migration cost + #[pov_mode = Measured] + v9_translate_wasm_module { + let c in 0 .. Perbill::from_percent(49).mul_ceil(T::MaxCodeLen::get()); + migration::v9::store_old_dummy_code::(c as usize); + let mut migration = migration::v9::Migration::::default(); + }: { + migration.step(); + } + // 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 diff --git a/frame/contracts/src/lib.rs b/frame/contracts/src/lib.rs index 41d510aa01e20..391aac43f4907 100644 --- a/frame/contracts/src/lib.rs +++ b/frame/contracts/src/lib.rs @@ -131,7 +131,7 @@ use sp_std::{fmt::Debug, marker::PhantomData, prelude::*}; pub use crate::{ address::{AddressGenerator, DefaultAddressGenerator}, exec::Frame, - migration::Migration, + migration::*, pallet::*, schedule::{HostFnWeights, InstructionWeights, Limits, Schedule}, wasm::Determinism, @@ -177,8 +177,12 @@ pub mod pallet { use frame_system::pallet_prelude::*; /// The current storage version. + #[cfg(not(test))] const STORAGE_VERSION: StorageVersion = StorageVersion::new(9); + #[cfg(test)] + const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); + #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(PhantomData); @@ -314,10 +318,12 @@ pub mod pallet { #[pallet::hooks] impl Hooks> for Pallet { fn on_idle(_block: T::BlockNumber, remaining_weight: Weight) -> Weight { - // TODO: run migration here - if Migration::::ensure_migrated().is_err() { - return T::DbWeight::get().reads(1) - } + let remaining_weight = match Migration::::migrate(remaining_weight) { + MigrateResult::InProgress(weight) => return remaining_weight.saturating_sub(weight), + MigrateResult::NoMigrationPerformed(weight) | MigrateResult::Completed(weight) => + remaining_weight.saturating_sub(weight), + }; + ContractInfo::::process_deletion_queue_batch(remaining_weight) .saturating_add(T::WeightInfo::on_process_deletion_queue_batch()) } @@ -724,15 +730,23 @@ pub mod pallet { ) } - /// TODO: add benchmark for base weight - /// TODO: add a minimum weight limit so that people don't spam small free extrinsics #[pallet::call_index(9)] - #[pallet::weight(*weight_limit)] + #[pallet::weight(T::WeightInfo::migrate().saturating_add(*weight_limit))] pub fn migrate(origin: OriginFor, weight_limit: Weight) -> DispatchResultWithPostInfo { ensure_signed(origin)?; - Migration::::migrate(weight_limit) - .map(|weight| PostDispatchInfo { actual_weight: Some(weight), pays_fee: Pays::No }) - .map_err(|(weight, error)| error.with_weight(weight)) + ensure!( + weight_limit.any_gt(T::WeightInfo::migrate().saturating_mul(10)), + Error::::MigrateWeightLimitTooLow + ); + + match Migration::::migrate(weight_limit) { + MigrateResult::InProgress(weight) | MigrateResult::Completed(weight) => + Ok(PostDispatchInfo { actual_weight: Some(weight), pays_fee: Pays::No }), + MigrateResult::NoMigrationPerformed(weight) => { + let err: DispatchError = >::NoMigrationPerformed.into(); + Err(err.with_weight(weight)) + }, + } } } @@ -886,8 +900,12 @@ 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, + /// The weight limit for a migration dispatch call was too low. + MigrateWeightLimitTooLow, } /// A mapping from an original code hash to the original code, untouched by instrumentation. diff --git a/frame/contracts/src/migration.rs b/frame/contracts/src/migration.rs index 1d69baad453b5..48b9ef6bece16 100644 --- a/frame/contracts/src/migration.rs +++ b/frame/contracts/src/migration.rs @@ -15,6 +15,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(feature = "runtime-benchmarks")] +pub mod v9; +#[cfg(not(feature = "runtime-benchmarks"))] mod v9; use crate::{Config, Error, MigrationInProgress, Pallet, Weight, LOG_TARGET}; @@ -24,38 +27,97 @@ use frame_support::{ pallet_prelude::*, traits::{ConstU32, Get, OnRuntimeUpgrade}, }; -use sp_std::{marker::PhantomData, prelude::*}; +use sp_std::marker::PhantomData; + +#[cfg(feature = "try-runtime")] +use sp_std::prelude::*; const PROOF_ENCODE: &str = "Tuple::max_encoded_len() < Cursor::max_encoded_len()` is verified in `Self::integrity_test()`; qed"; const PROOF_DECODE: &str = "We encode to the same type in this trait only. No other code touches this item; qed"; -const PROOF_EXISTS: &str = "Required migration not supported by this runtime. This is a bug."; + +fn invalid_version(version: StorageVersion) -> ! { + panic!("Required migration {version:?} not supported by this runtime. This is a bug."); +} pub type Cursor = BoundedVec>; -type Migrations = (v9::Migration,); +type Migrations = (NoopMigration<7>, NoopMigration<8>, v9::Migration); -enum IsFinished { +/// IsFinished describes whether a migration is finished or not. +pub enum IsFinished { Yes, No, } -trait Migrate: Codec + MaxEncodedLen + Default { +/// A trait that allows to migrate storage from one version to another. +/// The migration is done in steps. The migration is finished when +/// `step()` returns `IsFinished::Yes`. +pub trait Migrate: Codec + MaxEncodedLen + Default { + /// Returns the version of the migration. const VERSION: u16; + /// Returns the maximum weight that can be consumed in a single step. fn max_step_weight() -> Weight; - fn step(&mut self) -> (IsFinished, Option); + /// Process one step of the migration. + /// Returns whether the migration is finished and the weight consumed. + fn step(&mut self) -> (IsFinished, Weight); +} + +/// A noop migration that can be used when there is no migration to be done for a given version. +#[derive(frame_support::DefaultNoBound, Encode, Decode, MaxEncodedLen)] +pub struct NoopMigration; + +impl Migrate for NoopMigration { + const VERSION: u16 = N; + fn max_step_weight() -> Weight { + Weight::zero() + } + fn step(&mut self) -> (IsFinished, Weight) { + log::debug!(target: LOG_TARGET, "No migration for version {}", N); + (IsFinished::Yes, Weight::zero()) + } } -trait MigrateSequence { - const VERSION_RANGE: Option<(u16, u16)>; +mod private { + pub trait Sealed {} + #[impl_trait_for_tuples::impl_for_tuples(10)] + #[tuple_types_custom_trait_bound(crate::Migrate)] + impl Sealed for Tuple {} +} +/// Defines a sequence of migrations. +/// The sequence must be defined by a tuple of migrations, each of which must implement the +/// `Migrate` trait. Migrations must be ordered by their versions with no gaps. +pub trait MigrateSequence: private::Sealed { + /// Returns the range of versions that this migration can handle. + /// 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; + /// ``` + /// The following code will compile: + /// ``` + /// # use pallet_contracts::{NoopMigration, MigrateSequence}; + /// let _ = <(NoopMigration<1>, NoopMigration<2>)>::VERSION_RANGE; + /// ``` + const VERSION_RANGE: (u16, u16); + + /// Returns the default cursor for the given version. fn new(version: StorageVersion) -> Cursor; + /// Execute the migration step until the weight limit is reached. + /// Returns the new cursor or `None` if the migration is finished. fn steps(version: StorageVersion, cursor: &[u8], weight_left: &mut Weight) -> Option; + /// Verify that each migration's cursor fits into the Cursor type. fn integrity_test(); + /// Returns whether migration from `in_storage` to `target` is supported. + /// A migration is supported if (in_storage + 1, target) == VERSION_RANGE. fn is_upgrade_supported(in_storage: StorageVersion, target: StorageVersion) -> bool { if in_storage == target { return true @@ -63,9 +125,8 @@ trait MigrateSequence { if in_storage > target { return false } - let Some((low, high)) = Self::VERSION_RANGE else { - return false - }; + + let (low, high) = Self::VERSION_RANGE; let Some(first_supported) = low.checked_sub(1) else { return false }; @@ -74,9 +135,9 @@ trait MigrateSequence { } /// Performs all necessary migrations based on `StorageVersion`. -pub struct Migration(PhantomData); +pub struct Migration>(PhantomData<(T, M)>); -impl OnRuntimeUpgrade for Migration { +impl OnRuntimeUpgrade for Migration { fn on_runtime_upgrade() -> Weight { let latest_version = >::current_storage_version(); let storage_version = >::on_chain_storage_version(); @@ -98,10 +159,26 @@ impl OnRuntimeUpgrade for Migration { "RuntimeUpgraded. Upgrading storage from {storage_version:?} to {latest_version:?}.", ); - let cursor = Migrations::::new(storage_version + 1); + let cursor = M::new(storage_version + 1); MigrationInProgress::::set(Some(cursor)); weight.saturating_accrue(T::DbWeight::get().writes(1)); + // drive the migration to completion when using try-runtime + if cfg!(feature = "try-runtime") { + loop { + use MigrateResult::*; + match Migration::::migrate(Weight::MAX) { + InProgress(w) => { + weight.saturating_add(w); + }, + NoMigrationPerformed(w) | Completed(w) => { + weight.saturating_add(w); + break + }, + } + } + } + weight } @@ -112,72 +189,94 @@ impl OnRuntimeUpgrade for Migration { // over our migrations. let storage_version = >::on_chain_storage_version(); let target_version = >::current_storage_version(); - if Migrations::::is_upgrade_supported(storage_version, target_version) { + if M::is_upgrade_supported(storage_version, target_version) { Ok(Vec::new()) } else { + log::error!( + target: LOG_TARGET, + "Range supported {:?}, range requested {:?}", + M::VERSION_RANGE, + (storage_version, target_version) + ); Err("New runtime does not contain the required migrations to perform this upgrade.") } } } -impl Migration { +/// The result of a migration step. +pub enum MigrateResult { + NoMigrationPerformed(Weight), + Completed(Weight), + InProgress(Weight), +} + +impl Migration { pub(crate) fn integrity_test() { - Migrations::::integrity_test() + M::integrity_test() } - pub(crate) fn migrate(weight_limit: Weight) -> Result { + /// Migrate + /// Return the weight used and whether or not a migration is in progress + pub(crate) fn migrate(weight_limit: Weight) -> MigrateResult { let mut weight_left = weight_limit; // for mutating `MigrationInProgress` and `StorageVersion` - weight_left - .checked_reduce(T::DbWeight::get().reads_writes(2, 2)) - .ok_or_else(|| (0.into(), Error::::NoMigrationPerformed.into()))?; + if weight_left.checked_reduce(T::DbWeight::get().reads_writes(2, 2)).is_none() { + return MigrateResult::NoMigrationPerformed(Weight::zero()) + } - MigrationInProgress::::try_mutate_exists(|progress| { - let cursor_before = progress.as_mut().ok_or_else(|| { - (weight_limit.saturating_sub(weight_left), Error::::NoMigrationPerformed.into()) - })?; + MigrationInProgress::::mutate_exists(|progress| { + let Some(cursor_before) = progress.as_mut() else { + return MigrateResult::NoMigrationPerformed(weight_limit.saturating_sub(weight_left)) + }; // if a migration is running it is always upgrading to the next version let storage_version = >::on_chain_storage_version(); let in_progress_version = storage_version + 1; - *progress = match Migrations::::steps( + log::info!( + target: LOG_TARGET, + "Migrating from {:?} to {:?},", + storage_version, in_progress_version, - cursor_before.as_ref(), - &mut weight_left, - ) { - // ongoing - Some(cursor) => { - // refund as we did not update the storage version - weight_left.saturating_accrue(T::DbWeight::get().writes(1)); - // we still have a cursor which keeps the pallet disabled - Some(cursor) - }, - // finished - None => { - in_progress_version.put::>(); - if >::current_storage_version() != in_progress_version { - // chain the next migration - log::info!( - target: LOG_TARGET, - "Started migrating to {:?},", - in_progress_version + 1, - ); - Some(Migrations::::new(in_progress_version + 1)) - } else { - // enable pallet by removing the storage item - log::info!( - target: LOG_TARGET, - "All migrations done. At version {:?},", - in_progress_version + 1, - ); - None - } - }, - }; + ); + + *progress = + match M::steps(in_progress_version, cursor_before.as_ref(), &mut weight_left) { + // ongoing + Some(cursor) => { + // refund as we did not update the storage version + weight_left.saturating_accrue(T::DbWeight::get().writes(1)); + // we still have a cursor which keeps the pallet disabled + Some(cursor) + }, + // finished + None => { + in_progress_version.put::>(); + if >::current_storage_version() != in_progress_version { + // chain the next migration + log::info!( + target: LOG_TARGET, + "Next migration is {:?},", + in_progress_version + 1, + ); + Some(M::new(in_progress_version + 1)) + } else { + // enable pallet by removing the storage item + log::info!( + target: LOG_TARGET, + "All migrations done. At version {:?},", + in_progress_version, + ); + None + } + }, + }; - Ok(weight_limit.saturating_sub(weight_left)) + match progress { + Some(_) => MigrateResult::InProgress(weight_limit.saturating_sub(weight_left)), + None => MigrateResult::Completed(weight_limit.saturating_sub(weight_left)), + } }) } @@ -195,18 +294,18 @@ impl Migration { } #[impl_trait_for_tuples::impl_for_tuples(10)] -#[tuple_types_custom_trait_bound(Migrate)] -impl MigrateSequence for Tuple { - const VERSION_RANGE: Option<(u16, u16)> = { - let mut versions: Option<(u16, u16)> = None; +#[tuple_types_custom_trait_bound(Migrate)] +impl MigrateSequence for Tuple { + const VERSION_RANGE: (u16, u16) = { + let mut versions: (u16, u16) = (0, 0); for_tuples!( #( match versions { - None => { - versions = Some((Tuple::VERSION, Tuple::VERSION)); + (0, 0) => { + versions = (Tuple::VERSION, Tuple::VERSION); }, - Some((min_version, last_version)) if Tuple::VERSION == last_version + 1 => { - versions = Some((min_version, Tuple::VERSION)); + (min_version, last_version) if Tuple::VERSION == last_version + 1 => { + versions = (min_version, Tuple::VERSION); }, _ => panic!("Migrations must be ordered by their versions with no gaps.") } @@ -223,7 +322,7 @@ impl MigrateSequence for Tuple { } )* ); - panic!("{PROOF_EXISTS}") + invalid_version(version) } fn steps( @@ -239,7 +338,7 @@ impl MigrateSequence for Tuple { let max_weight = Tuple::max_step_weight(); while weight_left.all_gt(max_weight) { let (finished, weight) = migration.step(); - weight_left.saturating_reduce(weight.unwrap_or(max_weight)); + weight_left.saturating_reduce(weight); if matches!(finished, IsFinished::Yes) { return None } @@ -248,7 +347,7 @@ impl MigrateSequence for Tuple { } )* ); - panic!("{PROOF_EXISTS}") + invalid_version(version) } fn integrity_test() { @@ -270,8 +369,31 @@ impl MigrateSequence for Tuple { #[cfg(test)] mod test { + use frame_support::assert_err; + use super::*; - use crate::tests::Test; + use crate::tests::{ExtBuilder, Test}; + + #[derive(frame_support::DefaultNoBound, Encode, Decode, MaxEncodedLen)] + struct MockMigration { + _phantom: sp_std::marker::PhantomData, + last_key: u16, + } + + impl Migrate for MockMigration { + const VERSION: u16 = N; + fn max_step_weight() -> Weight { + Weight::from_all(1) + } + fn step(&mut self) -> (IsFinished, Weight) { + self.last_key += 1; + if self.last_key == N { + (IsFinished::Yes, Weight::from_all(1)) + } else { + (IsFinished::No, Weight::from_all(1)) + } + } + } #[test] fn check_versions() { @@ -279,4 +401,74 @@ mod test { // otherwise it will only be evaluated when the whole runtime is build let _ = Migrations::::VERSION_RANGE; } + + #[test] + fn version_range_works() { + let range = <(MockMigration<1, Test>, MockMigration<2, Test>)>::VERSION_RANGE; + assert_eq!(range, (1, 2)); + } + + #[test] + fn is_upgrade_supported_works() { + type M = (MockMigration<2, Test>, MockMigration<3, Test>); + let is_upgrade_supported = |(from, to)| { + M::is_upgrade_supported(StorageVersion::new(from), StorageVersion::new(to)) + }; + + // valid upgrades + [(1, 1), (1, 3)].into_iter().map(is_upgrade_supported).for_each(|r| assert!(r)); + + // invalid upgrades + [(1, 0), (0, 3), (1, 4)] + .into_iter() + .map(is_upgrade_supported) + .for_each(|r| assert!(!r)); + } + + #[test] + fn steps_works() { + type M = (MockMigration<2, Test>, MockMigration<3, Test>); + let version = StorageVersion::new(2); + let cursor = M::new(version); + + let mut weight = Weight::from_all(2); + let cursor = M::steps(version, &cursor, &mut weight).unwrap(); + assert_eq!(cursor.to_vec(), vec![1u8, 0]); + assert_eq!(weight, Weight::from_all(1)); + + let mut weight = Weight::from_all(2); + assert!(M::steps(version, &cursor, &mut weight).is_none()); + } + + #[test] + fn no_migration_performed_works() { + type M = (MockMigration<2, Test>, MockMigration<3, Test>); + type TestMigration = Migration; + + ExtBuilder::default().build().execute_with(|| { + assert_err!( + TestMigration::migrate(Weight::MAX), + (Weight::zero(), Error::::NoMigrationPerformed.into()) + ) + }); + } + + #[test] + fn migration_works() { + type M = (MockMigration<1, Test>, MockMigration<2, Test>); + type TestMigration = Migration; + + ExtBuilder::default().build().execute_with(|| { + TestMigration::on_runtime_upgrade(); + for i in 1..=2 { + TestMigration::migrate(Weight::MAX).unwrap(); + assert_eq!(>::on_chain_storage_version(), StorageVersion::new(i)); + } + + assert_err!( + TestMigration::migrate(Weight::MAX), + (Weight::zero(), Error::::NoMigrationPerformed.into()) + ) + }); + } } diff --git a/frame/contracts/src/migration/v9.rs b/frame/contracts/src/migration/v9.rs index 933d68ea375a3..0f431a94b7354 100644 --- a/frame/contracts/src/migration/v9.rs +++ b/frame/contracts/src/migration/v9.rs @@ -18,25 +18,46 @@ //! Update `CodeStorage` with the new `determinism` field. use crate::{ - migration::{IsFinished, Migrate, PROOF_DECODE}, - CodeHash, Config, Determinism, Pallet, Weight, + migration::{IsFinished, Migrate}, + weights::WeightInfo, + CodeHash, Config, Determinism, Pallet, Weight, LOG_TARGET, }; use codec::{Decode, Encode}; use frame_support::{ - codec, pallet_prelude::*, storage, storage_alias, traits::Get, BoundedVec, DefaultNoBound, - Identity, StoragePrefixedMap, + codec, pallet_prelude::*, storage_alias, BoundedVec, DefaultNoBound, Identity, }; use sp_std::{marker::PhantomData, prelude::*}; -#[derive(Encode, Decode)] -struct OldPrefabWasmModule { - #[codec(compact)] - pub instruction_weights_version: u32, - #[codec(compact)] - pub initial: u32, - #[codec(compact)] - pub maximum: u32, - pub code: Vec, +mod old { + use super::*; + + #[derive(Encode, Decode)] + pub struct PrefabWasmModule { + #[codec(compact)] + pub instruction_weights_version: u32, + #[codec(compact)] + pub initial: u32, + #[codec(compact)] + pub maximum: u32, + pub code: Vec, + } + + #[storage_alias] + pub type CodeStorage = + StorageMap, Identity, CodeHash, PrefabWasmModule>; +} + +#[cfg(feature = "runtime-benchmarks")] +pub fn store_old_dummy_code(len: usize) { + use sp_runtime::traits::Hash; + let module = old::PrefabWasmModule { + instruction_weights_version: 0, + initial: 0, + maximum: 0, + code: vec![42u8; len], + }; + let hash = T::Hashing::hash(&module.code); + old::CodeStorage::::insert(hash, module); } #[derive(Encode, Decode)] @@ -51,9 +72,6 @@ struct PrefabWasmModule { pub determinism: Determinism, } -#[storage_alias] -type OldCodeStorage = StorageMap, Identity, CodeHash, OldPrefabWasmModule>; - #[storage_alias] type CodeStorage = StorageMap, Identity, CodeHash, PrefabWasmModule>; @@ -63,22 +81,23 @@ pub struct Migration { _phantom: PhantomData, } -impl Migrate for Migration { +impl Migrate for Migration { const VERSION: u16 = 9; fn max_step_weight() -> Weight { - // TODO: benchmark step - Weight::from_parts(0, 0) + T::WeightInfo::v9_translate_wasm_module(T::MaxCodeLen::get()) } - fn step(&mut self) -> (IsFinished, Option) { + fn step(&mut self) -> (IsFinished, Weight) { let mut iter = if let Some(last_key) = self.last_key.take() { - OldCodeStorage::::iter_from(last_key.to_vec()) + old::CodeStorage::::iter_from(last_key.to_vec()) } else { - OldCodeStorage::::iter() + old::CodeStorage::::iter() }; if let Some((key, old)) = iter.next() { + log::debug!(target: LOG_TARGET, "Migrating contract code {:?}", key); + let len = old.code.len() as u32; let module = PrefabWasmModule { instruction_weights_version: old.instruction_weights_version, initial: old.initial, @@ -88,9 +107,10 @@ impl Migrate for Migration { }; CodeStorage::::insert(key, module); self.last_key = Some(iter.last_raw_key().to_vec().try_into().unwrap()); - (IsFinished::No, None) + (IsFinished::No, T::WeightInfo::v9_translate_wasm_module(len)) } else { - (IsFinished::Yes, None) + log::debug!(target: LOG_TARGET, "No more contracts code to migrate"); + (IsFinished::Yes, T::WeightInfo::v9_translate_wasm_module(0)) } } } diff --git a/frame/contracts/src/weights.rs b/frame/contracts/src/weights.rs index cdaba7f59b72f..5cecee9d39a0f 100644 --- a/frame/contracts/src/weights.rs +++ b/frame/contracts/src/weights.rs @@ -166,6 +166,16 @@ pub trait WeightInfo { fn instr_i64shru(r: u32, ) -> Weight; fn instr_i64rotl(r: u32, ) -> Weight; fn instr_i64rotr(r: u32, ) -> Weight; + + // TODO run benchmark to generate + fn migrate() -> Weight { + Weight::from_parts(0, 0) + } + + // TODO run benchmark to generate + fn v9_translate_wasm_module(_r: u32, ) -> Weight { + Weight::from_parts(0, 0) + } } /// Weights for pallet_contracts using the Substrate node and recommended hardware. diff --git a/utils/frame/try-runtime/cli/src/lib.rs b/utils/frame/try-runtime/cli/src/lib.rs index 9268ef2edba8b..bac5027b1b5e6 100644 --- a/utils/frame/try-runtime/cli/src/lib.rs +++ b/utils/frame/try-runtime/cli/src/lib.rs @@ -660,9 +660,13 @@ impl State { // then, we prepare to replace the code based on what the CLI wishes. let maybe_code_to_overwrite = match shared.runtime { - Runtime::Path(ref path) => Some(std::fs::read(path).map_err(|e| { - format!("error while reading runtime file from {:?}: {:?}", path, e) - })?), + Runtime::Path(ref path) => { + log::debug!(target: LOG_TARGET, "Reading runtime from {:?}", path); + + Some(std::fs::read(path).map_err(|e| { + format!("error while reading runtime file from {:?}: {:?}", path, e) + })?) + }, Runtime::Existing => None, }; @@ -701,9 +705,15 @@ impl State { HexDisplay::from(BlakeTwo256::hash(&new_code).as_fixed_bytes()) ); - if new_version.spec_name != old_version.spec_name { - return Err("Spec names must match.".into()) - } + // if new_version.spec_name != old_version.spec_name { + // log::error!( + // target: LOG_TARGET, + // "old spec: {:?} != new spec name {:?}", + // old_version.spec_name, + // new_version.spec_name, + // ); + // return Err("Spec names must match.".into()) + // } } // whatever runtime we have in store now must have been compiled with try-runtime feature.