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

contracts: switch to wasmi gas metering #14084

Merged
merged 66 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
eef15f1
upgrade to wasmi 0.29
agryaznov May 3, 2023
dee28ff
prepare cleanup
agryaznov May 3, 2023
efeb0bd
sync ref_time w engine from the stack frame
agryaznov May 3, 2023
4fdd5c8
proc_macro: sync gas in host funcs
agryaznov May 4, 2023
cac61c6
clean benchmarks & schedule: w_base = w_i64const
agryaznov May 5, 2023
be0df3c
scale gas values btw engine and gas meter
agryaznov May 5, 2023
8a32757
(re)instrumentation & code_cache removed
agryaznov May 5, 2023
f8abec4
remove gas() host fn, continue clean-up
agryaznov May 8, 2023
22440ae
address review comments
agryaznov Jun 9, 2023
c94821d
Merge branch 'master' into ag-wasmeter
agryaznov Jun 9, 2023
2010ff7
move from CodeStorage&PrefabWasmModule to PristineCode&WasmBlob
agryaznov Jun 9, 2023
100cdcd
refactor: no reftime_limit&schedule passes, no CodeStorage
agryaznov Jun 9, 2023
85a0340
bugs fixing
agryaznov Jun 10, 2023
505f750
fix tests: expected deposit amount
agryaznov Jun 10, 2023
c25f634
fix prepare::tests
agryaznov Jun 10, 2023
7f46e7e
update tests and fix bugs
agryaznov Jun 10, 2023
7bf4fbe
update docs
agryaznov Jun 11, 2023
cc05e88
bump wasmi 0.30.0
agryaznov Jun 12, 2023
0e2b0ab
benchmarks updated, tests pass
agryaznov Jun 12, 2023
e6e3563
refactoring
agryaznov Jun 12, 2023
f4ffcf2
s/OwnerInfo/CodeInfo/g;
agryaznov Jun 12, 2023
a50b7cf
migration: draft, compiles
agryaznov Jun 12, 2023
c6b9f74
migration: draft, runs
agryaznov Jun 13, 2023
132e9ce
migration: draft, runs (fixing)
agryaznov Jun 13, 2023
9935e92
deposits repaid non pro rata
agryaznov Jun 13, 2023
0b9125a
deposits repaid pro rata
agryaznov Jun 13, 2023
08a15b7
better try-runtime output
agryaznov Jun 13, 2023
5f6c1ac
even better try-runtime output
agryaznov Jun 13, 2023
b1290f0
benchmark migration
agryaznov Jun 14, 2023
9ffca5d
Merge branch 'master' into ag-wasmeter
agryaznov Jun 14, 2023
94f8476
fix merge leftover
agryaznov Jun 14, 2023
ba67751
add forgotten fixtures, fix docs
agryaznov Jun 14, 2023
0ee3293
address review comments
agryaznov Jun 14, 2023
a45c538
ci fixes
agryaznov Jun 14, 2023
f24a267
cleanup
agryaznov Jun 15, 2023
e6c21f3
benchmarks::prepare to return DispatchError
agryaznov Jun 15, 2023
444a97b
Merge branch 'master' into ag-wasmeter
agryaznov Jun 15, 2023
d3f59b2
".git/.scripts/commands/bench/bench.sh" pallet dev pallet_contracts
Jun 15, 2023
f12927d
store memory limits to CodeInfo
agryaznov Jun 16, 2023
0a9b351
ci: roll back weights
agryaznov Jun 16, 2023
cff9da7
Merge branch 'master' of https://github.com/paritytech/substrate into…
Jun 16, 2023
f0e24c5
".git/.scripts/commands/bench-vm/bench-vm.sh" pallet dev pallet_contr…
Jun 16, 2023
2e07393
drive-by: update Readme and pallet rustdoc
agryaznov Jun 16, 2023
f465ecf
Merge branch 'master' into ag-wasmeter
agryaznov Jun 17, 2023
94be691
".git/.scripts/commands/bench/bench.sh" pallet dev pallet_contracts
Jun 17, 2023
721d4b6
".git/.scripts/commands/bench/bench.sh" pallet dev pallet_contracts
Jun 17, 2023
e226c0e
use wasmi 0.29
agryaznov Jun 18, 2023
e57596f
Merge branch 'master' of https://github.com/paritytech/substrate into…
Jun 18, 2023
d687f1c
".git/.scripts/commands/bench/bench.sh" pallet dev pallet_contracts
Jun 18, 2023
76a8c20
use wasmi 0.30 again
agryaznov Jun 20, 2023
0b4bb91
query memory limits from wasmi
agryaznov Jun 20, 2023
b15d5a9
better migration types
agryaznov Jun 20, 2023
8eb8710
ci: pull weights from master
agryaznov Jun 20, 2023
dae3f32
Merge branch 'master' into ag-wasmeter
agryaznov Jun 21, 2023
f42db5d
refactoring
agryaznov Jun 21, 2023
b789e6b
".git/.scripts/commands/bench-vm/bench-vm.sh" pallet dev pallet_contr…
Jun 21, 2023
2544972
addressing review comments
agryaznov Jun 27, 2023
387bec9
refactor
agryaznov Jun 27, 2023
8be0537
address review comments
agryaznov Jun 29, 2023
ffdb8d1
optimize migration
agryaznov Jun 30, 2023
eaf7a31
Merge branch 'master' into ag-wasmeter
agryaznov Jun 30, 2023
f4dad0f
".git/.scripts/commands/bench/bench.sh" pallet dev pallet_contracts
Jun 30, 2023
b5ad1de
another review round comments addressed
agryaznov Jun 30, 2023
0e0155f
ci fix one
agryaznov Jun 30, 2023
d139757
clippy fix
agryaznov Jun 30, 2023
670ff1a
ci fix two
agryaznov Jun 30, 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
12 changes: 10 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frame/contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ serde = { version = "1", optional = true, features = ["derive"] }
smallvec = { version = "1", default-features = false, features = [
"const_generics",
] }
wasmi = { version = "0.28", default-features = false }
wasmi = { version = "0.30", default-features = false }
wasmparser = { package = "wasmparser-nostd", version = "0.100", default-features = false }
impl-trait-for-tuples = "0.2"

