eip | title | description | author | discussions-to | status | type | category | created |
---|---|---|---|---|---|---|---|---|
2935 |
Serve historical block hashes from state |
Store and serve last 8192 block hashes as storage slots of a system contract to allow for stateless execution |
Vitalik Buterin (@vbuterin), Tomasz Stanczak (@tkstanczak), Guillaume Ballet (@gballet), Gajinder Singh (@g11tech), Tanishq Jasoria (@tanishqjasoria), Ignacio Hagopian (@jsign), Jochem Brouwer (@jochem-brouwer) |
Draft |
Standards Track |
Core |
2020-09-03 |
Store last HISTORY_SERVE_WINDOW
historical block hashes in the storage of a system contract as part of the block processing logic. Furthermore this EIP has no impact on BLOCKHASH
resolution mechanism (and hence its range/costs etc).
EVM implicitly assumes the client has the recent block (hashes) at hand. This assumption is not future-proof given the prospect of stateless clients. Including the block hashes in the state will allow bundling these hashes in the witness provided to a stateless client. This is already possible in the MPT and will become more efficient post-Verkle.
Extending the range of blocks which BLOCKHASH
can serve (BLOCKHASH_SERVE_WINDOW
) would have been a semantics change. Using extending that via this contract storage would allow a soft-transition. Rollups can benefit from the longer history window through directly querying this contract.
A side benefit of this approach could be that it allows building/validating proofs related to last HISTORY_SERVE_WINDOW
ancestors directly against the current state.
Parameter | Value |
---|---|
FORK_TIMESTAMP |
TBD |
BLOCKHASH_SERVE_WINDOW |
256 |
HISTORY_SERVE_WINDOW |
8192 |
SYSTEM_ADDRESS |
0xfffffffffffffffffffffffffffffffffffffffe |
HISTORY_STORAGE_ADDRESS |
0x0aae40965e6800cd9b1f4b05ff21581047e3f91e |
This EIP specifies for storing last HISTORY_SERVE_WINDOW
block hashes in a ring buffer storage of HISTORY_SERVE_WINDOW
length. Note that HISTORY_SERVE_WINDOW
> BLOCKHASH_SERVE_WINDOW
(which remains unchanged).
At the start of processing any block where block.timestamp >= FORK_TIMESTAMP
(ie. before processing any transactions), update the state directly in the following way:
def process_block_hash_history(block: Block, state: State):
if block.timestamp >= FORK_TIMESTAMP:
state.insert_slot(HISTORY_STORAGE_ADDRESS, (block.number-1) % HISTORY_SERVE_WINDOW , block.parent.hash)
Alternatively clients can also choose to do a system update via a system call to the contract set
mechanism defined in the following sections.
Note that, it will take HISTORY_SERVE_WINDOW
blocks after FORK_TIMESTAMP
to completely fill up the ring buffer. The contract will only contain the parent hash of the fork block and no hashes prior to that.
As mentioned earlier the BLOCKHASH
opcode semantics remains the same as before. However the clients which want to leverage the history from state may do so making sure the query is within BLOCKHASH_SERVE_WINDOW
and is available in the contract.
Exact evm assembly that can be used for the history contract:
// if system call then jump to the set operation
caller
push20 0xfffffffffffffffffffffffffffffffffffffffe
eq
push1 0x57
jumpi
// check if input > 8 byte value and revert if this isn't the case
// the check is performed by comparing the biggest 8 byte number with
// the call data, which is a right-padded 32 byte number.
push8 0xffffffffffffffff
push0
calldataload
gt
push1 0x53
jumpi
// check if input > blocknumber-1 then return 0
push1 0x1
number
sub
push0
calldataload
gt
push1 0x4b
jumpi
// check if blocknumber > input + 8192 then return 0, no overflow expected for input of < max 8 byte value
push0
calldataload
push2 0x2000
add
number
gt
push1 0x4b
jumpi
// mod 8192 and sload
push2 0x1fff
push0
calldataload
and
sload
// load into mem and return 32 bytes
push0
mstore
push1 0x20
push0
return
// 0x4b: return 0
jumpdest
push0
push0
mstore
push1 0x20
push0
return
// 0x53: revert
jumpdest
push0
push0
revert
// 0x57: set op - sstore the input to number-1 mod 8192
jumpdest
push0
calldataload
push2 0x1fff
push1 0x1
number
sub
and
sstore
stop
Corresponding bytecode:
0x3373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35611fff60014303165500
Similar to EIP-4788 above contract provides a set
mechanism which clients can choose to update the block's parent hash in the ring buffer instead of direct state update. For this clients should call this contract with SYSTEM_ADDRESS
as caller and provide 32
bytes parent block hash as input.
If caller is not SYSTEM_ADDRESS
the contract treats it as get
with contract reading 32
bytes input via calldataload
as the arg
to resolve the block hash for. Users and clients doing get
EVM call should left pad the arg
correctly.
A special synthetic address is generated by working backwards from the desired deployment transaction:
{
"type": "0x0",
"nonce": "0x0",
"to": null,
"gas": "0x3d090",
"gasPrice": "0xe8d4a51000",
"maxPriorityFeePerGas": null,
"maxFeePerGas": null,
"value": "0x0",
"input": "0x60648060095f395ff33373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35611fff60014303165500",
"v": "0x1b",
"r": "0x539",
"s": "0x1b9b6eb1f0",
"hash": "0x3c769a03d6e2212f1d26ab59ba797dce0900df29ffd23c1dd391fd6b217973ad",
}
Note, the input in the transaction has a simple constructor prefixing the desired runtime code.
The sender of the transaction can be calculated as 0xe473f7e92ba2490e9fcbbe8bb9c3be3adbb74efc
. The address of the first contract deployed from the account is rlp([sender, 0])
which equals 0x0aae40965e6800cd9b1f4b05ff21581047e3f91e
. This is how HISTORY_STORAGE_ADDRESS
is determined. Although this style of contract creation is not tied to any specific initcode like create2 is, the synthetic address is cryptographically bound to the input data of the transaction (e.g. the initcode).
Some activation scenarios:
- For the fork to be activated at genesis, no history is written to the genesis state, and at the start of block
1
, genesis hash will be written as a normal operation to slot0
. - for activation at block
1
, only genesis hash will be written at slot0
. - for activation at block
32
, block31
's hash will be written to slot31
. Every other slot will be0
.
EIP-158 handling
The bytecode above will be deployed à la EIP-4788. As such the account at HISTORY_STORAGE_ADDRESS
will have code and a nonce of 1, and will be exempt from EIP-158 cleanup.
The system update at the beginning of the block, i.e. process_block_hash_history
(or via system call to the contract with SYSTEM_ADDRESS
caller), will not warm the HISTORY_STORAGE_ADDRESS
account or its storage slots as per EIP-2929 rules. As such the first call to the contract will pay for warming up the account and storage slots it accesses.To clarify further any contract call to the HISTORY_STORAGE_ADDRESS
will follow normal EVM execution semantics.
Since BLOCKHASH
semantics doesn't change, this EIP has no impact on BLOCKHASH
mechanism and costs.
Very similar ideas were proposed before in EIP-210 et al. This EIP is a simplification, removing two sources of needless complexity:
- Having a tree-like structure with multiple layers as opposed to a single list
- Writing the EIP in EVM code
- Serial unbounded storage of hashes for a deep access to the history
However after weighing pros and cons, we decided to go with just a limited ring buffer to only serve the requisite HISTORY_SERVE_WINDOW
as EIP-4788 and beacon state accumulators allow (albeit a bit more complex) proof against any ancestor since merge.
Second concern was how to best transition the BLOCKHASH resolution logic post fork by:
- Either waiting for
HISTORY_SERVE_WINDOW
blocks for the entire relevant history to persist - Storing of all last
HISTORY_SERVE_WINDOW
block hashes on the fork block.
We choose to go with the former. It simplifies the logic greatly. It will take roughly a day to bootstrap the contract. Given that this is a new way of accessing history and no contract depends on it, it is deemed a favorable tradeoff.
This EIP introduces backwards incompatible changes to the block validation rule set. But neither of these changes break anything related to current user activity and experience.
TBD
Having contracts (system or otherwise) with hot update paths (branches) poses a risk of "branch" poisioning attacks where attacker could sprinkle trivial amounts of eth around these hot paths (branches). But it has been deemed that cost of attack would escalate significantly to cause any meaningful slow down of state root updates.
Copyright and related rights waived via CC0.