Contract preparation

This document describes the contract preparation and instrumentation process as well as the limitations this process imposes on the contract authors.

In order to provide high performance execution of the function calls, the near validator utilizes ahead-of-time preparation of the contract WASM code. Today the contract code is prepared during the execution of the DeployContractAction. The results are then saved and later reported to the user when a [FunctionCall] is invoked.

Note that only some parts of this document are normative. This document delves into implementation details, which may change in the future as long as the behavior documented by the normative portions of this book is maintained. The non-normative portions of this document will be called out as such.

High level overview

This section is not normative.

Upon initiation of a contract deployment, broadly the following operations will be executed: validation, instrumentation, conversion to machine code (compilation) and storage of the compiled artifacts in the account’s storage.

Functions exported from the contract module may then be invoked through the mechanisms provided by the protocol. Two common ways to call a function is by submitting a function call action onto the chain or via a cross-contract call.

Most of the errors that have occurred as part of validation, instrumentation, compilation, etc. are saved and reported when a FunctionCallAction is submitted. Deployment itself may only report errors relevant to itself, as described in the specification for DeployContractAction.

Validation

The table below lists the limits that apply to a contract deployed at protocol version 88 (nearcore 2.14.0). When the parser and a later check both limit the same thing, the table gives the lower limit.

What is limitedLimitRuntime parameter
Size of the wasm code4_194_304 bytesmax_contract_size
Types (signatures) in the module1_024max_types_per_contract
Function imports and functions defined in the module10_000max_functions_number_per_contract
Parameters of one function defined in the module64max_params_per_function
Parameters of all functions defined in the module50_000max_params_per_contract
Parameters of one signature, for example of an imported function1_000None (parser limit)
Results of one signature1None (multi-value is not enabled)
Locals of one function50_000None (parser limit)
Locals of all functions1_000_000, and at most the code size in bytes divided by 2max_locals_per_contract, min_contract_size_per_local
Size of one function body196_608 bytesmax_function_body_size
block, loop and if instructions in one function5_000max_blocks_per_function
block, loop and if instructions in the module50_000max_blocks_per_contract
Operand stack of one function8_192 bytesmax_operand_stack_bytes_per_function
Size of the module after instrumentation16_777_216 bytesmax_instrumented_code_size
Tables1max_tables_per_contract
Initial size of a table10_000 entriesmax_elements_per_contract_table
ImportsFunction imports from the env module onlyNone

The parser also limits the number of imports, exports, globals and data segments, and the length of names. Only the first violation is reported, so the order of the checks matters. The checks run in the four steps below, one step after another.

Before preparation starts, the validation of the deploy action rejects code longer than the max_contract_size runtime parameter.

Then the module is parsed. The parser reads the sections in the order of the binary encoding and reports the first violation that it finds. So within this step, the order of the checks depends on the module, not on the order of this list. These limits are checked while parsing:

  • The wasm module must be a valid module according to the WebAssembly core 1.0 specification with these extensions enabled: sign-extension operators, non-trapping float-to-int conversions, and, from protocol version 84, reference types and bulk memory operations. Other extensions, such as multi-value returns, SIMD, threads, tail calls, multiple memories, memory64 and exception handling, are rejected;
  • The wasm module may contain no more than:
    • 1_024 types (signatures), set by the max_types_per_contract runtime parameter from protocol version 84;
    • 1_000_000 function imports and local function definitions;
    • 1_000_000 imports;
    • 1_000_000 exports;
    • 1_000_000 global imports and module-local global definitions;
    • 100_000 data segments;
    • 1 memory;
  • UTF-8 strings comprising the wasm module definition (e.g. export name) may not exceed 100_000 bytes each;
  • Function definitions may not specify more than 50_000 locals;
  • Signatures may not specify more than 1_000 parameters;
  • Signatures may not specify more than 1 result, because the multi-value proposal is not enabled;
  • All imports must be function imports from the env module. A module that imports a table, a global or a memory is rejected. The runtime provides the contract memory itself, as described in Memory normalization;
  • The wasm module may contain no more than max_tables_per_contract (1) tables;
  • Tables may not specify an initial size of more than 10_000 entries (the max_elements_per_contract_table runtime parameter, from protocol version 82);
  • The wasm module may contain no more function imports and local function definitions than the max_functions_number_per_contract runtime parameter (10_000);
  • Each function body may not exceed max_function_body_size (196_608 bytes), from protocol version 84;
  • The total number of locals declared in all functions may not exceed max_locals_per_contract (1_000_000). From protocol version 87 (nearcore 2.14.0), the total also may not exceed the contract size in bytes divided by min_contract_size_per_local (2).

