contracts: switch to wasmi gas metering (#14084)

* upgrade to wasmi 0.29

* prepare cleanup

* sync ref_time w engine from the stack frame

* proc_macro: sync gas in host funcs

save: compiles, only gas pushing left to macro

WIP proc macro

proc macro: done

* clean benchmarks & schedule: w_base = w_i64const

* scale gas values btw engine and gas meter

* (re)instrumentation & code_cache removed

* remove gas() host fn, continue clean-up

save

* address review comments

* move from CodeStorage&PrefabWasmModule to PristineCode&WasmBlob

* refactor: no reftime_limit&schedule passes, no CodeStorage

* bugs fixing

* fix tests: expected deposit amount

* fix prepare::tests

* update tests and fix bugs

tests::run_out_of_gas_engine, need 2 more

save: 2 bugs with gas syncs: 1 of 2 tests done

gas_syncs_no_overcharge bug fixed, test passes!

cleaned out debug prints

second bug is not a bug

disabled_chain_extension test fix (err msg)

tests run_out_of_fuel_host, chain_extension pass

all tests pass

* update docs

* bump wasmi 0.30.0

* benchmarks updated, tests pass

* refactoring

* s/OwnerInfo/CodeInfo/g;

* migration: draft, compiles

* migration: draft, runs

* migration: draft, runs (fixing)

* deposits repaid non pro rata

* deposits repaid pro rata

* better try-runtime output

* even better try-runtime output

* benchmark migration

* fix merge leftover

* add forgotten fixtures, fix docs

* address review comments

* ci fixes

* cleanup

* benchmarks::prepare to return DispatchError

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

* store memory limits to CodeInfo

* ci: roll back weights

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

* drive-by: update Readme and pallet rustdoc

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

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

* use wasmi 0.29

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

* use wasmi 0.30 again

* query memory limits from wasmi

* better migration types

* ci: pull weights from master

* refactoring

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

* addressing review comments

* refactor

* address review comments

* optimize migration

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

* another review round comments addressed

* ci fix one

* clippy fix

* ci fix two

---------

Co-authored-by: command-bot <>
This commit is contained in:
Sasha Gryaznov
2023-07-03 14:04:10 +03:00
committed by GitHub
parent e42768ea34
commit fda86dd501
23 changed files with 2786 additions and 4588 deletions
+51 -73
View File
@@ -30,7 +30,7 @@
//! These "smart-contract accounts" have the ability to instantiate smart-contracts and make calls
//! to other contract and non-contract accounts.
//!
//! The smart-contract code is stored once in a code cache, and later retrievable via its hash.
//! The smart-contract code is stored once, and later retrievable via its hash.
//! This means that multiple smart-contracts can be instantiated from the same hash, without
//! replicating the code each time.
//!
@@ -41,14 +41,14 @@
//! Finally, when an account is reaped, its associated code and storage of the smart-contract
//! account will also be deleted.
//!
//! ### Gas
//! ### Weight
//!
//! Senders must specify a gas limit with every call, as all instructions invoked by the
//! smart-contract require gas. Unused gas is refunded after the call, regardless of the execution
//! outcome.
//! Senders must specify a [`Weight`] limit with every call, as all instructions invoked by the
//! smart-contract require weight. Unused weight is refunded after the call, regardless of the
//! execution outcome.
//!
//! If the gas limit is reached, then all calls and state changes (including balance transfers) are
//! only reverted at the current call's contract level. For example, if contract A calls B and B
//! If the weight limit is reached, then all calls and state changes (including balance transfers)
//! are only reverted at the current call's contract level. For example, if contract A calls B and B
//! runs out of gas mid-call, then all of B's calls are reverted. Assuming correct error handling by
//! contract A, A's other calls and state changes still persist.
//!
@@ -63,22 +63,25 @@
//!
//! ### Dispatchable functions
//!
//! * [`Pallet::instantiate_with_code`] - Deploys a new contract from the supplied wasm binary,
//! * [`Pallet::instantiate_with_code`] - Deploys a new contract from the supplied Wasm binary,
//! optionally transferring
//! some balance. This instantiates a new smart contract account with the supplied code and
//! calls its constructor to initialize the contract.
//! * [`Pallet::instantiate`] - The same as `instantiate_with_code` but instead of uploading new
//! code an existing `code_hash` is supplied.
//! * [`Pallet::call`] - Makes a call to an account, optionally transferring some balance.
//! * [`Pallet::upload_code`] - Uploads new code without instantiating a contract from it.
//! * [`Pallet::remove_code`] - Removes the stored code and refunds the deposit to its owner. Only
//! allowed to code owner.
//! * [`Pallet::set_code`] - Changes the code of an existing contract. Only allowed to `Root`
//! origin.
//! * [`Pallet::migrate`] - Runs migration steps of curent multi-block migration in priority, before
//! [`Hooks::on_idle`][frame_support::traits::Hooks::on_idle] activates.
//!
//! ## Usage
//!
//! The Contracts module is a work in progress. The following examples show how this module
//! can be used to instantiate and call contracts.
//!
//! * [`ink!`](https://use.ink) is
//! an [`eDSL`](https://wiki.haskell.org/Embedded_domain_specific_language) that enables writing
//! WebAssembly based smart contracts in the Rust programming language.
//! * [`ink!`](https://use.ink) is language that enables writing Wasm-based smart contracts in plain
//! Rust.
#![allow(rustdoc::private_intra_doc_links)]
#![cfg_attr(not(feature = "std"), no_std)]
@@ -102,7 +105,7 @@ use crate::{
exec::{AccountIdOf, ErrorOrigin, ExecError, Executable, Key, Stack as ExecStack},
gas::GasMeter,
storage::{meter::Meter as StorageMeter, ContractInfo, DeletionQueueManager},
wasm::{OwnerInfo, PrefabWasmModule, TryInstantiate},
wasm::{CodeInfo, TryInstantiate, WasmBlob},
};
use codec::{Codec, Decode, Encode, HasCompact};
use environmental::*;
@@ -118,7 +121,7 @@ use frame_support::{
ReservableCurrency, Time,
},
weights::Weight,
BoundedVec, RuntimeDebugNoBound, WeakBoundedVec,
BoundedVec, RuntimeDebugNoBound,
};
use frame_system::{ensure_signed, pallet_prelude::OriginFor, EventRecord, Pallet as System};
use pallet_contracts_primitives::{
@@ -149,7 +152,6 @@ 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> = WeakBoundedVec<u8, <T as Config>::MaxCodeLen>;
type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
type DebugBufferVec<T> = BoundedVec<u8, <T as Config>::MaxDebugBufferLen>;
type EventRecordOf<T> =
@@ -184,7 +186,7 @@ pub mod pallet {
/// The current storage version.
#[cfg(not(any(test, feature = "runtime-benchmarks")))]
const STORAGE_VERSION: StorageVersion = StorageVersion::new(11);
const STORAGE_VERSION: StorageVersion = StorageVersion::new(12);
/// Hard coded storage version for running tests that depend on the current storage version.
#[cfg(any(test, feature = "runtime-benchmarks"))]
@@ -291,9 +293,7 @@ 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.
/// The maximum length of a contract code in bytes.
///
/// The value should be chosen carefully taking into the account the overall memory limit
/// your runtime has, as well as the [maximum allowed callstack
@@ -380,19 +380,19 @@ pub mod pallet {
// Check that given configured `MaxCodeLen`, runtime heap memory limit can't be broken.
//
// In worst case, the decoded wasm contract code would be `x16` times larger than the
// In worst case, the decoded Wasm contract code would be `x16` times larger than the
// encoded one. This is because even a single-byte wasm instruction has 16-byte size in
// wasmi. This gives us `MaxCodeLen*16` safety margin.
//
// Next, the pallet keeps both the original and instrumented wasm blobs for each
// contract, hence we add up `MaxCodeLen*2` more to the safety margin.
// Next, the pallet keeps the Wasm blob for each
// contract, hence we add up `MaxCodeLen` to the safety margin.
//
// Finally, the inefficiencies of the freeing-bump allocator
// being used in the client for the runtime memory allocations, could lead to possible
// memory allocations for contract code grow up to `x4` times in some extreme cases,
// which gives us total multiplier of `18*4` for `MaxCodeLen`.
// which gives us total multiplier of `17*4` for `MaxCodeLen`.
//
// That being said, for every contract executed in runtime, at least `MaxCodeLen*18*4`
// That being said, for every contract executed in runtime, at least `MaxCodeLen*17*4`
// memory should be available. Note that maximum allowed heap memory and stack size per
// each contract (stack frame) should also be counted.
//
@@ -401,7 +401,7 @@ pub mod pallet {
//
// This gives us the following formula:
//
// `(MaxCodeLen * 18 * 4 + MAX_STACK_SIZE + max_heap_size) * max_call_depth <
// `(MaxCodeLen * 17 * 4 + MAX_STACK_SIZE + max_heap_size) * max_call_depth <
// max_runtime_mem/2`
//
// Hence the upper limit for the `MaxCodeLen` can be defined as follows:
@@ -410,7 +410,7 @@ pub mod pallet {
.saturating_div(max_call_depth)
.saturating_sub(max_heap_size)
.saturating_sub(MAX_STACK_SIZE)
.saturating_div(18 * 4);
.saturating_div(17 * 4);
assert!(
T::MaxCodeLen::get() < code_len_limit,
@@ -521,7 +521,7 @@ pub mod pallet {
///
/// If the code does not already exist a deposit is reserved from the caller
/// and unreserved only when [`Self::remove_code`] is called. The size of the reserve
/// depends on the instrumented size of the the supplied `code`.
/// depends on the size of the supplied `code`.
///
/// If the code already exists in storage it will still return `Ok` and upgrades
/// the in storage version to the current
@@ -563,7 +563,7 @@ pub mod pallet {
) -> DispatchResultWithPostInfo {
Migration::<T>::ensure_migrated()?;
let origin = ensure_signed(origin)?;
<PrefabWasmModule<T>>::remove(&origin, code_hash)?;
<WasmBlob<T>>::remove(&origin, code_hash)?;
// we waive the fee because removing unused code is beneficial
Ok(Pays::No.into())
}
@@ -594,8 +594,8 @@ pub mod pallet {
} else {
return Err(<Error<T>>::ContractNotFound.into())
};
<PrefabWasmModule<T>>::add_user(code_hash)?;
<PrefabWasmModule<T>>::remove_user(contract.code_hash);
<WasmBlob<T>>::increment_refcount(code_hash)?;
<WasmBlob<T>>::decrement_refcount(contract.code_hash);
Self::deposit_event(
vec![T::Hashing::hash_of(&dest), code_hash, contract.code_hash],
Event::ContractCodeUpdated {
@@ -674,8 +674,7 @@ pub mod pallet {
///
/// Instantiation is executed as follows:
///
/// - The supplied `code` is instrumented, deployed, and a `code_hash` is created for that
/// code.
/// - The supplied `code` is deployed, and a `code_hash` is created for that code.
/// - If the `code_hash` already exists on the chain the underlying `code` will be shared.
/// - The destination address is computed based on the sender, code_hash and the salt.
/// - The smart-contract account is created at the computed address.
@@ -865,8 +864,8 @@ pub mod pallet {
#[pallet::error]
pub enum Error<T> {
/// A new schedule must have a greater version than the current one.
InvalidScheduleVersion,
/// Invalid schedule supplied, e.g. with zero weight of a basic operation.
InvalidSchedule,
/// Invalid combination of flags supplied to `seal_call` or `seal_delegate_call`.
InvalidCallFlags,
/// The executed contract exhausted its gas limit.
@@ -929,7 +928,7 @@ pub mod pallet {
/// or via RPC an `Ok` will be returned. In this case the caller needs to inspect the flags
/// to determine whether a reversion has taken place.
ContractReverted,
/// The contract's code was found to be invalid during validation or instrumentation.
/// The contract's code was found to be invalid during validation.
///
/// The most likely cause of this is that an API was used which is not supported by the
/// node. This happens if an older node is used with a new version of ink!. Try updating
@@ -946,18 +945,13 @@ pub mod pallet {
NoMigrationPerformed,
}
/// A mapping from an original code hash to the original code, untouched by instrumentation.
/// A mapping from a contract's code hash to its code.
#[pallet::storage]
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.
/// A mapping from a contract's code hash to its code info.
#[pallet::storage]
pub(crate) type CodeStorage<T: Config> =
StorageMap<_, Identity, CodeHash<T>, PrefabWasmModule<T>>;
/// A mapping between an original code hash and its owner information.
#[pallet::storage]
pub(crate) type OwnerInfoOf<T: Config> = StorageMap<_, Identity, CodeHash<T>, OwnerInfo<T>>;
pub(crate) type CodeInfoOf<T: Config> = StorageMap<_, Identity, CodeHash<T>, CodeInfo<T>>;
/// This is a **monotonic** counter incremented on contract instantiation.
///
@@ -1196,7 +1190,7 @@ impl<T: Config> Invokable<T> for CallInput<T> {
},
};
let schedule = T::Schedule::get();
let result = ExecStack::<T, PrefabWasmModule<T>>::run_call(
let result = ExecStack::<T, WasmBlob<T>>::run_call(
origin.clone(),
dest.clone(),
&mut gas_meter,
@@ -1239,7 +1233,7 @@ impl<T: Config> Invokable<T> for InstantiateInput<T> {
let origin = contract_origin.account_id()?;
let (extra_deposit, executable) = match &self.code {
Code::Upload(binary) => {
let executable = PrefabWasmModule::from_code(
let executable = WasmBlob::from_code(
binary.clone(),
&schedule,
origin.clone(),
@@ -1257,12 +1251,10 @@ impl<T: Config> Invokable<T> for InstantiateInput<T> {
// uploaded module does not already exist. This deposit is not part of the
// storage meter because it is not transferred to the contract but
// reserved on the uploading account.
(executable.open_deposit(), executable)
(executable.open_deposit(&executable.code_info()), executable)
},
Code::Existing(hash) => (
Default::default(),
PrefabWasmModule::from_storage(*hash, &schedule, &mut gas_meter)?,
),
Code::Existing(hash) =>
(Default::default(), WasmBlob::from_storage(*hash, &mut gas_meter)?),
};
let contract_origin = Origin::from_account_id(origin.clone());
let mut storage_meter = StorageMeter::new(
@@ -1271,7 +1263,7 @@ impl<T: Config> Invokable<T> for InstantiateInput<T> {
common.value.saturating_add(extra_deposit),
)?;
let CommonInput { value, data, debug_message, .. } = common;
let result = ExecStack::<T, PrefabWasmModule<T>>::run_instantiate(
let result = ExecStack::<T, WasmBlob<T>>::run_instantiate(
origin.clone(),
executable,
&mut gas_meter,
@@ -1443,15 +1435,10 @@ impl<T: Config> Pallet<T> {
) -> CodeUploadResult<CodeHash<T>, BalanceOf<T>> {
Migration::<T>::ensure_migrated()?;
let schedule = T::Schedule::get();
let module = PrefabWasmModule::from_code(
code,
&schedule,
origin,
determinism,
TryInstantiate::Instantiate,
)
.map_err(|(err, _)| err)?;
let deposit = module.open_deposit();
let module =
WasmBlob::from_code(code, &schedule, origin, determinism, TryInstantiate::Instantiate)
.map_err(|(err, _)| err)?;
let deposit = module.open_deposit(&module.code_info());
if let Some(storage_deposit_limit) = storage_deposit_limit {
ensure!(storage_deposit_limit >= deposit, <Error<T>>::StorageDepositLimitExhausted);
}
@@ -1494,26 +1481,17 @@ impl<T: Config> Pallet<T> {
ContractInfo::<T>::load_code_hash(account)
}
/// Store code for benchmarks which does not check nor instrument the code.
/// Store code for benchmarks which does not validate the code.
#[cfg(feature = "runtime-benchmarks")]
fn store_code_raw(
code: Vec<u8>,
owner: T::AccountId,
) -> frame_support::dispatch::DispatchResult {
let schedule = T::Schedule::get();
PrefabWasmModule::store_code_unchecked(code, &schedule, owner)?;
WasmBlob::store_code_unchecked(code, &schedule, owner)?;
Ok(())
}
/// This exists so that benchmarks can determine the weight of running an instrumentation.
#[cfg(feature = "runtime-benchmarks")]
fn reinstrument_module(
module: &mut PrefabWasmModule<T>,
schedule: &Schedule<T>,
) -> frame_support::dispatch::DispatchResult {
self::wasm::reinstrument(module, schedule).map(|_| ())
}
/// Deposit a pallet contracts event. Handles the conversion to the overarching event type.
fn deposit_event(topics: Vec<T::Hash>, event: Event<T>) {
<frame_system::Pallet<T>>::deposit_event_indexed(