The cost of executing a transaction is measured in gas, and counted by updating the
We charge gas for 5 categories of operations:
- WASM opcode execution inside contract calls.
- Reading and writing WASM memory from host functions inside contract calls.
- Transaction-related data storage.
- World state storage and access.
- Cryptographic operations inside contract calls.
These categories are not an exhaustive enumeration of all of the tasks that a validator does to maintain service for users, but are what we deem to be the tasks that are either most expensive, or most variable, and therefore most vulnerable to abuse by users if they are free.
The following sections specify the gas costs of each category of operations, and then defines the transaction inclusion cost
The basic principle in deciding the cost of executing a WASM opcode is 1 CPU cycle, 1 gas unit.
Precisely, the cost of each opcode was decided by compiling each opcode one-by-one into x86 assembly using the online WebAssembly Explorer tool, and then counting the latency of the generated x86 assembly instructions in a contemporary Intel Core processor (Coffee Lake) using Fog's instruction tables for reference.
For simplicity, we ignore the memory pressure caused by executing certain opcodes and highly CPU-specific details such as instruction pipelining and energy use, and we also do not specify the gas costs of illegal opcodes.
The gas cost of executing every legal opcode is specified at the end of this document in the Appendix for readability.
We assume that the cost of loading a byte from a guest WASM module's memory into host code is roughly 1/8th as the cost of loading 8 bytes from the module's memory into a virtual register using the I64Load instruction, and the converse for storing a byte.
| Name | Formula | Description |
|---|---|---|
| Cost of reading |
||
| Cost of writing |
As alluded to previously, we charge gas to store two kinds of data:
- Transaction-related data (Transactions and their Receipts) which are stored in Blocks.
- Tuples in the world state.
In deciding the costs of both kinds of data storage, we use Ethereum as the baseline and then make adjustments based on a specific desideratum: we want applications to use less storage.
To achieve this desideratum, we set the cost of writing 1 byte as a multiple of the cost of 1 CPU cycle to be 2 or 4x greater than Ethereum for transaction-related data and world state data respectively.
Transaction-related data includes every byte in the Borsh-serialization of transactions (and its fields), and receipts (and its fields). Each byte of transaction-related data costs
| Formula | Value | Description | Ethereum counterpart |
|---|---|---|---|
| 30 | Cost of including 1 byte of data in a Block as part of a transaction or a receipt. |
The cost of storing a transaction and the fixed-sized component of its receipt is known before the transaction is charged in the transaction's inclusion cost. On the other hand, the size of the dynamic-sized contents of command receipts, namely return values and logs, are dynamically charged during execution.
An amount of gas called the "transaction inclusion cost" is charged before every other step in a transaction's execution by initializing the $gc$ Runtime state variable to it:
| Formula | Value | Description |
|---|---|---|
| The transaction inclusion cost of a transaction |
The above formula consists of two terms that are added together:
- The first term accounts for storing the transaction and its "minimal-size" receipt.
- The second term accounts for getting and then setting 5 pair of keys in the Accounts Trie in the pre-charge and charge execution phases:
- The signer's nonce.
- The signer's balance, in the pre-charge phase.
- The signer's balance again, in the charge phase.
- The proposer's balance.
- The treasury's balance.
The minimal-size receipt of any transaction with Vec<CommandReceipt> containing
| Formula | Value | Description |
|---|---|---|
| Size of a receipt containing |
||
| Size of a single minimal-sized command receipt. |
An MPT is a dictionary with three fundamental access operations:
- set: create a mapping between a variable length and a variable length value. Setting a key to an empty value is equivalent to deleting a key.
- get: get the value mapped to a key.
- contains: check whether a key is mapped to a value.
This section defines cost formulas for each fundamental access operation first on the Accounts Trie, and then a Storage Trie, and finally on a generic MPT.
Getting, setting, and checking for the existence of a key-value pair in the Accounts Trie simply entails doing the corresponding operation in the dedicated Accounts Trie MPT.
| Symbol | Formula | Description |
|---|---|---|
| Cost of getting a key of length |
Getting contract code from the Accounts Trie is discounted:
| Symbol | Formula | Description |
|---|---|---|
| 50% | Proportion of |
| Symbol | Formula | Description |
|---|---|---|
| Cost of setting a key of length |
| Symbol | Formula | Description |
|---|---|---|
| Cost of checking whether a key with length |
The cost of getting, setting, or checking for the existence of a key-value pair in a storage trie where key is of length
| Formula | Value | Description |
|---|---|---|
| 33 | The length of Accounts Trie keys. |
The first two terms of the key in the combined MPT simulates an architecture that has each storage MPT 'attached' as a subtrie of the Accounts Trie on the tuple that stores the account's storage hash, while the
| Symbol | Formula | Description |
|---|---|---|
| Cost of getting a key of length |
| Symbol | Formula | Description |
|---|---|---|
| Cost of setting a key of length |
| Symbol | Formula | Description |
|---|---|---|
| Cost of checking whether a key with length |
To decide the cost of each MPT operation, we imagine how a simple implementation of an MPT may implement the operation.
To get a key of length
- Traverse down the MPT until we reach a matching node or dead end.
- Read and return its value, or none, if we reached a dead end.
| Symbol | Formula | Description |
|---|---|---|
| Cost of getting a key of length |
||
| Cost of step 1 of get. | ||
| Cost of step 2 of get. |
To set a key of length
- Get the key.
- Delete the old value for a refund.
- Write a new value.
- Recompute node hashes until the root.
| Symbol | Formula | Description |
|---|---|---|
| Cost of setting a key of length |
||
| Cost of step 1 of set. | ||
| Given in the paragraph below this table. | Cost of step 2 of set. | |
| Cost of step 3 of set. | ||
| Cost of step 4 of set. |
| Formula | Value | Description | Ethereum counterpart |
|---|---|---|---|
| 2500 | Cost of writing a single byte into an the backing storage of an MPT. |
|
|
| 50 | Cost of reading a single byte from the backing storage of an MPT. |
|
|
| 20 | Cost of traversing 1 byte (2 nibbles) down an MPT. | None | |
| 130 | Cost of traversing 1 byte up (2 nibbles) and recomputing the SHA256 hashes of 2 nodes in an MPT after it or one of its descendants is changed. | None | |
| 50% | Proportion of the cost of writing a tuple into an MPT that is refunded when that tuple is re-set or deleted. |
|
| Formula | Value | Description |
|---|---|---|
| Cost of computing the SHA256 hash over a message of length |
||
| Cost of computing the Keccak256 hash over a message of length |
||
| Cost of RIPEMD160 hash over a message of length |
||
| Cost of verifying whether an Ed25519 signature over a message of length |
These per-byte costs were decided using semi-standardized experiments:
The methodology we used to decide
- Select a popular Rust implementation of SHA256 (and Keccak256, and RIPEMD160).
- Write a simple Rust binary that uses the implementation to SHA256-hash a message.
- Compile the Rust binary on Ubuntu 20.04 LTS running on a modern platform (Intel Core Coffee Lake) with
--releaseoptimizations enabled. - Inspect the generated assembly to count how many CPU cycles it takes to hash the message (per byte), assuming no pipelining.
Implementation details of Ed25519 verification made the methodology we used to decide on the cost of hashes difficult to apply for deciding
The idea for this experiment is to decide on a rough ratio estimate of how much more efficient running a cryptographic operation is in native code than running it inside WASM. Once we have this ratio, we gas meter Ed25519 signature verification in WASM, then apply the ratio to estimate what it would have costed to verify a signature in native code. The experiment has two parts.
The first part:
- Compile the Rust binary we wrote previously for the SHA256 experiment into wasm32-unknown-unknown target with
--releaseoptimizations. - Execute the WASM module, gas metering opcodes according to our schedule.
We found that the execution consumed 80 gas, which we treated as equivalent to 80 CPU cycles. This led us to the rough estimation that running a cryptographic operation in native code is 5x faster (80/16) than running it in a contract.
The second part:
- Select a popular Rust implementation of Ed25519 verification.
- Write a trivial Rust binary that uses the implementation to verify signatures of varying lengths.
- Compile the Rust binary to a wasm32-unknown-unknown target with
--releaseoptimizations. - Execute the WASM module with varying message lengths, gas metering opcodes according to our schedule.
We found that the base cost of verifying an Ed25519 signature in a contract is 7,000,000. Applying the 5x faster estimation from before, this led us to setting a base cost of 1,400,000 in
| Opcodes | Gas Cost |
|---|---|
| I32Const | 0 |
| I64Const | 0 |
| Opcodes | Gas Cost |
|---|---|
| Drop | 2 |
| Select | 3 |
| Opcodes | Gas Cost |
|---|---|
| Nop, Unreachable, Else, Loop, If | 0 |
| Br, BrTable, Call, CallIndirect, Return | 2 |
| BrIf | 3 |
| Opcodes | Gas Cost |
|---|---|
| GlobalGet, GlobalSet, LocalGet, LocalSet | 3 |
| Opcodes | Gas Cost |
|---|---|
| RefIsNull, RefFunc, RefNull, ReturnCall, ReturnCallIndirect | 2 |
| Opcodes | Gas Cost |
|---|---|
| CatchAll, Throw, Rethrow, Delegate | 2 |
| Opcodes | Gas Cost |
|---|---|
| ElemDrop, DataDrop | 1 |
| TableInit | 2 |
| MemoryCopy, MemoryFill, TableCopy, TableFill | 3 |
| Opcodes | Gas Cost |
|---|---|
| I32Load, I64Load, I32Store, I64Store, I32Store8, I32Store16, I32Load8S, I32Load8U, I32Load16S, I32Load16U, I64Load8S, I64Load8U, I64Load16S, I64Load16U, I64Load32S, I64Load32U, I64Store8, I64Store16, I64Store32 | 3 |
| Opcodes | Gas Cost |
|---|---|
| I32Add, I32Sub, I64Add, I64Sub, I64LtS, I64LtU, I64GtS, I64GtU, I64LeS, I64LeU, I64GeS, I64GeU, I32Eqz, I32Eq, I32Ne, I32LtS, I32LtU, I32GtS, I32GtU, I32LeS, I32LeU, I32GeS, I32GeU, I64Eqz, I64Eq, I64Ne, I32And, I32Or, I32Xor, I64And, I64Or, I64Xor, | 1 |
| I32Shl, I32ShrU, I32ShrS, I32Rotl, I32Rotr, I64Shl, I64ShrU, I64ShrS, I64Rotl, I64Rotr, | 2 |
| I32Mul, I64Mul | 3 |
| I32DivS, I32DivU, I32RemS, I32RemU, I64DivS, I64DivU, I64RemS, I64RemU | 80 |
| I32Clz, I64Clz | 105 |
| Opcodes | Gas Cost |
|---|---|
| I32WrapI64, I32Extend8S, I32Extend16S, I64ExtendI32S, I64ExtendI32U, I64Extend8S, I64Extend16S, I64Extend32S | 3 |