Contents

Gas ⛽ Metering for Wasm Programs

In this post we consider the following methods of gas metering for Wasm contracts:

  1. By preparatory instrumentation of the program module.

  2. By the Wasm executor engine itself.

Wasm Module Instrumentation

Instrumentation means a process of injecting auxiliary instructions into the module blob. This is generally done once and results in a new instrumented module which allows host to measure gas spent during its execution.

Tip
There is a host program and a guest program. In our case the host is the chain runtime and the guest is smart contract itself. Guest is being executed in a special sandboxed environment so that it cannot access data of the host unauthorized. For everything it needs from the world outside the sandbox, guest uses special API exposed to him by the host. Guest utilizes this API through importing special functions called host functions.

There are several ways of doing such an instrumentation. Here we consider two of them:

  1. Injection of gas host function calls.

  2. Injection of gas_left mutable global which is shared with the host.

Metering via Host Function Calls

How it works

Instrumentation:

  1. Injects the gas host function import into the module.
  2. Goes through all instructions in every program block and sums up gas charges implied for them.
  3. Injects an invocation of the gas host function with a constant value of the gas amount to be charged for all the instructions of the block.

Metering:

  1. Upon starting a contract call, host tracks gas_left during the call execution.
  2. Executed contract calls the host function at the beginning of each block.
  3. Host function charges the gas amount passed in by subtracting it from the gas_left.
  4. In case gas_left falls to zero, host interrupts contract execution and returns OutOfGas error to the caller.
Note
Quite nice, brief and capacious description of this method can be also found here.

Example

Let’s take a simple example of a smart contract which returns the caller account balance:

By the way, this program is a valid Polkadot smart contract which you can deploy to any parachain having runtime equipped with pallet_contracts. For that you need to convert it to binary Wasm, which can be done e.g. with wat2wasm utility from the WebAssembly Binary Toolkit. When contract code gets uploaded to the pallet (usually via upload_code extrinsic), it undergoes a number of checks as well as gas metering instrumentation.

