EIP-4762

Overview

This EIP can initially be intimidating, but the underlying principle is simple. Every direct or indirect state access required in a block execution must be included in the execution witness. Not doing so means that if a stateless client attempts to re-execute this block, it will be unable to do so due to a lack of information.

This EIP’s goal is to define new gas cost rules to account for:

  • Chunkification of account properties (e.g., nonce, balance, code).
  • Disincentivize growing the block execution witness.
  • Avoid changing current costs as much as possible to limit UX impact.

A necessary clarification is that this EIP is currently designed for the Verkle Trees proposal.

Dimensions of gas cost changes

Let’s look at the different dimensions of gas cost changes.

EVM instructions new gas costs

The following instructions have a new gas cost:

  • CALL, CALLCODE, DELEGATECALL, STATICCALL, CALLCODE
  • SELFDESTRUCT
  • EXTCODESIZE, EXTCODECOPY, EXTCODEHASH
  • CODECOPY
  • BALANCE
  • SSLOAD, SSTORE
  • CREATE, CREATE2

These instructions share the common feature of accessing the state to perform their action; thus, their gas cost should reflect the new reality.

Let’s look at some examples:

  • BALANCE: when this instruction is executed, the BASIC_DATA leaf for the target address is accessed. This leaf should now be part of the witness.
  • SLOAD: might be the most obvious example, since the target storage slot leaf will be included in the witness.
  • EXTCODECOPY: recall that now the account code is part of the tree, so accessing the code of any account means state access, which is included in the witness.

Code execution

As explained in the Accounts code section of Trees chapter, account code is now part of the tree. This is required since stateless clients need the code to re-execute the block. Instead of providing all the account codes, we only need the code chunks effectively accessed during block execution (which is usually far less than the complete code).

The EVM interpreter must include any code chunk containing executed instructions to do this. Note that if two or more instructions from a given code chunk are executed, the code chunk is only included and charged once. The included code chunks aren’t always consecutive since instructions like JUMP or JUMPI can jump to a different code chunk.

Note that most opcodes only span a single byte of a code chunk, except instructions with immediates. For example, when executing a PUSH(N) instruction, we must account for 1+N bytes in the code since the immediate should also be included. This is important since a PUSH(N) corresponding bytecode could span multiple code chunks.

Out of gas and REVERT

It’s worth noting that if a transaction execution runs out of gas or executes a REVERT instruction, everything added to the witness isn’t removed. This should feel natural since when a stateless client re-executes the block, it still needs all the code chunks and state required to reach the reversion point.

Transaction preamble

In today’s rules, an intrinsic cost of 21_000 gas includes warming rules or particular accounts, such as the transaction origin. This is required since we have to validate that the transaction is valid by doing nonce and balance checks. All these implicit (or rather, included) accesses within the intrinsic cost are also included in the execution witness at today’s cost.

Contract creation

Creating a contract has some preliminary rules that must be enforced. For example, we must do a collision check on the new contract address since we can’t overwrite the existing contract code. This means the execution witness will include account field accesses since stateless clients require this data for validity checks.

Recall that the contract code is now stored in the tree. This means that the transaction that creates a contract now performs writes into the tree, which should be accounted for. The current gas rules indicate that 200 gas per contract byte should be charged — this rule has been removed and simplified to be considered a tree write. This new reality must be reflected in the gas costs.

Block-level operations

A block execution entails more actions than transaction execution. For example, staking withdrawals are operations done after the list of transactions is executed—these actions also perform read/write operations into the tree. These operations don’t have any gas cost, but they should still be accounted for as information to be included in the execution witness.

Precompiles & system contracts

Precompiles and system contracts have some exceptions to the rules described in this EIP.

Regarding precompiles, if we do a non-value bearing CALL to a precompile, we are not strictly required to include its BASIC_DATA, which contains the code size. Usually, the code size is needed so a stateless client can detect jumps beyond the contract size. But precompiles don’t have bytecode contracts, and we know their size is always 0.

We can look at an example of system contracts. By EIP-2935, a system contract stores the previous block’s hash. Although in the previous section, we mentioned that contract execution must include all code chunks, the bytecode for this system contract is defined at the spec level—stateless clients already have the contract’s bytecode, so we can avoid including it in the witness.

Access events

All the above rules are handled in a unified way via access events. Access events keep track of all state access (read/write) required during a block execution, which is needed for correctly charging the gas costs.

These are the relevant constants to understand how gas is charged:

  • WITNESS_BRANCH_COST when a new tree branch has a read access for the first time.
  • WITNESS_CHUNK_COST when a specific leaf has a read access for the first time. Recall that tree branches contain 256 leaves.
  • SUBTREE_EDIT_COST when a branch must be updated. This means that at least one write was done in this branch leaves.
  • CHUNK_EDIT_COST when an existing leaf is overwritten with a new value.
  • CHUNK_FILL_COST when a non-existing leaf is written for the first time.

Every time a state read/write is included as an access event, we must check if the operation should charge each of the costs mentioned above. These costs are charged once per location, per transaction execution. For example:

  • A second write to a storage slot only charges warm-cost since the branch and leaf inclusion were already charged on the first access.
  • If the storage slot 0 of a contract is accessed for the first time, only WITNESS_CHUNK_COST is charged. Recall that the first 64 storage slots live in the same branch as accounts fields. Some form of “call” that started this contract code execution already paid for WITNESS_BRANCH_COST, so we only need to charge WITNESS_CHUNK_COST.
  • When the first byte of a code-chunk is accessed, we charge WITNESS_BRANCH_COST+WITNESS_CHUNK_COST. Access costs for the subsequent instructions executed in the same code chunk aren’t charged. When bytecode execution overflows into the next chunk in the same branch, only WITNESS_CHUNK_COST is charged. And so on.