EIP-4788 Beacon block roots in the EVM (Beacon block root in the EVM)

Final · EVM & execution · EIP status Final · created 2022-02-10

Each execution block header includes the hash tree root of the parent beacon block, and these roots are stored in a dedicated smart contract, so consensus layer information can be verified inside the EVM.

Why it is needed

Ethereum is split into the beacon chain, which handles consensus, and the execution layer, which executes transactions. However, smart contracts running in the EVM had no way to directly check what happens on the beacon chain. As a result, services such as staking pools and bridges had to depend on external oracles or operator reports for consensus layer information, which added parties that had to be trusted. A beacon block root is a cryptographic accumulator that can prove arbitrary consensus state. Putting this root inside the EVM lets contracts verify consensus layer data in a trust-minimized way. The original says this improves the trust assumptions of staking pools, restaking, smart contract bridges, MEV mitigation, and more.

What changes

Today

After EIP-4788

Who is affected

In depth

Header schema extension

From execution timestamp FORK_TIMESTAMP (1710338135), execution clients must add a parent_beacon_block_root field to the header. This value is 32 bytes and is the hash tree root, SSZ-merkleized, of the parent beacon block of that execution block. In RLP encoding it is the last field, after excess_blob_gas. As with withdrawal processing, the consensus layer guarantees the validity of the value, and when validating a block the execution client must check that the root in the header matches the value provided by the consensus client. In the genesis block, which has no parent beacon block root, a 32-byte zero value is used as a placeholder.

get and set of the beacon roots contract

The contract at BEACON_ROOTS_ADDRESS (0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02) performs only two operations, get and set. Which operation runs is decided by the caller, not by the input. If the caller is SYSTEM_ADDRESS (0xfffffffffffffffffffffffffffffffffffffffe) it is set, otherwise it is get. get takes the timestamp to look up as 32-byte big-endian, and reverts if the input is not exactly 32 bytes or is 0. If the timestamp stored in the timestamp % HISTORY_BUFFER_LENGTH slot differs from the input it reverts, and if they match it returns the beacon root in the timestamp % HISTORY_BUFFER_LENGTH + HISTORY_BUFFER_LENGTH slot. set writes header.timestamp to the header.timestamp % HISTORY_BUFFER_LENGTH slot, and writes the root from calldata[0:32] to the slot obtained by adding HISTORY_BUFFER_LENGTH to it.

System call during block processing

Every execution block with block.timestamp >= FORK_TIMESTAMP calls BEACON_ROOTS_ADDRESS as SYSTEM_ADDRESS before processing transactions. The input is the 32 bytes of header.parent_beacon_block_root, the gas limit is 30_000_000, and the value is 0. Because this is a system operation, the call must run to completion, is not counted toward the block gas limit, and does not follow the EIP-1559 burn rule, so no value is transferred. If BEACON_ROOTS_ADDRESS has no code, it must fail silently. A client may skip the explicit EVM call and write the storage values directly, which the original says is a valid optimization on mainnet but can be a problem in non-mainnet environments that use a different contract. When activated at the genesis block, the header's parent_beacon_block_root must be 0x0 and no system transaction occurs.

Deploy to a synthetic address

The contract is deployed like an ordinary contract, but it uses a synthetic address computed backward from a chosen deployment transaction. Computing the sender from the published deployment transaction gives 0x0B799C86a49DEeb90402691F1041aa3AF2d3C875, and the address of the first contract this account deploys, rlp([sender, 0]), is exactly BEACON_ROOTS_ADDRESS. The transaction input has a simple constructor placed before the runtime code. It is not a creation method tied to a specific initcode like create2, but the original explains that the synthetic address is cryptographically bound to the transaction input data.

Why there are two ring buffers

A single ring buffer holding only roots would allow an attack. If a timestamp arrives in a situation where slots were skipped and it has the same remainder when divided by the buffer length, the contract could return an old root instead of the latest one. To prevent this while keeping fixed storage space, each index stores a (parent_beacon_block_root, timestamp) pair, and on read it verifies that the timestamp matches the one recorded when the root was written. Storage slots are fixed at 32 bytes, so holding a pair of values requires two ring buffers rather than one.

Why the buffer size is 8191

The ring buffer is sized to hold 8191 consensus layer roots. With a prime number, no value is overwritten until the whole buffer is full, and after that each value is updated once per cycle. Even if the slot time changes, at most 8191 slots of storage are written. At current mainnet values, 8191 roots are about one day's worth. The original explains that this is enough time to build a transaction containing verification against a specific root and get it included on chain.

Why BLOCKHASH reuse and the state root were not chosen

The BLOCKHASH opcode could have been changed to return the beacon root instead of the execution block hash. To keep code changes small, avoid breaking existing contracts and simplify the mainnet rollout, the EIP leaves BLOCKHASH as it is and adds a new feature. It also uses the block root rather than the state root. Using the state root would require work that grows linearly each time a slot is skipped, while the block root needs only a constant amount of work per new block. Using the block root means a proof needs a few more nodes, but the source says this cost is negligible, and all consumers can share it, for example through a singleton state-root contract that caches per-slot proofs.

Original EIP · ← EIPs