Expand Down
52 changes: 23 additions & 29 deletions frame/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ This module extends accounts based on the `Currency` trait to have smart-contrac
be used with other modules that implement accounts based on `Currency`. 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 `code_hash`.
This means that multiple smart-contracts can be instantiated from the same `code_cache`, without replicating
The smart-contract code is stored once, and later retrievable via its `code_hash`.
This means that multiple smart-contracts can be instantiated from the same `code`, without replicating
the code each time.

When a smart-contract is called, its associated code is retrieved via the code hash and gets executed.
Expand All @@ -24,48 +24,44 @@ or call other smart-contracts.
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`](https://paritytech.github.io/substrate/master/sp_weights/struct.Weight.html) 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 runs out of gas mid-call,
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 weight 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.

One gas is equivalent to one [weight](https://docs.substrate.io/v3/runtime/weights-and-fees)
which is defined as one picosecond of execution time on the runtime's reference machine.
One `ref_time` `Weight` is defined as one picosecond of execution time on the runtime's reference machine.

### Revert Behaviour

Contract call failures are not cascading. When failures occur in a sub-call, they do not "bubble up",
and the call will only revert at the specific contract level. For example, if contract A calls contract B, and B
fails, A can decide how to handle that failure, either proceeding or reverting A's changes.

### Offchain Execution
### Off-chain Execution

In general, a contract execution needs to be deterministic so that all nodes come to the same
conclusion when executing it. To that end we disallow any instructions that could cause
indeterminism. Most notable are any floating point arithmetic. That said, sometimes contracts
are executed off-chain and hence are not subject to consensus. If code is only executed by a
single node and implicitly trusted by other actors is such a case. Trusted execution environments
come to mind. To that end we allow the execution of indeterminstic code for offchain usages
come to mind. To that end we allow the execution of indeterminstic code for off-chain usages
with the following constraints:

1. No contract can ever be instantiated from an indeterministic code. The only way to execute
the code is to use a delegate call from a deterministic contract.
2. The code that wants to use this feature needs to depend on `pallet-contracts` and use `bare_call`
2. The code that wants to use this feature needs to depend on `pallet-contracts` and use [`bare_call()`](https://paritytech.github.io/substrate/master/pallet_contracts/pallet/struct.Pallet.html#method.bare_call)
agryaznov marked this conversation as resolved.
Show resolved Hide resolved
directly. This makes sure that by default `pallet-contracts` does not expose any indeterminism.

## How to use

When setting up the `Schedule` for your runtime make sure to set `InstructionWeights::fallback`
to a non zero value. The default is `0` and prevents the upload of any non deterministic code.
#### How to use

An indeterministic code can be deployed on-chain by passing `Determinism::Relaxed`
to `upload_code`. A deterministic contract can then delegate call into it if and only if it
is ran by using `bare_call` and passing `Determinism::Relaxed` to it. **Never use
to [`upload_code()`](https://paritytech.github.io/substrate/master/pallet_contracts/pallet/struct.Pallet.html#method.upload_code). A deterministic contract can then delegate call into it if and only if it
is ran by using [`bare_call()`](https://paritytech.github.io/substrate/master/pallet_contracts/pallet/struct.Pallet.html#method.bare_call) and passing [`Determinism::Relaxed`](https://paritytech.github.io/substrate/master/pallet_contracts/enum.Determinism.html#variant.Relaxed) to it. **Never use
this argument when the contract is called from an on-chain transaction.**

## Interface
Expand Down Expand Up @@ -99,24 +95,22 @@ Each contract is one WebAssembly module that looks like this:
```

