EIP-7709 BLOCKHASH storage reads and cost adjustment (Read BLOCKHASH from Storage and Update Cost)

Hegota Considered for Inclusion · EVM & execution · EIP status Draft · created 2024-05-18

The BLOCKHASH opcode is changed to read the hashes of the most recent 256 blocks from the storage of the EIP-2935 system contract, and charges an additional storage read cost accordingly. The returned value stays the same as today.

Why it is needed

Smart contracts fetch the hash of a recent block with the BLOCKHASH (block hash) opcode. Today this opcode assumes that the node (client) keeps recent block history on the side. Every other data read is described as reading from Ethereum state, but BLOCKHASH remains the one exception that relies on past chain data. Once EIP-2935 is introduced, recent block hashes are recorded in the storage of a system contract. BLOCKHASH lookups can then be handled in exactly the same way as ordinary storage reads. The cost also follows the resource actually used, charging a storage read cost. When building the witness, the bundle of data needed to validate a block, BLOCKHASH no longer needs separate handling either. This EIP specifies that transition while leaving the returned value unchanged, so existing contracts receive the same result.

What changes

Today

After EIP-7709

Who is affected

In depth

Constants and the two window sizes

The spec fixes four values. FORK_TIMESTAMP is TBD, HISTORY_STORAGE_ADDRESS is 0x0000F90827F1C53a10cb7A02335B175320002935, BLOCKHASH_SERVE_WINDOW is 256 and HISTORY_SERVE_WINDOW is 8191. There are two windows. BLOCKHASH_SERVE_WINDOW (256) is the range in which the BLOCKHASH opcode returns a hash, and HISTORY_SERVE_WINDOW (8191) is both the size of the EIP-2935 ring buffer and the divisor used to compute the slot number. In other words, the returned range is limited to 256 while the slot is obtained as arg % 8191. The source repeatedly stresses that the two values differ. Even if the 2935 contract stores more hashes, BLOCKHASH keeps returning 0 for anything beyond 256.

resolve_blockhash behavior

The activation point is fork_block. It is the block where fork_block.timestamp >= FORK_TIMESTAMP and fork_block.parent.timestamp < FORK_TIMESTAMP. From then on, BLOCKHASH gets the hash through resolve_blockhash(block, state, arg). First, if arg >= block.number or (arg + BLOCKHASH_SERVE_WINDOW) < block.number, it returns 0. If it is in range, it runs state.load_slot(HISTORY_STORAGE_ADDRESS, arg % HISTORY_SERVE_WINDOW). This load_slot is not a simple lookup but an SLOAD that includes gas charging, slot warming and state access recording. The semantics of the opcode do not change, so the same input returns the same hash.

Three implementations a client can choose from

When arg is in range, a client MAY choose one of three. First, do a direct SLOAD from state. Second, make a system call through the get path of the EIP-2935 contract (with a caller other than SYSTEM_ADDRESS). Third, if it holds the needed history like a full client, return it from memory or in the existing way. Whichever way it picks, it MUST apply all of the effects of SLOAD defined by the active fork. Specifically, these are the cold or warm SLOAD gas for the slot arg % HISTORY_SERVE_WINDOW, the follow-up effect of making the slot warm, and the state access record required by the active fork. It gives implementation freedom while unifying the gas and state effects that affect consensus into one.

Gas cost and exemption of system contract execution cost

Lookups within the range are charged the SLOAD cost and access for the slot arg % HISTORY_SERVE_WINDOW. According to the test cases, the BLOCKHASH opcode still charges its own base cost, and if a lookup is performed, the SLOAD cost is added on top. If the slot is cold, the cold SLOAD cost applies. If the same slot is read again in the same transaction, the warm SLOAD cost applies. On the other hand, even if the client executes the EIP-2935 contract as a system call to get the hash, it does not charge the gas for executing that contract's code. Only the SLOAD effects apply. However, if the 2935 contract is called directly without going through BLOCKHASH, it is treated as ordinary contract execution, and gas, state access records and the rest all apply as usual.

Design rationale

The source gives five reasons. First, the changed gas cost matches the resource actually accessed, a storage read. Second, charging the same cold and warm costs as a regular SLOAD avoids creating a separate gas rule just for recent block hashes. Third, recent hashes could instead be proven through a chain of parent headers, but then BLOCKHASH would remain a special case in witness construction. Fourth, charging only the warm cost always, or setting a BLOCKHASH-specific price, would reduce compatibility risk but would introduce a new special rule. Fifth, excluding the 2935 contract execution cost keeps gas low and keeps implementation simple for clients that resolve the read directly or from memory. BLOCKHASH returning only 256 hashes is likewise meant to preserve backward compatibility and not widen this exemption. Older hashes must be fetched by calling the 2935 contract directly and paying the ordinary execution cost.

Activation conditions and backward compatibility

This EIP assumes EIP-2935 is already active. One of two conditions must hold. Either 2935 was activated at least BLOCKHASH_SERVE_WINDOW (256 blocks) before this EIP, or 2935 and this EIP are enabled together at genesis, as on testnets and devnets. If the gap between the 2935 fork and this EIP's fork is less than 256 blocks, the 2935 contract will not have accumulated the required history, and the change becomes a breaking one. The meaning of the returned value does not change, but the cost of an in-range BLOCKHASH lookup rises substantially, so uses that depend on the old gas cost may break. On security, the source states that nothing new has been identified beyond what is covered in EIP-2935.

Test cases

These are the verification items the source lists. Calling with an argument outside the most recent 256 ancestors, or at or above the current block number, returns 0 with no additional storage access effects. If the argument is in range and the slot is cold, the BLOCKHASH base cost plus the cold SLOAD cost is charged. If a block number mapping to the same slot is looked up again in the same transaction, the later lookup costs the warm SLOAD cost. When a lookup is performed, each BLOCKHASH is charged both the instruction cost and the SLOAD cost. Calling the 2935 contract directly applies gas and state access records as ordinary contract execution under the current fork. If 2935 was properly activated at least 256 blocks earlier, BLOCKHASH must be interpreted consistently.

Current status and core developer call decisions

The source status is Draft. Constant values and implementation requirements may still change, and FORK_TIMESTAMP is also TBD. According to the call records, at 10.08 ACDE #247 EIP-7709 was moved to Considered for Inclusion (CFI) for the Hegotá fork. Nethermind raised its rating to A, and the benefit for BAL-based light clients was mentioned as the rationale. CFI is not a stage where fork inclusion is confirmed, so the decision may change in later calls.

Original EIP · ← EIPs