diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index 1cc70b1f04be90..dd3dd46b9019a2 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -29,7 +29,7 @@ restaking constructions, smart contract bridges, MEV mitigations and more. |--- |--- |--- | `FORK_TIMESTAMP` | TBD | | `HISTORY_STORAGE_ADDRESS` | `Bytes20(0xB)` | -| `G_beacon_root` | 2100 | gas +| `G_beacon_root` | 4200 | gas | `HISTORICAL_ROOTS_LENGTH` | 98304 | ### Background @@ -53,22 +53,29 @@ Validity is guaranteed from the consensus layer, much like how withdrawals are h At the start of processing any execution block where `block.timestamp >= FORK_TIMESTAMP` (i.e. before processing any transactions), write the parent beacon root provided in the block header into the storage of the contract at `HISTORY_STORAGE_ADDRESS`. -The timestamp (a 64-bit unsigned integer value) of the header is used as a key into the contract's storage. -To map the timestamp to the correct key, the timestamp as a number is reduced modulo `HISTORICAL_ROOTS_LENGTH` and -this resulting 64-bit unsigned integer should be encoded as 32 bytes in big-endian format when writing to the storage. +In order to bound the storage used by this precompile, two ring buffers are used: one to track the latest root at a given index and another to track +the latest timestamp at a given index. -The 32 bytes of the `parent_beacon_block_root` (as provided) are the -value to write in the contract's storage. +To derive the index `root_index` into the root ring buffer, the timestamp (a 64-bit unsigned integer value) is reduced modulo `HISTORICAL_ROOTS_LENGTH`. +To derive the index `timestamp_index` into the timestamp ring buffer, add `HISTORICAL_ROOTS_LENGTH` to the index into the root ring buffer. +Both resulting 64-bit unsigned integers should be encoded as 32 bytes in big-endian format when writing to the storage. + +The 32 bytes of the `parent_beacon_block_root` (as provided) are the value to write behind the `root_index`. +The timestamp from the header, encoded as 32 bytes in big-endian format, is the value to write behind the `timestamp_index`. In Python pseudocode: ```python timestamp_reduced = block_header.timestamp % HISTORICAL_ROOTS_LENGTH -key = to_uint256_be(timestamp_reduced) +timestamp_extended = timestamp_reduced + HISTORICAL_ROOTS_LENGTH +root_index = to_uint256_be(timestamp_reduced) +timestamp_index = to_uint256_be(timestamp_extended) parent_beacon_block_root = block_header.parent_beacon_block_root +timestamp_as_uint256 = to_uint256_be(block_header.timestamp) -sstore(HISTORY_STORAGE_ADDRESS, key, parent_beacon_block_root) +sstore(HISTORY_STORAGE_ADDRESS, root_index, parent_beacon_block_root) +sstore(HISTORY_STORAGE_ADDRESS, timestamp_index, timestamp_as_uint256) ``` #### New stateful precompile @@ -76,24 +83,35 @@ sstore(HISTORY_STORAGE_ADDRESS, key, parent_beacon_block_root) Beginning at the execution timestamp `FORK_TIMESTAMP`, a "stateful" precompile is deployed at `HISTORY_STORAGE_ADDRESS`. Callers of the precompile should provide the `timestamp` they are querying encoded as 32 bytes in big-endian format. -This `timestamp` is reduced in the same way to point to a unique storage location into the ring buffer from any given block. -Alongside the existing gas for calling the precompile, there is an additional gas cost of `G_beacon_root` cost to reflect the implicit `SLOAD` from -the precompile's state. +Given this input, the precompile reduces the `timestamp` in the same way during the write routines and first checks if +the `timestamp` at the ring buffer index matches the one supplied by the caller. + +If the `timestamp` **does NOT** match, the client **MUST** return the "zero" word -- the 32-byte value where each byte is `0x00`. -The parent beacon block root for the given timestamp is returned as 32 bytes in the caller's provided return buffer. +If the `timestamp` **does** match, the client **MUST** read the root ring buffer and return the 32-byte value there in the caller's return buffer. In pseudocode: ```python timestamp = evm.calldata[:32] timestamp_reduced = to_uint64_be(timestamp) % HISTORICAL_ROOTS_LENGTH -key = to_uint32_be(timestamp_reduced) -root = sload(HISTORY_STORAGE_ADDRESS, key) -evm.returndata[:32].set(root) +timestamp_extended = timetsamp_reduced + HISTORICAL_ROOTS_LENGTH +timestamp_index = to_uint256_be(timestamp_extended) + +recorded_timestamp = sload(HISTORY_STORAGE_ADDRESS, timestamp_index) +if recorded_timestamp != timestamp: + evm.returndata[:32].set(0x0000000000000000000000000000000000000000000000000000000000000000) +else: + root_index = to_uint256_be(timestamp_reduced) + root = sload(HISTORY_STORAGE_ADDRESS, root_index) + evm.returndata[:32].set(root) ``` -If there is no timestamp stored at the given root, the opcode follows the existing EVM semantics of `SLOAD` returning `0`. +Alongside the existing gas for calling the precompile, there is an additional gas cost of `G_beacon_root` cost to reflect the two (2) implicit `SLOAD`s from +the precompile's state. + +If there is no root stored at the given timestamp, the opcode follows the existing EVM semantics of `SLOAD` returning `0`. ## Rationale