This EIP introduces changes in the gas schedule to reflect the costs of creating a witness. It requires clients to update their database layout to match this, so as to avoid potential DoS attacks.
The introduction of Verkle trees into Ethereum requires fundamental changes and as a preparation, this EIP is targeting the fork coming right before the verkle tree fork, in order to incentivize Dapp developers to adopt the new storage model, and ample time to adjust to it. It also incentivizes client developers to migrate their database format ahead of the verkle fork.
def get_storage_slot_tree_keys(storage_key: int) -> [int, int]:
if storage_key < (CODE_OFFSET - HEADER_STORAGE_OFFSET):
pos = HEADER_STORAGE_OFFSET + storage_key
else:
pos = MAIN_STORAGE_OFFSET + storage_key
return (
pos // 256,
pos % 256
)
Whenever the state is read, one or more of the access events of the form(address, sub_key, leaf_key)
take place, determining what data is being accessed. We define access events as follows:
When:
*CALL
, CALLCODE
, SELFDESTRUCT
, EXTCODESIZE
, or EXTCODECOPY
opcode,BALANCE
opcodeCODECOPY
process this access event:
(address, 0, BASIC_DATA_LEAF_KEY)
Note: a non-value-bearing SELFDESTRUCT
, *CALL
or CALLCODE
, targeting a precompile or a system contract, will not cause the BASIC_DATA_LEAF_KEY
to be added to the witness.
If a *CALL
, CALLCODE
or SELFDESTRUCT
is value-bearing (ie. it transfers nonzero wei), whether or not the callee
is a precompile or a system contract, process this additional access event:
(caller, 0, BASIC_DATA_LEAF_KEY)
Note: when checking for the existence of the callee
, the existence check is done by validating that there is an extension-and-suffix tree at the corresponding stem, and does not rely on CODEHASH_LEAF_KEY
.
When calling EXTCODEHASH
, process the access event:
(address, 0, CODEHASH_LEAF_KEY)
Note that precompiles and system contracts are excluded, as their hashes are known to the client.
When a contract is created, process these access events:
(contract_address, 0, BASIC_DATA_LEAF_KEY)
(contract_address, 0, CODEHASH_LEAF_KEY)
SLOAD
and SSTORE
opcodes with a given address and key process an access event of the form
(address, tree_key, sub_key)
Where tree_key
and sub_key
are computed as tree_key, sub_key = get_storage_slot_tree_keys(address, key)
In the conditions below, “chunk chunk_id is accessed” is understood to mean an access event of the form
(address, (chunk_id + 128) // 256, (chunk_id + 128) % 256)
PC < len(code)
, chunk PC // CHUNK_SIZE
(where PC
is the current program counter) of the callee is accessed. In particular, note the following corner cases:JUMP
(or positively evaluated JUMPI
) is considered to be accessed, even if the destination is not a jumpdest or is inside pushdataJUMPI
is not considered to be accessed if the jump conditional is false
.JUMP
opcode (including chunk access cost if the JUMP
is the first opcode in a not-yet-accessed chunk)destination >= len(code)
)PC = len(code)
is not considered to be accessedPUSH{n}
, all chunks (PC // CHUNK_SIZE) <= chunk_index <= ((PC + n) // CHUNK_SIZE)
of the callee are accessed.CODECOPY
or EXTCODECOPY
read bytes x...y
inclusive, all chunks (x // CHUNK_SIZE) <= chunk_index <= (min(y, code_size - 1) // CHUNK_SIZE)
of the accessed contract are accessed.CODECOPY
with start position 100, read size 50, code_size = 200
, x = 100
and y = 149
CODECOPY
with start position 600, read size 0, no chunks are accessedCODECOPY
with start position 1500, read size 2000, code_size = 3100
, x = 1500
and y = 3099
CODESIZE
, EXTCODESIZE
and EXTCODEHASH
do NOT access any chunks.
When a contract is created, access chunks 0 ... (len(code)+30)//31
We define write events as follows. Note that when a write takes place, an access event also takes place (so the definition below should be a subset of the definition of access events). A write event is of the form (address, sub_key, leaf_key)
, determining what data is being written to.
When a nonzero-balance-sending CALL
, CALLCODE
or SELFDESTRUCT
with a given sender and recipient takes place, process these write events:
(caller, 0, BASIC_DATA_LEAF_KEY)
(callee, 0, BASIC_DATA_LEAF_KEY)
if no account exists at callee_address
, also process:
(callee, 0, CODEHASH_LEAF_KEY)
When a contract creation is initialized, process these write events:
(contract_address, 0, BASIC_DATA_LEAF_KEY)
(contract_address, 0, CODEHASH_LEAF_KEY)
SSTORE
opcodes with a given address
and key
process a write event of the form
(address, tree_key, sub_key)
Where tree_key
and sub_key
are computed as tree_key, sub_key = get_storage_slot_tree_keys(address, key)
When a contract is created, process the write events:
(
address,
(CODE_OFFSET + i) // VERKLE_NODE_WIDTH,
(CODE_OFFSET + i) % VERKLE_NODE_WIDTH
)
For i
in 0 ... (len(code)+30)//31
.
Note: since no access list existed for code up until this EIP, note that no warm costs are charged for code accesses.
For a transaction, make these access events:
(tx.origin, 0, BASIC_DATA_LEAF_KEY)
(tx.origin, 0, CODEHASH_LEAF_KEY)
(tx.target, 0, BASIC_DATA_LEAF_KEY)
(tx.target, 0, CODEHASH_LEAF_KEY)
(tx.origin, 0, BASIC_DATA_LEAF_KEY)
If value
is non-zero:
(tx.target, 0, BASIC_DATA_LEAF_KEY)
Remove the following gas costs:
CALL
if it is nonzero-value-sendingSSTORE
gas costs except for the SLOAD_GAS
CALLCODE
costs related to nonzero-value-sendingReduce gas cost:
CREATE
/CREATE2
to 1000Constant | Value |
---|---|
WITNESS_BRANCH_COST |
1900 |
WITNESS_CHUNK_COST |
200 |
SUBTREE_EDIT_COST |
3000 |
CHUNK_EDIT_COST |
500 |
CHUNK_FILL_COST |
6200 |
When executing a transaction, maintain four sets:
accessed_subtrees: Set[Tuple[address, int]]
accessed_leaves: Set[Tuple[address, int, int]]
edited_subtrees: Set[Tuple[address, int]]
edited_leaves: Set[Tuple[address, int, int]]
When an access event of (address, sub_key, leaf_key)
occurs, perform the following checks:
(address, sub_key)
is not in accessed_subtrees
, charge WITNESS_BRANCH_COST
gas and add that tuple to accessed_subtrees
.leaf_key
is not None
and (address, sub_key, leaf_key)
is not in accessed_leaves
, charge WITNESS_CHUNK_COST
gas and add it to accessed_leaves
When a write event of (address, sub_key, leaf_key)
occurs, perform the following checks:
(address, sub_key)
is not in edited_subtrees
, charge SUBTREE_EDIT_COST
gas and add that tuple to edited_subtrees
.leaf_key
is not None
and (address, sub_key, leaf_key)
is not in edited_leaves
, charge CHUNK_EDIT_COST
gas and add it to edited_leaves
(address, sub_key, leaf_key)
(ie. the state held None
at that position), charge CHUNK_FILL_COST
Note that tree keys can no longer be emptied: only the values 0...2**256-1
can be written to a tree key, and 0 is distinct from None
. Once a tree key is changed from None
to not-None
, it can never go back to None
.
Note that values should only be added to the witness if there is sufficient gas to cover their associated event costs. If there is not enough gas to cover the event costs, all the remaining gas should be consumed.
CREATE*
and *CALL
reserve 1/64th of the gas before the nested execution. In order to match the behavior of this charge with the pre-fork behavior of access lists:
CALL
, CALLCODE
, DELEGATECALL
orSTATICCALL
CREATE
or CREATE2
None of:
are warm at the start of a transaction.
When (and only when) calling a system contract either
the system contract's code chunks and account headers accesses should not appear in the witness as these should be known/cached in the clients. However any other accesses and all writes should appear in the witness.
Also corresponding witness costs need to be charged for precompile/opcode resolution but are not charged in the system call.
TODO : still waiting on a final decision between 7702 and 3074
Gas costs for reading storage and code are reformed to more closely reflect the gas costs under the new Verkle tree design. WITNESS_CHUNK_COST
is set to charge 6.25 gas per byte for chunks, and WITNESS_BRANCH_COST
is set to charge ~13,2 gas per byte for branches on average (assuming 144 byte branch length) and ~2.5 gas per byte in the worst case if an attacker fills the tree with keys deliberately computed to maximize proof length.
The main differences from gas costs in Berlin are:
key1 // 256 == key2 // 256
) decreases from 2100 to 200 for all slots after the first in the group,Gains from the latter two properties have not yet been analyzed, but are likely to significantly offset the losses from the first property. It’s likely that once compilers adapt to these rules, efficiency will increase further.
The precise specification of when access events take place, which makes up most of the complexity of the gas repricing, is necessary to clearly specify when data needs to be saved to the period 1 tree.
This EIP requires a hard fork, since it modifies consensus rules.
The main backwards-compatibility-breaking changes is the gas costs for code chunk access making some applications less economically viable. It can be mitigated by increasing the gas limit at the same time as implementing this EIP, reducing the risk that applications will no longer work at all due to transaction gas usage rising above the block gas limit.
This EIP will mean that certain operations, mostly reading and writing several elements in the same suffix tree, become cheaper. If clients retain the same database structure as they have now, this would result in a DOS vector.
So some adaptation of the database is required in order to make this work:
Copyright and related rights waived via CC0.