Note
What we describe in this section is how pallet_contracts worked before switching to executor’s built-in gas metering (see substrate#14084 PR for details). Newer versions use engine-sided gas metering which does not require instrumentation. More on this in the next section.

Now let’s take a look at how this works in practice. The described operations on the contract module are being done by the wasm-instrument crate. To see particular injections in a human-readable format we convert the .wat source code to the Wasm binary and then back to text format via wat2wasm <file.wat> | wasm2wat and then compare it with the resulting module converted to the same format by wasm2wat.

Here is how our module looks before instrumentation:

Control flow graph (drawn with octopus tool) of this Wasm program is:

Now here is how this program’s code looks after instrumented with gas metering:

And control flow graph after instrumentation:

Injected gas metering instructions are marked with yellow. The amount of gas being charged in each block (i64.const instructions in the marked chunks) is calculated by summing up the cost of all instructions of the block. The costs of all instructions are defined in the cost schedule, which has them calculated via benchmarks. But this is another topic which deserves a dedicated post.

Cons of This Metering Method

Calling a host function is a computationally expensive operation, as it requires execution context switching between guest and host. Doing this in each basic block of a guest program adds up performance costs.

Metering via Mutable Global

Tip
This method has been implemented in this pull request: wasm-instrument#34. Take a look into it for more details.

How it works

Instrumentation:

  1. Injects a mutable global variable gas_left into the module.

    The variable is exported from the module having initialized value of 0.

  2. Injects a local gas function definition into the module.

    The function code is:

    It tries to decrease the gas_left global value by the passed in gas amount, raising an error in case there is not enough gas left.

  3. Goes through all instructions in every program block and sums up gas charges implied for them.

  4. Injects an invocation of the local gas function with a constant value of the gas amount to be charged for all the instructions of the block.

Metering:

  1. Upon staring a contract call, the host initializes the gas_left global (shared with the guest) with the gas amount left for the contract execution.

  2. Executed contract calls its injected local gas function at the beginning of each basic block.

    The function decreases gas_left global variable by the gas amount needed for the block execution, failing the execution if not enough gas left. In order to signal the host the exact reason of the failure, the guest sets the gas_left value to the special sentinel value which equals -1i64 and casts to u64::MAX if host (reasonably) uses unsigned integer for representing gas amounts.

  3. If all gas is exhausted (which can happen both in host and guest execution contexts), the host interrupts contract execution and returns OutOfGas error to the caller.

Example

Let’s instrument the same balance contract with the mutable global -based gas metering facilities.

We get the following code as the result:

This code has the following control flow graph:

Injected gas metering code is marked with yellow. As we see, it has the injected local gas function (on the right) as well as its invocation injected into every block.

Method Validity

Whether this method is beneficial to use in comparison to the host function method, should be determined by benchmarking for every particular execution engine as well as specific gas metering host function implementation. For some engines, the costs of calling an imported function and calling a local function are the same, and for them using this method would not necessarily bring performance wins.


Engine-resided Metering of Gas

It is good to have gas metering implemented in the execution engine itself, for the following reasons:

  1. It is likely to occur more optimal performance-wise.
  2. No code instrumentation required, which makes embedder logic simpler and saves storage and computational resources for the host.
  3. No benchmarks required on the embedder side.
Note
Engine-sided fuel metering does brings performance costs as well. Still normally they are lower than in the instrumentation-based approaches. And more optimization ideas are coming for improving it further. Look for example into this wasmtime proposal for details.

Now let’s consider how this approach works in general and see some details on the particular example of Substrate’s pallet_contracts use case.

How it works

Basic Flow

While specific implementations might vary a bit in some details, overall algorithm is mostly the same:

  1. Fuel metering feature is usually optional and is set to off by default.

    It’s needed to be opted-in in the engine config when spinning an instance.

  2. Current gas level is being stored into the state.

    Engine initializes it to 0. Upon spinning an execution instance, embedder sets the initial value of the gas left.

    This value is usually set by the user who calls the contract as gas_max value, meaning the upper limit of the gas (essentially funds) the user is ready to pay for the execution.

  3. Engine charges the gas during execution.

    Cost schedule is either hardcoded into the engine or provided from the outside (different in different engines). Units of gas measurement are usually normalized so that the minimal (base) fuel cost is 1.

    Different instructions cost different amounts of gas. Some of them could even cost 0 as they could only be present in Wasm program together with other instructions (examples are loop, block, drop and nop, see here for details).

  4. During the execution embedder can check either gas_consumed amount or gas_left amount (depending on particular engine’s implementation).

  5. During execution embedder can arbitrary charge additional fuel for the operations on its side, e.g. for a host function execution.

    For that a special charge_gas function provided by the engine.

  6. When fuel in store is exhausted, engine traps Wasm execution with OutOfGas error.

    While this seems to be an obvious outcome, engines supporting asynchronous execution allow pit stops for refueling.

Particular Implementations

Without getting too deep into the details, in a nutshell here’s how three popular Wasm engines deal with gas metering:

  • wasmtime delegates gas charges to its code generator Cranelift, which processes it during translation of each operator into its IR instructions.

    Costs schedule is as simple as it gets and is hardcoded here.

  • wasmi is an interpreter, thus it seems reasonable that it charges gas for each executed instruction during a program run.

    For that it adds ConsumeFuel instruction into its IR of the program. Costs schedule is defined in the engine config currently without a method to customize it (though with the design taken this could be done as a one-liner change).

  • wasmer basically injects a mutable global and updates it during proccessing each operator of the Wasm code, adding a bunch of instructions to check if it is exhasted.

    This looks quite similar to the technique we’ve discussed in the previous section. Albeit wasmer appears to inject Wasm instructions of the checker code on every operation, which seems a bit wasteful approach resource-wise. It has no internal costs schedule and expects it to be provided by the embedder as a specific cost_function, which also reminds the out-of-engine metering facilities described above.

APIs

These engines expose the following interfaces which we list here with the mapping to the basic steps of gas metering flow:

Step wasmtime wasmi wasmer
1 Config::consume_fuel Config::consume_fuel CompilerConfig::push_middleware
2 Store::add_fuel Store::add_fuel set_remaining_points
3 fuel_before_op Config::FuelCosts cost_function
4 Store::fuel_remaining, Store::fuel_consumed Store::fuel_consumed get_remaining_points
5 Store::consume_fuel Store::consume_fuel set_remaining_points
6 Store::out_of_fuel_trap FuelError::OutOfFuel traps with Unreachable

Now let’s move on to some use case examples of those gas metering APIs.

Examples

  1. wasmtime: an example of using the metering API through Go bindings.
  2. wasmer: see metering example from the project repository.
  3. wasmi: on this example I’d like to dwell in a bit more detail since work on it is what inspired me to write this post.
Tip
Polkadot smart contracts switched to engine-sided gas metering in this PR: substrate#14084. Its diff shows how moving from out-of-engine to in-engine fuel metering can benefit both your code base clarity and runtime efficiency.

Instantiation

Upon a contract call invocation pallet instantiates a fresh wasmi instance. Its engine is configured to account for the fuel consumption in the Eager mode:

let mut config = WasmiConfig::default();
config
	.wasm_multi_value(false)
	.wasm_mutable_global(false)
	.wasm_sign_extension(false)
	.wasm_bulk_memory(false)
	.wasm_reference_types(false)
	.wasm_tail_call(false)
	.wasm_extended_const(false)
	.wasm_saturating_float_to_int(false)
	.floats(matches!(determinism, Determinism::Relaxed))
	.consume_fuel(true)
	.fuel_consumption_mode(FuelConsumptionMode::Eager);

Fuel Syncs

We have now two separate fuel meters, each having its own units of measurement:

  1. Gas Meter built into the pallet, measures 2-dimensional Weight1 burn during runtime host function execution.
  2. Fuel Meter built into the wasmi, measures 1-dimensional Fuel burn in engine for Wasm instructions execution.

UoMs for fuel consumption in wasmi are normalized, so that basic operations like i64.const cost 1 Fuel. For conversion between the two UoMs, the ref_time component of i64.const instruction Weight is used. This is why we keep its benchmark and its weight in the Schedule

We do fuel consumption level synchronizations at the times of execution context switching, namely:

  1. Right before starting Wasm module execution in the engine.
  2. At very beginning and very end of each host function (which is generated with the proc macro).
  3. Right after completed module execution in the engine.

Storage Migration

Contract code instrumented with metering instructions had to be stored on-chain for every contract. This added up the costs for contract authors in the form of additional storage deposit reserved when uploading contract, as well as cost of performing the instrumentation, which burnt additional gas as well.

Note

When the changes described got deployed on-chain via a runtime upgrade, a corresponding migration has been run which

  • Freed up >50% of storage occupied with contract codes.
  • Unlocked >40% of funds locked for the storage deposits of the code stored.

Code Became Simpler

Before switching to engine-sided fuel metering, we also had to re-instrument contract code every time the cost schedule was changed. First, it took additional gas burnt for the instrumentation itself. Second, this approach required a bunch of caching logic to guarantee the contract was always run with the latest schedule. All those inefficiencies were wiped out with the change.

More to that, we don’t need to benchmark all Wasm instructions in the pallet anymore, except the single one which serves as a conversion factor base between UoMs of two meters, which looks like this:

Conclusion: Which One Is Better?

Module instrumentation-based fuel metering allows host to be Wasm executor agnostic, which could be preferable at early stages of a project development when it is not yet decided which particular execution engine would be best to use in long term.

On the other hand, it brings additional costs for performing this instrumentation upon each smart contract uploaded to the runtime. Also, it bulks the contract code significantly, while storage is a precious resource especially in blockchain-specific environments. These two points make this type of metering more expensive for the end user, as she pays more for the gas and storage used for the contract.

Last but not the least, it makes the host program implementation being based on certain assumptions, and it requires her to write and maintain a set of benchmarks for every Wasm instruction. In practice this means artificially reducing the scope of Wasm features being used in smart contracts.

Well-designed Wasm engine provides a clear and simple API both for validating the module and for its execution, including fuel metering functional. Therefore, even using specific engine does not necessary make the host being tightly coupled with it. That is why we consider using engine-resided gas metering as a more beneficial approach long-term.


  1. See substrate#10918 for more details on Weight V2 (aka 2-dimensional weight); ↩︎