Then preparation instruments the module. It checks the functions one by one. For each function it checks the parameters first, then the operand stack, then the blocks. These limits apply from protocol version 84:

  • Functions defined in the module may not have more than 64 parameters each (max_params_per_function). All functions defined in the module together may not have more than 50_000 parameters (max_params_per_contract).
  • The operand stack of each function may not exceed max_operand_stack_bytes_per_function (8_192 bytes).
  • Each function may contain no more than max_blocks_per_function (5_000) block, loop and if instructions. The whole module may contain no more than max_blocks_per_contract (50_000) of these instructions.

Last, the module after instrumentation may not exceed max_instrumented_code_size (16_777_216 bytes), from protocol version 84.

Memory normalization

All near contracts have the same amount of memory made available for execution. The exact amount is specified by the initial_memory_pages (1_024) and max_memory_pages (2_048) runtime parameters, in pages of 64 KiB. In order to ensure a level playing field, preparation removes any memory definition of the module and adds one memory definition with these limits. A module without a memory definition gets the same memory. The limits that the module declared are not used. A module that imports a memory is rejected.

Gas instrumentation

In order to implement precise and efficient gas accounting, the contract code is analyzed and instrumented with additional operations before the compilation occurs. One such instrumentation implements accounting of the gas fees.

Gas fees are accounted for at a granularity of sequences of instructions forming a metered block and are consumed before execution of any instruction part of such a sequence. The accounting mechanism verifies the remaining gas is sufficient, and subtracts the gas fee from the remaining gas budget before continuing execution. In the case where the remaining gas balance is insufficient to continue execution, the GasExceeded error is raised and execution of the contract is terminated.

The gas instrumentation analysis will segment a wasm function into metered blocks. In the end, every instruction will belong to exactly one metered block. The algorithm uses a stack of metered blocks and instructions are assigned to the metered block on top of the stack. The following terminology will be used throughout this section to refer to metered block operations:

  • active metered block – the metered block at the top of the stack;
  • pop – the active metered block is removed from the top of the stack;
  • push – a new metered block is added to the stack, becoming the new active metered block.

A metered block is pushed onto the stack upon a function entry and after every if and loop instruction. After the br, br_if, br_table, else & return (pseudo-)instructions the active metered block is popped and a new metered block is pushed onto the stack.

The end pseudo-instruction associated with the if & loop instructions, or when it terminates the function body, will cause the top-most metered block to be popped off the stack. As a consequence, the instructions within a metered block need not be consecutive in the original function. If the if..end, loop..end or block..end control block terminated by this end pseudo-instruction contained any branching instructions targeting control blocks other than the control block terminated by this end pseudo instruction, the currently active metered block is popped and a new metered block is pushed.

Note that some of the instructions considered to affect the control flow in the WebAssembly specification such as call, call_indirect or unreachable do not affect metered block construction and are accounted for much like other instructions not mentioned in this section. This also means that calling the used_gas host function at different points of the same metered block would return the same value if the base cost was 0.

All the instructions covered by a metered block are assigned a fee based on the regular_op_cost runtime parameter. The block, else and end instructions do not cause any fee to be charged. A sum of these fees is then charged by instrumentation inserted at the beginning of each metered block.

The memory.grow, memory.init, memory.copy, memory.fill, table.grow, table.init, table.copy and table.fill instructions are not part of this sum. Each of them is charged just before it runs: linear_op_base_cost plus linear_op_unit_cost for each unit of its count operand.

Examples

This section is not normative.

In this section some examples of the instrumentation are presented as an understanding aid to the specification above. The examples are annotated with comments describing how much gas is charged at a specific point of the program. The programs presented here are not intended to be executable or to produce meaningful behavior.

