From ef46d84aedb0b5c95e121bb7ec2796b0fdf1808b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Thei=C3=9Fen?= <alex.theissen@me.com> Date: Wed, 18 May 2022 09:40:53 +0200 Subject: [PATCH] contracts: Get rid of `#[pallet::without_storage_info]` (#11414) * Implement `MaxEncodeLen` for pallet-contracts storage * Remove redundant debug println * Move code len check to PrefabWasmModule::from_code --- substrate/bin/node/runtime/src/lib.rs | 2 + .../frame/contracts/src/benchmarking/mod.rs | 8 ++-- substrate/frame/contracts/src/lib.rs | 43 +++++++++++-------- substrate/frame/contracts/src/schedule.rs | 8 +--- substrate/frame/contracts/src/storage.rs | 29 +++++++------ substrate/frame/contracts/src/tests.rs | 2 + .../frame/contracts/src/wasm/code_cache.rs | 10 +++-- substrate/frame/contracts/src/wasm/mod.rs | 31 ++++++++++--- substrate/frame/contracts/src/wasm/prepare.rs | 33 +++++++------- 9 files changed, 99 insertions(+), 67 deletions(-) diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 40316cee9b2..de6f8eeb9f1 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -1118,6 +1118,8 @@ impl pallet_contracts::Config for Runtime { type Schedule = Schedule; type AddressGenerator = pallet_contracts::DefaultAddressGenerator; type ContractAccessWeight = pallet_contracts::DefaultContractAccessWeight<RuntimeBlockWeights>; + type MaxCodeLen = ConstU32<{ 128 * 1024 }>; + type RelaxedMaxCodeLen = ConstU32<{ 256 * 1024 }>; } impl pallet_sudo::Config for Runtime { diff --git a/substrate/frame/contracts/src/benchmarking/mod.rs b/substrate/frame/contracts/src/benchmarking/mod.rs index 194f97f8c87..01e62e07e1c 100644 --- a/substrate/frame/contracts/src/benchmarking/mod.rs +++ b/substrate/frame/contracts/src/benchmarking/mod.rs @@ -231,7 +231,7 @@ benchmarks! { // first time after a new schedule was deployed: For every new schedule a contract needs // to re-run the instrumentation once. reinstrument { - let c in 0 .. T::Schedule::get().limits.code_len; + let c in 0 .. Perbill::from_percent(49).mul_ceil(T::MaxCodeLen::get()); let WasmModule { code, hash, .. } = WasmModule::<T>::sized(c, Location::Call); Contracts::<T>::store_code_raw(code, whitelisted_caller())?; let schedule = T::Schedule::get(); @@ -247,7 +247,7 @@ benchmarks! { // which is in the wasm module but not executed on `call`. // The results are supposed to be used as `call_with_code_kb(c) - call_with_code_kb(0)`. call_with_code_per_byte { - let c in 0 .. T::Schedule::get().limits.code_len; + let c in 0 .. T::MaxCodeLen::get(); let instance = Contract::<T>::with_caller( whitelisted_caller(), WasmModule::sized(c, Location::Deploy), vec![], )?; @@ -271,7 +271,7 @@ benchmarks! { // We cannot let `c` grow to the maximum code size because the code is not allowed // to be larger than the maximum size **after instrumentation**. instantiate_with_code { - let c in 0 .. Perbill::from_percent(49).mul_ceil(T::Schedule::get().limits.code_len); + let c in 0 .. Perbill::from_percent(49).mul_ceil(T::MaxCodeLen::get()); let s in 0 .. code::max_pages::<T>() * 64 * 1024; let salt = vec![42u8; s as usize]; let value = T::Currency::minimum_balance(); @@ -360,7 +360,7 @@ benchmarks! { // We cannot let `c` grow to the maximum code size because the code is not allowed // to be larger than the maximum size **after instrumentation**. upload_code { - let c in 0 .. Perbill::from_percent(50).mul_ceil(T::Schedule::get().limits.code_len); + let c in 0 .. Perbill::from_percent(49).mul_ceil(T::MaxCodeLen::get()); let caller = whitelisted_caller(); T::Currency::make_free_balance_be(&caller, caller_funding::<T>()); let WasmModule { code, hash, .. } = WasmModule::<T>::sized(c, Location::Call); diff --git a/substrate/frame/contracts/src/lib.rs b/substrate/frame/contracts/src/lib.rs index ca639d7781a..ffac3e222bb 100644 --- a/substrate/frame/contracts/src/lib.rs +++ b/substrate/frame/contracts/src/lib.rs @@ -114,8 +114,9 @@ use codec::{Encode, HasCompact}; use frame_support::{ dispatch::Dispatchable, ensure, - traits::{Contains, Currency, Get, Randomness, ReservableCurrency, Time}, + traits::{ConstU32, Contains, Currency, Get, Randomness, ReservableCurrency, Time}, weights::{DispatchClass, GetDispatchInfo, Pays, PostDispatchInfo, Weight}, + BoundedVec, }; use frame_system::{limits::BlockWeights, Pallet as System}; use pallet_contracts_primitives::{ @@ -129,9 +130,11 @@ use sp_runtime::traits::{Convert, Hash, Saturating, StaticLookup}; use sp_std::{fmt::Debug, marker::PhantomData, prelude::*}; type CodeHash<T> = <T as frame_system::Config>::Hash; -type TrieId = Vec<u8>; +type TrieId = BoundedVec<u8, ConstU32<128>>; type BalanceOf<T> = <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance; +type CodeVec<T> = BoundedVec<u8, <T as Config>::MaxCodeLen>; +type RelaxedCodeVec<T> = BoundedVec<u8, <T as Config>::RelaxedMaxCodeLen>; /// Used as a sentinel value when reading and writing contract memory. /// @@ -224,7 +227,6 @@ pub mod pallet { #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] - #[pallet::without_storage_info] pub struct Pallet<T>(PhantomData<T>); #[pallet::config] @@ -356,6 +358,20 @@ pub mod pallet { /// The address generator used to generate the addresses of contracts. type AddressGenerator: AddressGenerator<Self>; + + /// The maximum length of a contract code in bytes. This limit applies to the instrumented + /// version of the code. Therefore `instantiate_with_code` can fail even when supplying + /// a wasm binary below this maximum size. + type MaxCodeLen: Get<u32>; + + /// The maximum length of a contract code after reinstrumentation. + /// + /// When uploading a new contract the size defined by [`Self::MaxCodeLen`] is used for both + /// the pristine **and** the instrumented version. When a existing contract needs to be + /// reinstrumented after a runtime upgrade we apply this bound. The reason is that if the + /// new instrumentation increases the size beyond the limit it would make that contract + /// inaccessible until rectified by another runtime upgrade. + type RelaxedMaxCodeLen: Get<u32>; } #[pallet::hooks] @@ -698,7 +714,7 @@ pub mod pallet { /// A mapping from an original code hash to the original code, untouched by instrumentation. #[pallet::storage] - pub(crate) type PristineCode<T: Config> = StorageMap<_, Identity, CodeHash<T>, Vec<u8>>; + pub(crate) type PristineCode<T: Config> = StorageMap<_, Identity, CodeHash<T>, CodeVec<T>>; /// A mapping between an original code hash and instrumented wasm code, ready for execution. #[pallet::storage] @@ -746,7 +762,8 @@ pub mod pallet { /// Child trie deletion is a heavy operation depending on the amount of storage items /// stored in said trie. Therefore this operation is performed lazily in `on_initialize`. #[pallet::storage] - pub(crate) type DeletionQueue<T: Config> = StorageValue<_, Vec<DeletedContract>, ValueQuery>; + pub(crate) type DeletionQueue<T: Config> = + StorageValue<_, BoundedVec<DeletedContract, T::DeletionQueueDepth>, ValueQuery>; } /// Return type of the private [`Pallet::internal_call`] function. @@ -864,8 +881,8 @@ where storage_deposit_limit: Option<BalanceOf<T>>, ) -> CodeUploadResult<CodeHash<T>, BalanceOf<T>> { let schedule = T::Schedule::get(); - let module = PrefabWasmModule::from_code(code, &schedule, origin) - .map_err(|_| <Error<T>>::CodeRejected)?; + let module = + PrefabWasmModule::from_code(code, &schedule, origin).map_err(|(err, _)| err)?; let deposit = module.open_deposit(); if let Some(storage_deposit_limit) = storage_deposit_limit { ensure!(storage_deposit_limit >= deposit, <Error<T>>::StorageDepositLimitExhausted); @@ -971,19 +988,11 @@ where let schedule = T::Schedule::get(); let (extra_deposit, executable) = match code { Code::Upload(Bytes(binary)) => { - ensure!( - binary.len() as u32 <= schedule.limits.code_len, - <Error<T>>::CodeTooLarge - ); let executable = PrefabWasmModule::from_code(binary, &schedule, origin.clone()) - .map_err(|msg| { + .map_err(|(err, msg)| { debug_message.as_mut().map(|buffer| buffer.extend(msg.as_bytes())); - <Error<T>>::CodeRejected + err })?; - ensure!( - executable.code_len() <= schedule.limits.code_len, - <Error<T>>::CodeTooLarge - ); // The open deposit will be charged during execution when the // uploaded module does not already exist. This deposit is not part of the // storage meter because it is not transfered to the contract but diff --git a/substrate/frame/contracts/src/schedule.rs b/substrate/frame/contracts/src/schedule.rs index 9e9f213fabb..907ce9e0886 100644 --- a/substrate/frame/contracts/src/schedule.rs +++ b/substrate/frame/contracts/src/schedule.rs @@ -33,7 +33,7 @@ use wasm_instrument::{gas_metering, parity_wasm::elements}; /// How many API calls are executed in a single batch. The reason for increasing the amount /// of API calls in batches (per benchmark component increase) is so that the linear regression /// has an easier time determining the contribution of that component. -pub const API_BENCHMARK_BATCH_SIZE: u32 = 100; +pub const API_BENCHMARK_BATCH_SIZE: u32 = 80; /// How many instructions are executed in a single batch. The reasoning is the same /// as for `API_BENCHMARK_BATCH_SIZE`. @@ -147,11 +147,6 @@ pub struct Limits { /// The maximum size of a storage value and event payload in bytes. pub payload_len: u32, - - /// The maximum length of a contract code in bytes. This limit applies to the instrumented - /// version of the code. Therefore `instantiate_with_code` can fail even when supplying - /// a wasm binary below this maximum size. - pub code_len: u32, } impl Limits { @@ -522,7 +517,6 @@ impl Default for Limits { subject_len: 32, call_depth: 32, payload_len: 16 * 1024, - code_len: 128 * 1024, } } } diff --git a/substrate/frame/contracts/src/storage.rs b/substrate/frame/contracts/src/storage.rs index 2bdacc15cb1..a9a78f12581 100644 --- a/substrate/frame/contracts/src/storage.rs +++ b/substrate/frame/contracts/src/storage.rs @@ -24,11 +24,10 @@ use crate::{ weights::WeightInfo, BalanceOf, CodeHash, Config, ContractInfoOf, DeletionQueue, Error, TrieId, SENTINEL, }; -use codec::{Decode, Encode}; +use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ dispatch::{DispatchError, DispatchResult}, storage::child::{self, ChildInfo, KillStorageResult}, - traits::Get, weights::Weight, }; use scale_info::TypeInfo; @@ -44,7 +43,7 @@ pub type ContractInfo<T> = RawContractInfo<CodeHash<T>, BalanceOf<T>>; /// Information for managing an account and its sub trie abstraction. /// This is the required info to cache for an account. -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct RawContractInfo<CodeHash, Balance> { /// Unique ID for the subtree encoded as a bytes vector. pub trie_id: TrieId, @@ -67,7 +66,7 @@ fn child_trie_info(trie_id: &[u8]) -> ChildInfo { ChildInfo::new_default(trie_id) } -#[derive(Encode, Decode, TypeInfo)] +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] pub struct DeletedContract { pub(crate) trie_id: TrieId, } @@ -217,12 +216,8 @@ where /// /// You must make sure that the contract is also removed when queuing the trie for deletion. pub fn queue_trie_for_deletion(contract: &ContractInfo<T>) -> DispatchResult { - if <DeletionQueue<T>>::decode_len().unwrap_or(0) >= T::DeletionQueueDepth::get() as usize { - Err(Error::<T>::DeletionQueueFull.into()) - } else { - <DeletionQueue<T>>::append(DeletedContract { trie_id: contract.trie_id.clone() }); - Ok(()) - } + <DeletionQueue<T>>::try_append(DeletedContract { trie_id: contract.trie_id.clone() }) + .map_err(|_| <Error<T>>::DeletionQueueFull.into()) } /// Calculates the weight that is necessary to remove one key from the trie and how many @@ -293,7 +288,11 @@ where /// Generates a unique trie id by returning `hash(account_id ++ nonce)`. pub fn generate_trie_id(account_id: &AccountIdOf<T>, nonce: u64) -> TrieId { let buf: Vec<_> = account_id.as_ref().iter().chain(&nonce.to_le_bytes()).cloned().collect(); - T::Hashing::hash(&buf).as_ref().into() + T::Hashing::hash(&buf) + .as_ref() + .to_vec() + .try_into() + .expect("Runtime uses a reasonable hash size. Hence sizeof(T::Hash) <= 128; qed") } /// Returns the code hash of the contract specified by `account` ID. @@ -305,9 +304,11 @@ where /// Fill up the queue in order to exercise the limits during testing. #[cfg(test)] pub fn fill_queue_with_dummies() { - let queue: Vec<_> = (0..T::DeletionQueueDepth::get()) - .map(|_| DeletedContract { trie_id: vec![] }) + use frame_support::{traits::Get, BoundedVec}; + let queue: Vec<DeletedContract> = (0..T::DeletionQueueDepth::get()) + .map(|_| DeletedContract { trie_id: TrieId::default() }) .collect(); - <DeletionQueue<T>>::put(queue); + let bounded: BoundedVec<_, _> = queue.try_into().unwrap(); + <DeletionQueue<T>>::put(bounded); } } diff --git a/substrate/frame/contracts/src/tests.rs b/substrate/frame/contracts/src/tests.rs index 407e1e999dd..e52ce5ca0e1 100644 --- a/substrate/frame/contracts/src/tests.rs +++ b/substrate/frame/contracts/src/tests.rs @@ -288,6 +288,8 @@ impl Config for Test { type DepositPerItem = DepositPerItem; type AddressGenerator = DefaultAddressGenerator; type ContractAccessWeight = DefaultContractAccessWeight<BlockWeights>; + type MaxCodeLen = ConstU32<{ 128 * 1024 }>; + type RelaxedMaxCodeLen = ConstU32<{ 256 * 1024 }>; } pub const ALICE: AccountId32 = AccountId32::new([1u8; 32]); diff --git a/substrate/frame/contracts/src/wasm/code_cache.rs b/substrate/frame/contracts/src/wasm/code_cache.rs index b194c283e90..10de436bfb1 100644 --- a/substrate/frame/contracts/src/wasm/code_cache.rs +++ b/substrate/frame/contracts/src/wasm/code_cache.rs @@ -163,7 +163,8 @@ pub fn load<T: Config>( where T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>, { - let charged = gas_meter.charge(CodeToken::Load(schedule.limits.code_len))?; + let max_code_len = T::MaxCodeLen::get(); + let charged = gas_meter.charge(CodeToken::Load(max_code_len))?; let mut prefab_module = <CodeStorage<T>>::get(code_hash).ok_or(Error::<T>::CodeNotFound)?; gas_meter.adjust_gas(charged, CodeToken::Load(prefab_module.code.len() as u32)); @@ -172,7 +173,7 @@ where if prefab_module.instruction_weights_version < schedule.instruction_weights.version { // The instruction weights have changed. // We need to re-instrument the code with the new instruction weights. - let charged = gas_meter.charge(CodeToken::Reinstrument(schedule.limits.code_len))?; + let charged = gas_meter.charge(CodeToken::Reinstrument(max_code_len))?; let code_size = reinstrument(&mut prefab_module, schedule)?; gas_meter.adjust_gas(charged, CodeToken::Reinstrument(code_size)); } @@ -190,7 +191,10 @@ pub fn reinstrument<T: Config>( let original_code = <PristineCode<T>>::get(&prefab_module.code_hash).ok_or(Error::<T>::CodeNotFound)?; let original_code_len = original_code.len(); - prefab_module.code = prepare::reinstrument_contract::<T>(original_code, schedule)?; + prefab_module.code = prepare::reinstrument_contract::<T>(&original_code, schedule) + .map_err(|_| <Error<T>>::CodeRejected)? + .try_into() + .map_err(|_| <Error<T>>::CodeTooLarge)?; prefab_module.instruction_weights_version = schedule.instruction_weights.version; <CodeStorage<T>>::insert(&prefab_module.code_hash, &*prefab_module); Ok(original_code_len as u32) diff --git a/substrate/frame/contracts/src/wasm/mod.rs b/substrate/frame/contracts/src/wasm/mod.rs index d710cad199f..8764bf3690b 100644 --- a/substrate/frame/contracts/src/wasm/mod.rs +++ b/substrate/frame/contracts/src/wasm/mod.rs @@ -31,10 +31,15 @@ use crate::{ exec::{ExecResult, Executable, ExportedFunction, Ext}, gas::GasMeter, wasm::env_def::FunctionImplProvider, - AccountIdOf, BalanceOf, CodeHash, CodeStorage, Config, Schedule, + AccountIdOf, BalanceOf, CodeHash, CodeStorage, CodeVec, Config, Error, RelaxedCodeVec, + Schedule, }; use codec::{Decode, Encode, MaxEncodedLen}; -use frame_support::dispatch::{DispatchError, DispatchResult}; +use frame_support::{ + dispatch::{DispatchError, DispatchResult}, + ensure, + traits::Get, +}; use sp_core::crypto::UncheckedFrom; use sp_sandbox::{SandboxEnvironmentBuilder, SandboxInstance, SandboxMemory}; use sp_std::prelude::*; @@ -50,7 +55,8 @@ pub use tests::MockExt; /// `instruction_weights_version` and `code` change when a contract with an outdated instrumentation /// is called. Therefore one must be careful when holding any in-memory representation of this /// type while calling into a contract as those fields can get out of date. -#[derive(Clone, Encode, Decode, scale_info::TypeInfo)] +#[derive(Clone, Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)] +#[codec(mel_bound())] #[scale_info(skip_type_params(T))] pub struct PrefabWasmModule<T: Config> { /// Version of the instruction weights with which the code was instrumented. @@ -63,14 +69,14 @@ pub struct PrefabWasmModule<T: Config> { #[codec(compact)] maximum: u32, /// Code instrumented with the latest schedule. - code: Vec<u8>, + code: RelaxedCodeVec<T>, /// The uninstrumented, pristine version of the code. /// /// It is not stored because the pristine code has its own storage item. The value /// is only `Some` when this module was created from an `original_code` and `None` if /// it was loaded from storage. #[codec(skip)] - original_code: Option<Vec<u8>>, + original_code: Option<CodeVec<T>>, /// The code hash of the stored code which is defined as the hash over the `original_code`. /// /// As the map key there is no need to store the hash in the value, too. It is set manually @@ -122,8 +128,19 @@ where original_code: Vec<u8>, schedule: &Schedule<T>, owner: AccountIdOf<T>, - ) -> Result<Self, &'static str> { - prepare::prepare_contract(original_code, schedule, owner) + ) -> Result<Self, (DispatchError, &'static str)> { + let module = prepare::prepare_contract( + original_code.try_into().map_err(|_| (<Error<T>>::CodeTooLarge.into(), ""))?, + schedule, + owner, + )?; + // When instrumenting a new code we apply a stricter limit than enforced by the + // `RelaxedCodeVec` in order to leave some headroom for reinstrumentation. + ensure!( + module.code.len() as u32 <= T::MaxCodeLen::get(), + (<Error<T>>::CodeTooLarge.into(), ""), + ); + Ok(module) } /// Store the code without instantiating it. diff --git a/substrate/frame/contracts/src/wasm/prepare.rs b/substrate/frame/contracts/src/wasm/prepare.rs index 6e9babe1264..f6fff20de6b 100644 --- a/substrate/frame/contracts/src/wasm/prepare.rs +++ b/substrate/frame/contracts/src/wasm/prepare.rs @@ -23,10 +23,10 @@ use crate::{ chain_extension::ChainExtension, storage::meter::Diff, wasm::{env_def::ImportSatisfyCheck, OwnerInfo, PrefabWasmModule}, - AccountIdOf, Config, Schedule, + AccountIdOf, CodeVec, Config, Error, Schedule, }; use codec::{Encode, MaxEncodedLen}; -use sp_runtime::traits::Hash; +use sp_runtime::{traits::Hash, DispatchError}; use sp_std::prelude::*; use wasm_instrument::parity_wasm::elements::{ self, External, Internal, MemoryType, Type, ValueType, @@ -401,19 +401,19 @@ fn check_and_instrument<C: ImportSatisfyCheck, T: Config>( } fn do_preparation<C: ImportSatisfyCheck, T: Config>( - original_code: Vec<u8>, + original_code: CodeVec<T>, schedule: &Schedule<T>, owner: AccountIdOf<T>, -) -> Result<PrefabWasmModule<T>, &'static str> { - let (code, (initial, maximum)) = - check_and_instrument::<C, T>(original_code.as_ref(), schedule)?; +) -> Result<PrefabWasmModule<T>, (DispatchError, &'static str)> { + let (code, (initial, maximum)) = check_and_instrument::<C, T>(original_code.as_ref(), schedule) + .map_err(|msg| (<Error<T>>::CodeRejected.into(), msg))?; let original_code_len = original_code.len(); let mut module = PrefabWasmModule { instruction_weights_version: schedule.instruction_weights.version, initial, maximum, - code, + code: code.try_into().map_err(|_| (<Error<T>>::CodeTooLarge.into(), ""))?, code_hash: T::Hashing::hash(&original_code), original_code: Some(original_code), owner_info: None, @@ -446,10 +446,10 @@ fn do_preparation<C: ImportSatisfyCheck, T: Config>( /// /// The preprocessing includes injecting code for gas metering and metering the height of stack. pub fn prepare_contract<T: Config>( - original_code: Vec<u8>, + original_code: CodeVec<T>, schedule: &Schedule<T>, owner: AccountIdOf<T>, -) -> Result<PrefabWasmModule<T>, &'static str> { +) -> Result<PrefabWasmModule<T>, (DispatchError, &'static str)> { do_preparation::<super::runtime::Env, T>(original_code, schedule, owner) } @@ -459,10 +459,10 @@ pub fn prepare_contract<T: Config>( /// /// Use this when an existing contract should be re-instrumented with a newer schedule version. pub fn reinstrument_contract<T: Config>( - original_code: Vec<u8>, + original_code: &[u8], schedule: &Schedule<T>, ) -> Result<Vec<u8>, &'static str> { - Ok(check_and_instrument::<super::runtime::Env, T>(&original_code, schedule)?.0) + Ok(check_and_instrument::<super::runtime::Env, T>(original_code, schedule)?.0) } /// Alternate (possibly unsafe) preparation functions used only for benchmarking. @@ -493,9 +493,12 @@ pub mod benchmarking { instruction_weights_version: schedule.instruction_weights.version, initial: memory_limits.0, maximum: memory_limits.1, - code: contract_module.into_wasm_code()?, code_hash: T::Hashing::hash(&original_code), - original_code: Some(original_code), + original_code: Some(original_code.try_into().map_err(|_| "Original code too large")?), + code: contract_module + .into_wasm_code()? + .try_into() + .map_err(|_| "Instrumented code too large")?, owner_info: Some(OwnerInfo { owner, // this is a helper function for benchmarking which skips deposit collection @@ -546,7 +549,7 @@ mod tests { ($name:ident, $wat:expr, $($expected:tt)*) => { #[test] fn $name() { - let wasm = wat::parse_str($wat).unwrap(); + let wasm = wat::parse_str($wat).unwrap().try_into().unwrap(); let schedule = Schedule { limits: Limits { globals: 3, @@ -559,7 +562,7 @@ mod tests { .. Default::default() }; let r = do_preparation::<env::Test, Test>(wasm, &schedule, ALICE); - assert_matches::assert_matches!(r, $($expected)*); + assert_matches::assert_matches!(r.map_err(|(_, msg)| msg), $($expected)*); } }; } -- GitLab