Code Metering In CoinEx Smart Chain: Part I
Written by the CoinEx Chain lab, this article is the part one of the Code Metering In CoinEx Smart Chain. CoinEx Chain is the world’s first public chain exclusively designed for DEX, and will also include a Smart Chain supporting smart contracts and a Privacy Chain protecting users’ privacy.
Outline
CoinEx Smart Chain supports WebAssembly (Wasm for short) as the underlying virtual machine, and the smart contract code written by Rust / C ++ / AssemblyScript will be compiled into Wasm binary format and stored on the chain. To avoid unlimited consumption of computing and storage resources by smart contracts, we introduced Gas metering rules to limit the resource consumption of smart contracts. Each call of the smart contract requires the user to specify the amount of Gas he can provide and the fees to pay. Contract execution, during which Gas is exhausted, is considered failed, and the corresponding status change will be discarded. At the same time, if there is still gas remaining after the contract is executed, the corresponding fees can be returned to the user (or not). Since the Wasm specification does not include Gas metering instruction, we need to manually insert the corresponding metering instructions into the compiled Wasm module, while minimizing the performance impact on contract execution.
The Gas charging function implemented on CoinEx Smart Chain has two major characteristics:
To begin with, it is VM-Side Metering. The first question to be considered in the Metering program is where to maintain the remaining Gas. In a specific implementation, you can choose to maintain in the Host or the VM. If it is maintained in the Host, the Host function call needs to be frequently made in the VM to timely update current Gas consumption, at the cost of large performance loss. If it is maintained in the VM, you only need to call the external function exposed by the VM from the Host side to obtain and update the surplus of the current Gas when calling across the contract, and the performance loss is relatively small. However, the maintenance of remaining Gas in the VM requires more complex design. We will detail the design of Metering in CoinEx Smart Chain and how we do VM-Side Metering later.
The second major feature is to support two kinds of metering granularity at the same time: Basic-block-based & Super-block-based. Based on the size of the metering granularity, the metering strategy can be divided into three catogeries, respectively Instruction-based, Basic-block-based, and Super-block-based. Instruction-based Metering inserts metering logic to each instruction, and its advantages are evident: it can detect the Gas over-limit in the most timely manner, yet at the cost of large performance loss. Basic-block-based Metering inserts metering logic to each instruction block (ie, basic block) uninterrupted by a jump instruction. Compared with Instruction-based Metering, it leads to smaller performance loss. Super-block-based Metering only inserts metering logic into instructions that may cause the PC to jump in the direction of reduction, function call, and return (that is, the Branch instruction and Return instruction matching Loop), with the smallest performance loss. Admittedly, compared with Basic-block-based Metering, Super-block-based Metering cannot do accurate metering for each basic-block, but this does not prevent it from limiting the unlimited consumption of smart contract resources. Therefore, the implementation of CoinEx Smart Chain supports both Basic-block-based & Super-block-based metering strategies.
All the above are planned to be introduced in two articles, and this article focuses on the implementation logic of the Basic-block-based & Super-block-based Metering strategies in CoinEx Smart Chain. Next time we will give a Benchmark comparison between these two Metering strategies.
GasLimit as a global variable
To implement Gas metering on VM-Side, we need to introduce a global variable (denoted as GasLimit) in the Wasm module to record the currently available Gas. When the user sends a transaction with contract call, the variable will be initialized according to the Gas attached to the transaction. During the execution of Wasm bytecode, the GasLimit variable will be updated at specific positions, If Gas used exceeds the limit , the contract execution will be terminated immediately.
As the Wasm contract requires not only VM support but also some external host functions. In order to enable the host environment to obtain and update remaining Gas, we need to export two functions in the compiled Wasm module: getGasLimit
& setGasLimit
. When initializing a virtual machine, we need to call setGasLimit
to initialize GasLimit for the contract execution. At the end of the contract execution, the host environment needs to call getGasLimit
to get remained Gas. At the same time, when the current contract calls another contract, it is also necessary to obtain the remaining Gas at the current position, and then initialize the GasLimit of the next contract according to this value. Conversely, after the callee’s contract execution ends, the currently remaining Gas needs to be updated in the caller’s contract instance. For all the above, we need two export functions: getGasLimit
& setGasLimit
.
Figure 1: Initialization and Update of GasLimit Variable When a User / Contract Initiates a Contract Call
Branch operation triggers a new metering Unit
In the previous section, we mentioned that the GasLimit
updating instructions will be inserted at specific positions of the Wasm module. The selection of these specific positions is related to the Basic-block-based & Super-block-based Metering strategies. Now take a look at the Basic-block-based Metering strategy first.
Basic-block-based Metering
Suppose we scan some function of a Wasm module from the beginning, and the Gas consumed for each Wasm instruction is 1. For accurate charging of instructions, every time we cross an instruction, we add 1 to the accumulated Gas consumption. At this time, when an ‘if’ instruction is encountered, execution path will fork. Since we cannot determine which path will be chosen during execution, we need to update the global variable of GasLimit
at the end of these two paths, respectively, so these three locations belong to the specific positions mentioned above. That is to say, between two adjacent specific positions, the execution path of the instruction sequence is uniquely determined, and will diverge after the current position. Instructions provided in Wasm that satisfy the above properties are: if / else / end / br / br_if / br_table / loop
. Besides, in order to update the Gas at the end of the function run, it is necessary to add the return
instruction.
Furthermore, if another function is called inside current function, the Gas consumed accumulatively needs to be updated before the function call. This is to prevent a malicious function that does nothing but calls itself recursively. Instructions related to function call in Wasm includes: call / call_indirect
.
In addition, there is an instruction that needs special handling: memory.grow
. To prevent the contract from requesting huge memory resources from the virtual machine, this operation will be charged in advance. At the same time, the Gas consumption of this instruction is proportional to the requested memory page size. For example, if the scale factor is 1000, for every one memory page requested, 1 * 1000 Gas will be consumed.
We refer to the instructions corresponding to these specific positions as branch operations
, which contain the following Wasm instructions:
var branchingOps = []byte{
operators.End,
operators.GrowMemory,
operators.Br,
operators.BrIf,
operators.BrTable,
operators.If,
operators.Else,
operators.Return,
operators.Loop,
operators.Call,
operators.CallIndirect,
}
The Wasm code will eventually be divided into several sections by the above instructions. The current Gas consumption will be updated at the end of each section, and the contract execution will be terminated if the GasLimit recorded is below zero.
Super-block-based Metering
Compared with Basic-block-based Metering, Super-block-based Metering doesn’t aim at accurate Gas charging but updating the remaining Gas in front of the position that may cause large/unlimited resource consumption. In other words, only the Br / BrIf / BrTable instruction, GrowMemory instruction, and Return instruction that will jump to the beginning of the Loop will trigger the update of Gas. That can minimize the performance loss caused by Metering while preventing smart contracts from consuming system resources indefinitely.
The branch operations
of Super-block-based Metering include the following Wasm instructions:
var branchingOps = []byte{
operators.GrowMemory,
operators.Br,
operators.BrIf,
operators.BrTable,
operators.Return,
}
For instructions that jump to the beginning of the Loop, the Gas charged is proportional to the number of all instructions in the Loop; for the Return instruction, the Gas charged is proportional to the number of instructions contained in the function.
Gas consume for external functions
The contract written by the user needs to obtain some information from the host environment, such as function execution parameters and block height. To solve this, CoinEx Smart Chain also provides some external functions for the Wasm virtual machine to call. Compared with Wasm instructions, external functions, when executed, often consume more resources, so the charge of external functions is particularly important. One of the most direct approaches is to call the getGasLimit
& setGasLimit
exported by the Wasm module at the end of the external function to update Gas consumption. That costs more because of a switch between the Wasm virtual machine and the host environment back and forth. Similarly, we still want to keep Gas consumption of the external function inside the virtual machine.
Here is our solution. We determine whether the called function is an internal one or an external one according to the funcIndex of the call
instruction’s parameter when the call instruction of Wasm is scanned (the order of function index in the Wasm virtual machine specification is increased from the external function to the internal). If it is an external function, according to the predetermined table of Gas consumed by external functions, we get the amount of Gas to be consumed in this call, and update GasLimit. If it is an internal function, no action is required. When it is a call_indirect
instruction, things are somewhat different.
First of all, let’s roughly explain the usage of the call_indirect
instruction: The Opcode of the call_indirect
instruction is 0x11
, followed by the type
index of the called function as an immediate argument. During execution, an i32
value will be popped up from the stack as the function index. Get the type index of the called function from the table
section of the Wasm module, and compare whether the signature of the function actually called is consistent with the function signature pointed by the type index. Since the function that is actually called cannot be determined by static analysis, we can’t determine in advance whether the call_indirect
instruction has called an internal function or an external one, neither can we deal with Gas consumption of the external function by a similar approach for the call
instruction. Yet since the call_indirect
instruction is mainly used to support the functions of dynamic dispatch in such high-level languages as C ++ / Rust: function pointers, virtual functions, trait objects, etc., we impose a restriction on this instruction: the call_indirect
instruction is not allowed to call external functions. We will insert such a check in the Wasm binary file. It should be pointed out that this does not affect the functionality of the contract, and it also makes it easier to handle Gas. Under this restriction, there is no need for additional Metering processing for call_indirect
.
Conclusion
So far we have fully introduced the principle of Gas metering in CoinEx Smart Chain for the Wasm contract and the two strategies: Basic-block-based Metering & Super-block-based Metering. They differ only in the insertion position of the logic of Gas consumption. Still, Basic-block-based Metering is more accurate in calculating Gas charging, and Super-block-based Metering outruns the other by performance. In the next article, we will give a comparison of Benchmark based on these two strategies.