block instruction does not terminate a metered block

(func
  (; charge_gas(5 regular_op_cost) ;)
  nop
  block
    nop
    unreachable
    nop
  end
  nop)

This function has just 1 metered block, covering both the block..end block as well as the 2 nop instructions outside of it. As a result the gas fee for all 6 instructions (5 regular_op_cost, as block is free) will be charged at the beginning of the function (even if we can see unreachable would be executed after 4 instructions, terminating the execution).

Branching instructions pop a metered block

Introducing a conditional branch to the example from the previous section would split the metered block and the gas accounting would be introduced in two locations:

(func
  (; charge_gas([nop block br.0 nop]) ;)
  nop
  block
    br 0
    (; charge_gas([nop nop]) ;)
    nop
    nop
  end
  nop)

Note that the first metered block is disjoint and covers the [nop, block, br 0] instruction sequence as well as the final nop. The analysis is able to deduce that the br 0 will not be able to jump past the final nop instruction, and therefore is able to account for the gas fees incurred by this instruction earlier.

Replacing br 0 with a return would enable jumping past this final nop instruction, splitting the code into three distinct metered blocks instead:

(func
  (; charge_gas([nop block return]) ;)
  nop
  block
    return
    (; charge_gas([nop nop]) ;)
    nop
    nop
  end
  (; charge_gas([nop]) ;)
  nop)

if and loop push a new metered block

(func
  (; charge_gas([loop unreachable]) ;)
  loop
    (; charge_gas([br 0]) ;)
    br 0
  end
  unreachable
)

In this example the loop instruction will always introduce a new nested metered block for its body, for the end pseudo-instruction as well as br 0 cause a backward jump back to the beginning of the loop body. A similar reasoning works for if .. else .. end sequence since the body is only executed conditionally:

(func
  (; charge_gas([i32.const.42 if nop]) ;)
  i32.const 42
  if
    (; charge_gas([nop nop]) ;)
    nop
    nop
  else
    (; charge_gas([unreachable]) ;)
    unreachable
  end
  nop)

Consensus stack metering

This limit does not measure the native stack that Wasmtime uses. For each function, preparation computes its maximum WebAssembly operand stack size and a frame charge. Instrumentation subtracts their sum from a budget when the function is entered, and adds it back when the function returns. The budget is a protocol parameter, so all nodes stop a contract at the same point.

The operand stack holds the values that WebAssembly instructions push and pop, as defined in section 4 of the WebAssembly core 1.0 specification. Each value uses 4 bytes (i32, f32) or 8 bytes (i64, f64, references). The frame charge of a function is 64 bytes plus the size of its locals.

The max_stack_height runtime parameter (262_144 bytes) is the budget. If entering a function would take the budget below zero, the execution of the contract stops with a MemoryAccessViolation error. From protocol version 84, a function whose maximum operand stack size exceeds max_operand_stack_bytes_per_function (8_192 bytes) is rejected at preparation.

The maximum operand stack size of each function, in bytes, is computed statically by simulating the operand stack operations executed by each instruction. For example, i32.const 1 pushes one 4-byte value on the operand stack, whereas i32.add pops two values and pushes one value containing the result. The maximum operand stack size of the if..else..end control block is the larger of the sizes for the two bodies of the conditional. Stack operations for each instruction are otherwise specified in the section 4 of the WebAssembly core 1.0 specification and in the enabled extensions.

Both the operand stack sizes and the gas fees are computed from the code before any instrumentation is added.

Examples

This section is not normative.

Picking the example from the gas instrumentation section, without the gas charges, we can tell that this function needs 4 bytes of operand stack. Let's annotate the operand stack operations at each of the instructions:

(func
  i32.const 42             (; [] => [i32]       ;)
  if                       (; [i32] => []       ;)
    nop                      (; []                ;)
    nop                      (; []                ;)
  else
    unreachable              (; []                ;)
  end
  nop                      (; [] ;)
)

At no point does the operand stack contain more than one i32 value. As a result, the maximum operand stack size of this function is 4 bytes. When the function starts, the runtime takes these 4 bytes plus the frame size of the function from the stack budget.