The documentation of all importable functions can be found
[here](https://github.com/paritytech/substrate/blob/master/frame/contracts/src/wasm/runtime.rs).
Look for the `define_env!` macro invocation.
[here](https://paritytech.github.io/substrate/master/pallet_contracts/api_doc/trait.Current.html).

## Usage

This module executes WebAssembly smart contracts. These can potentially be written in any language
that compiles to web assembly. However, using a language that specifically targets this module
will make things a lot easier. One such language is [`ink!`](https://use.ink)
which is an [`eDSL`](https://wiki.haskell.org/Embedded_domain_specific_language) that enables
writing WebAssembly based smart contracts in the Rust programming language.
that compiles to Wasm. However, using a language that specifically targets this module
will make things a lot easier. One such language is [`ink!`](https://use.ink). It enables
writing WebAssembly-based smart-contracts in the Rust programming language.

## Debugging

Contracts can emit messages to the client when called as RPC through the `seal_debug_message`
Contracts can emit messages to the client when called as RPC through the [`debug_message`](https://paritytech.github.io/substrate/master/pallet_contracts/api_doc/trait.Current.html#tymethod.debug_message)
API. This is exposed in [ink!](https://use.ink) via
[`ink_env::debug_message()`](https://paritytech.github.io/ink/ink_env/fn.debug_message.html).

Those messages are gathered into an internal buffer and send to the RPC client.
Those messages are gathered into an internal buffer and sent to the RPC client.
It is up the the individual client if and how those messages are presented to the user.

This buffer is also printed as a debug message. In order to see these messages on the node
Expand Down Expand Up @@ -154,11 +148,11 @@ this pallet contains the concept of an unstable interface. Akin to the rust nigh
it allows us to add new interfaces but mark them as unstable so that contract languages can
experiment with them and give feedback before we stabilize those.

In order to access interfaces marked as `#[unstable]` in `runtime.rs` one need to set
`pallet_contracts::Config::UnsafeUnstableInterface` to `ConstU32<true>`. It should be obvious
In order to access interfaces marked as `#[unstable]` in [`runtime.rs`](src/wasm/runtime.rs) one need to set
`pallet_contracts::Config::UnsafeUnstableInterface` to `ConstU32<true>`. **It should be obvious
that any production runtime should never be compiled with this feature: In addition to be
subject to change or removal those interfaces might not have proper weights associated with
them and are therefore considered unsafe.
them and are therefore considered unsafe**.

New interfaces are generally added as unstable and might go through several iterations
before they are promoted to a stable interface.
Expand Down
14 changes: 14 additions & 0 deletions frame/contracts/fixtures/seal_input_noop.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
;; Everything prepared for the host function call, but no call is performed.
(module
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "env" "memory" (memory 1 1))

;; [0, 8) buffer to write input

;; [8, 12) size of the input buffer
(data (i32.const 8) "\04")

(func (export "call"))

(func (export "deploy"))
)
22 changes: 22 additions & 0 deletions frame/contracts/fixtures/seal_input_once.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
;; Stores a value of the passed size. The host function is called once.
(module
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "env" "memory" (memory 1 1))

;; [0, 8) buffer to write input

;; [8, 12) size of the input buffer
(data (i32.const 8) "\04")

(func (export "call")
;; instructions to consume engine fuel
(drop
(i32.const 42)
)

(call $seal_input (i32.const 0) (i32.const 8))

)

(func (export "deploy"))
)
28 changes: 28 additions & 0 deletions frame/contracts/fixtures/seal_input_twice.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
;; Stores a value of the passed size. The host function is called twice.
(module
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "env" "memory" (memory 1 1))

;; [0, 8) buffer to write input

;; [8, 12) size of the input buffer
(data (i32.const 8) "\04")

(func (export "call")
;; instructions to consume engine fuel
(drop
(i32.const 42)
)

(call $seal_input (i32.const 0) (i32.const 8))

;; instructions to consume engine fuel
(drop
(i32.const 42)
)

(call $seal_input (i32.const 0) (i32.const 8))
)

(func (export "deploy"))
)
54 changes: 44 additions & 10 deletions frame/contracts/proc-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,19 +624,15 @@ fn expand_functions(def: &EnvDef, expand_blocks: bool, host_state: TokenStream2)
let trace_fmt_str = format!("{}::{}({}) = {{:?}}\n", module, name, params_fmt_str);

quote! {
let result = #body;
if ::log::log_enabled!(target: "runtime::contracts::strace", ::log::Level::Trace) {
let result = #body;
{
use sp_std::fmt::Write;
let mut w = sp_std::Writer::default();
let _ = core::write!(&mut w, #trace_fmt_str, #( #trace_fmt_args, )* result);
let msg = core::str::from_utf8(&w.inner()).unwrap_or_default();
ctx.ext().append_debug_buffer(msg);
}
result
} else {
#body
}
result
}
};

Expand All @@ -661,7 +657,7 @@ fn expand_functions(def: &EnvDef, expand_blocks: bool, host_state: TokenStream2)
::core::unreachable!()
} }
};
let map_err = if expand_blocks {
let into_host = if expand_blocks {
quote! {
|reason| {
::wasmi::core::Trap::from(reason)
Expand All @@ -677,6 +673,43 @@ fn expand_functions(def: &EnvDef, expand_blocks: bool, host_state: TokenStream2)
} else {
quote! { #[allow(unused_variables)] }
};
let sync_gas_before = if expand_blocks {
quote! {
// Gas left in the gas meter right before switching to engine execution.
let __gas_before__ = {
let engine_consumed_total =
__caller__.fuel_consumed().expect("Fuel metering is enabled; qed");
let gas_meter = __caller__.data_mut().ext().gas_meter_mut();
gas_meter
.charge_fuel(engine_consumed_total)
.map_err(TrapReason::from)
.map_err(#into_host)?
athei marked this conversation as resolved.
Show resolved Hide resolved
.ref_time()
};
}
} else {
quote! { }
};
// Gas left in the gas meter right after returning from engine execution.
let sync_gas_after = if expand_blocks {
quote! {
let mut gas_after = __caller__.data_mut().ext().gas_meter().gas_left().ref_time();
let mut host_consumed = __gas_before__.saturating_sub(gas_after);
// Possible undercharge of at max 1 fuel here, if host consumed less than `instruction_weights.base`
// Not a problem though, as soon as host accounts its spent gas properly.
let fuel_consumed = host_consumed
.checked_div(__caller__.data_mut().ext().schedule().instruction_weights.base as u64)
.ok_or(Error::<E::T>::InvalidSchedule)
.map_err(TrapReason::from)
.map_err(#into_host)?;
__caller__
.consume_fuel(fuel_consumed)
.map_err(|_| TrapReason::from(Error::<E::T>::OutOfGas))
.map_err(#into_host)?;
}
} else {
quote! { }
};

quote! {
// We need to allow all interfaces when runtime benchmarks are performed because
Expand All @@ -688,10 +721,11 @@ fn expand_functions(def: &EnvDef, expand_blocks: bool, host_state: TokenStream2)
{
#allow_unused
linker.define(#module, #name, ::wasmi::Func::wrap(&mut*store, |mut __caller__: ::wasmi::Caller<#host_state>, #( #params, )*| -> #wasm_output {
#sync_gas_before
let mut func = #inner;
func()
.map_err(#map_err)
.map(::core::convert::Into::into)
let result = func().map_err(#into_host).map(::core::convert::Into::into);
#sync_gas_after
result
}))?;
}
}
Expand Down
Loading