From 2cafd5e026f4f6a4c9f1a09a4d1c36179686c304 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 13 Jun 2023 13:06:09 -0600 Subject: [PATCH 01/11] write roots into ring buffer --- EIPS/eip-4788.md | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index 72e787a6ca11d..1cc70b1f04be9 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -30,6 +30,7 @@ restaking constructions, smart contract bridges, MEV mitigations and more. | `FORK_TIMESTAMP` | TBD | | `HISTORY_STORAGE_ADDRESS` | `Bytes20(0xB)` | | `G_beacon_root` | 2100 | gas +| `HISTORICAL_ROOTS_LENGTH` | 98304 | ### Background @@ -52,37 +53,47 @@ 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 root itself is used as a key into the contract's storage and the timestamp of the header is written as the key's value. -The timestamp (a 64-bit unsigned integer value) is encoded as 32 bytes in big-endian format. +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. + +The 32 bytes of the `parent_beacon_block_root` (as provided) are the +value to write in the contract's storage. In Python pseudocode: ```python +timestamp_reduced = block_header.timestamp % HISTORICAL_ROOTS_LENGTH +key = to_uint256_be(timestamp_reduced) + parent_beacon_block_root = block_header.parent_beacon_block_root -timestamp = to_uint256_be(block_header.timestamp) -sstore(HISTORY_STORAGE_ADDRESS, parent_beacon_block_root, timestamp) +sstore(HISTORY_STORAGE_ADDRESS, key, parent_beacon_block_root) ``` #### New stateful precompile -Beginning at the execution timestamp `FORK_TIMESTAMP`, the code and storage at `HISTORY_STORAGE_ADDRESS` constitute a "stateful" precompile. +Beginning at the execution timestamp `FORK_TIMESTAMP`, a "stateful" precompile is deployed at `HISTORY_STORAGE_ADDRESS`. -Callers of the precompile should provide the `root` they are querying encoded as 32 bytes. +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. The timestamp of the corresponding root is returned as 32 bytes in the caller's provided return buffer and represents the -64-bit unsigned integer from the header in big-endian format. +the precompile's state. + +The parent beacon block root for the given timestamp is returned as 32 bytes in the caller's provided return buffer. In pseudocode: ```python -root = evm.calldata[:32] -timestamp = sload(HISTORY_STORAGE_ADDRESS, root) -evm.returndata[:32].set(timestamp) +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) ``` -If there is no timestamp stored at the given root, the opcode follows the existing EVM semantics of `sload` returning `0`. +If there is no timestamp stored at the given root, the opcode follows the existing EVM semantics of `SLOAD` returning `0`. ## Rationale From 22bd1e592b4fe90a0bb201f3bfbca530bf5a62ed Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 13 Jun 2023 13:42:03 -0600 Subject: [PATCH 02/11] use two ring buffers to avoid collision attacks --- EIPS/eip-4788.md | 58 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index 1cc70b1f04be9..edaf77de97f95 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,33 @@ 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 routine and first checks if +the `timestamp` recorded in the ring buffer matches the one supplied by the caller. -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 NOT** match, the client **MUST** return the "zero" word -- the 32-byte value where each byte is `0x00`. + +If the `timestamp` **does** match, the client **MUST** read the root from the contract storage and return those 32 bytes 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 = timestamp_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` to reflect the two (2) implicit `SLOAD`s from +the precompile's state. ## Rationale @@ -115,6 +131,16 @@ be nonfavorable conditions. Use of block root over state root does mean proofs will require a few additional nodes but this cost is negligible (and could be amortized across all consumers, e.g. with a singleton state root contract that caches the proof per slot). +### Why two ring buffers? + +The first ring buffer only tracks `HISTORICAL_ROOTS_LENGTH` worth of roots and so for all possible timestamp values would consume a constant amount of storage. +However, this design opens the precompile to an attack where a skipped slot that has the same value modulo the ring buffer length would return an old root value, +rather than the most recent one. + +To nullify this attack, this EIP keeps track of the pair of data `(parent_beacon_block_root, timestamp)` for each index into the +ring buffer and verifies the timestamp matches the one originally used to write the root data when being read. Given the fixed size of storage slots (only 32 bytes), the requirement +to store a pair of values necessitates two ring buffers, rather than just one. + ## Backwards Compatibility No issues. From 1726e468623263a3e4b6b596a5f4f47d0bdc8fef Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 16 Jun 2023 13:08:18 -0600 Subject: [PATCH 03/11] restore PR feedback --- EIPS/eip-4788.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index edaf77de97f95..a0792519a3a2a 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -137,7 +137,7 @@ The first ring buffer only tracks `HISTORICAL_ROOTS_LENGTH` worth of roots and s However, this design opens the precompile to an attack where a skipped slot that has the same value modulo the ring buffer length would return an old root value, rather than the most recent one. -To nullify this attack, this EIP keeps track of the pair of data `(parent_beacon_block_root, timestamp)` for each index into the +To nullify this attack while retaining a fixed memory footprint, this EIP keeps track of the pair of data `(parent_beacon_block_root, timestamp)` for each index into the ring buffer and verifies the timestamp matches the one originally used to write the root data when being read. Given the fixed size of storage slots (only 32 bytes), the requirement to store a pair of values necessitates two ring buffers, rather than just one. From dfcaf04994da46925a5768baeee92ce018a48a81 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 16 Jun 2023 13:15:10 -0600 Subject: [PATCH 04/11] clarify gas costs for precompile --- EIPS/eip-4788.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index a0792519a3a2a..3a883f3695aff 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -108,14 +108,13 @@ else: evm.returndata[:32].set(root) ``` -Alongside the existing gas for calling the precompile, there is an additional gas cost of `G_beacon_root` to reflect the two (2) implicit `SLOAD`s from -the precompile's state. +The precompile costs `G_beacon_root` gas to reflect the two (2) implicit `SLOAD`s from the precompile's state. ## Rationale ### Gas cost of precompile -The suggested gas cost reflects a cold `SLOAD` analogous to the operation performed while executing the precompile's logic. +The suggested gas cost reflects a cold `SLOAD` analogous to the operation(s) performed while executing the precompile's logic. ### Why not repurpose `BLOCKHASH`? From 18853a8d70404fb82452f606ea239e4887991078 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 16 Jun 2023 13:15:30 -0600 Subject: [PATCH 05/11] use more compact constant for zero word --- EIPS/eip-4788.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index 3a883f3695aff..1e1beeacecbd8 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -101,7 +101,7 @@ timestamp_index = to_uint256_be(timestamp_extended) recorded_timestamp = sload(HISTORY_STORAGE_ADDRESS, timestamp_index) if recorded_timestamp != timestamp: - evm.returndata[:32].set(0x0000000000000000000000000000000000000000000000000000000000000000) + evm.returndata[:32].set(uint256(0)) else: root_index = to_uint256_be(timestamp_reduced) root = sload(HISTORY_STORAGE_ADDRESS, root_index) From 037b4a2dc2e14663b27a6945848247900c738a1c Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 16 Jun 2023 13:21:04 -0600 Subject: [PATCH 06/11] swap relative positions in storage of root and timestamp buffers --- EIPS/eip-4788.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index 1e1beeacecbd8..cfe1520ceaf62 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -53,29 +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`. -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. +In order to bound the storage used by this precompile, two ring buffers are used: one to track the latest timestamp at a given index and another to track +the latest root at a given index. -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. +To derive the index `timestamp_index` into the timestamp ring buffer, the timestamp (a 64-bit unsigned integer value) is reduced modulo `HISTORICAL_ROOTS_LENGTH`. +To derive the index `root_index` into the root ring buffer, add `HISTORICAL_ROOTS_LENGTH` to the index into the timestamp 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`. +The 32 bytes of the `parent_beacon_block_root` (as provided) are the value to write behind the `root_index`. In Python pseudocode: ```python timestamp_reduced = block_header.timestamp % HISTORICAL_ROOTS_LENGTH timestamp_extended = timestamp_reduced + HISTORICAL_ROOTS_LENGTH -root_index = to_uint256_be(timestamp_reduced) -timestamp_index = to_uint256_be(timestamp_extended) +timestamp_index = to_uint256_be(timestamp_reduced) +root_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) +parent_beacon_block_root = block_header.parent_beacon_block_root -sstore(HISTORY_STORAGE_ADDRESS, root_index, parent_beacon_block_root) sstore(HISTORY_STORAGE_ADDRESS, timestamp_index, timestamp_as_uint256) +sstore(HISTORY_STORAGE_ADDRESS, root_index, parent_beacon_block_root) ``` #### New stateful precompile @@ -96,14 +96,14 @@ In pseudocode: ```python timestamp = evm.calldata[:32] timestamp_reduced = to_uint64_be(timestamp) % HISTORICAL_ROOTS_LENGTH -timestamp_extended = timestamp_reduced + HISTORICAL_ROOTS_LENGTH -timestamp_index = to_uint256_be(timestamp_extended) +timestamp_index = to_uint256_be(timestamp_reduced) recorded_timestamp = sload(HISTORY_STORAGE_ADDRESS, timestamp_index) if recorded_timestamp != timestamp: evm.returndata[:32].set(uint256(0)) else: - root_index = to_uint256_be(timestamp_reduced) + timestamp_extended = timestamp_reduced + HISTORICAL_ROOTS_LENGTH + root_index = to_uint256_be(timestamp_extended) root = sload(HISTORY_STORAGE_ADDRESS, root_index) evm.returndata[:32].set(root) ``` From 3a95b30415163282ab1dbbc56d00331102d0d75e Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 16 Jun 2023 18:36:45 -0600 Subject: [PATCH 07/11] Update EIPS/eip-4788.md --- EIPS/eip-4788.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index cfe1520ceaf62..29de39d02be70 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -53,7 +53,7 @@ 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`. -In order to bound the storage used by this precompile, two ring buffers are used: one to track the latest timestamp at a given index and another to track +In order to bound the storage used by this precompile, two ring buffers are used: one to track the latest timestamp at a given index in the ring buffer and another to track the latest root at a given index. To derive the index `timestamp_index` into the timestamp ring buffer, the timestamp (a 64-bit unsigned integer value) is reduced modulo `HISTORICAL_ROOTS_LENGTH`. From 5ef6bcde5f7a6b7872bcf17cb8ee503bdde8066f Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 22 Jun 2023 13:49:58 -0600 Subject: [PATCH 08/11] clarify block validity --- EIPS/eip-4788.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index 29de39d02be70..153b78c1ffa4f 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -44,7 +44,9 @@ To bound the amount of storage this construction consumes, a ring buffer is used Beginning at the execution timestamp `FORK_TIMESTAMP`, execution clients **MUST** extend the header schema with an additional field: the `parent_beacon_block_root`. This root consumes 32 bytes and is exactly the [hash tree root](https://github.com/ethereum/consensus-specs/blob/fa09d896484bbe240334fa21ffaa454bafe5842e/ssz/simple-serialize.md#merkleization) of the parent beacon block for the given execution block. -Validity is guaranteed from the consensus layer, much like how withdrawals are handled. +Validity of the parent beacon block root is guaranteed from the consensus layer, much like how withdrawals are handled. + +When verifying a block, execution clients should ensure the root value in the block header matches the one provided by the consensus client. ### EVM changes From 613c66528685fd02067bc704affcf8591bb1496b Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 22 Jun 2023 16:43:44 -0600 Subject: [PATCH 09/11] sanitize precompile input --- EIPS/eip-4788.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index 153b78c1ffa4f..dafb0859018d2 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -85,6 +85,9 @@ sstore(HISTORY_STORAGE_ADDRESS, root_index, 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. +Clients **MUST** sanitize this input call data to the precompile. +If the input is _more_ than 32 bytes, the precompile only takes the first 32 bytes of the input buffer and ignores the rest. +If the input is _less_ than 32 bytes, the precompile should revert. Given this input, the precompile reduces the `timestamp` in the same way during the write routine and first checks if the `timestamp` recorded in the ring buffer matches the one supplied by the caller. @@ -97,6 +100,10 @@ In pseudocode: ```python timestamp = evm.calldata[:32] +if len(timestamp) != 32: + evm.revert() + return + timestamp_reduced = to_uint64_be(timestamp) % HISTORICAL_ROOTS_LENGTH timestamp_index = to_uint256_be(timestamp_reduced) From 85015e024b422c37ceb23f87db3938e513f18e25 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 23 Jun 2023 11:07:10 -0600 Subject: [PATCH 10/11] Update EIPS/eip-4788.md --- EIPS/eip-4788.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index dafb0859018d2..b9c298e3c9912 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -46,7 +46,7 @@ This root consumes 32 bytes and is exactly the [hash tree root](https://github.c Validity of the parent beacon block root is guaranteed from the consensus layer, much like how withdrawals are handled. -When verifying a block, execution clients should ensure the root value in the block header matches the one provided by the consensus client. +When verifying a block, execution clients **MUST** ensure the root value in the block header matches the one provided by the consensus client. ### EVM changes From 2fb0316359192e4af79a8c05ad124a0908f14180 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Wed, 28 Jun 2023 14:25:59 -0600 Subject: [PATCH 11/11] update name of ring buffer size and add rationale for sizes --- EIPS/eip-4788.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-4788.md b/EIPS/eip-4788.md index b9c298e3c9912..f66eba05ad494 100644 --- a/EIPS/eip-4788.md +++ b/EIPS/eip-4788.md @@ -30,7 +30,7 @@ restaking constructions, smart contract bridges, MEV mitigations and more. | `FORK_TIMESTAMP` | TBD | | `HISTORY_STORAGE_ADDRESS` | `Bytes20(0xB)` | | `G_beacon_root` | 4200 | gas -| `HISTORICAL_ROOTS_LENGTH` | 98304 | +| `HISTORICAL_ROOTS_MODULUS` | 98304 | ### Background @@ -58,8 +58,8 @@ write the parent beacon root provided in the block header into the storage of th In order to bound the storage used by this precompile, two ring buffers are used: one to track the latest timestamp at a given index in the ring buffer and another to track the latest root at a given index. -To derive the index `timestamp_index` into the timestamp ring buffer, the timestamp (a 64-bit unsigned integer value) is reduced modulo `HISTORICAL_ROOTS_LENGTH`. -To derive the index `root_index` into the root ring buffer, add `HISTORICAL_ROOTS_LENGTH` to the index into the timestamp ring buffer. +To derive the index `timestamp_index` into the timestamp ring buffer, the timestamp (a 64-bit unsigned integer value) is reduced modulo `HISTORICAL_ROOTS_MODULUS`. +To derive the index `root_index` into the root ring buffer, add `HISTORICAL_ROOTS_MODULUS` to the index into the timestamp ring buffer. Both resulting 64-bit unsigned integers should be encoded as 32 bytes in big-endian format when writing to the storage. The timestamp from the header, encoded as 32 bytes in big-endian format, is the value to write behind the `timestamp_index`. @@ -68,8 +68,8 @@ The 32 bytes of the `parent_beacon_block_root` (as provided) are the value to wr In Python pseudocode: ```python -timestamp_reduced = block_header.timestamp % HISTORICAL_ROOTS_LENGTH -timestamp_extended = timestamp_reduced + HISTORICAL_ROOTS_LENGTH +timestamp_reduced = block_header.timestamp % HISTORICAL_ROOTS_MODULUS +timestamp_extended = timestamp_reduced + HISTORICAL_ROOTS_MODULUS timestamp_index = to_uint256_be(timestamp_reduced) root_index = to_uint256_be(timestamp_extended) @@ -104,14 +104,14 @@ if len(timestamp) != 32: evm.revert() return -timestamp_reduced = to_uint64_be(timestamp) % HISTORICAL_ROOTS_LENGTH +timestamp_reduced = to_uint64_be(timestamp) % HISTORICAL_ROOTS_MODULUS timestamp_index = to_uint256_be(timestamp_reduced) recorded_timestamp = sload(HISTORY_STORAGE_ADDRESS, timestamp_index) if recorded_timestamp != timestamp: evm.returndata[:32].set(uint256(0)) else: - timestamp_extended = timestamp_reduced + HISTORICAL_ROOTS_LENGTH + timestamp_extended = timestamp_reduced + HISTORICAL_ROOTS_MODULUS root_index = to_uint256_be(timestamp_extended) root = sload(HISTORY_STORAGE_ADDRESS, root_index) evm.returndata[:32].set(root) @@ -141,7 +141,7 @@ e.g. with a singleton state root contract that caches the proof per slot). ### Why two ring buffers? -The first ring buffer only tracks `HISTORICAL_ROOTS_LENGTH` worth of roots and so for all possible timestamp values would consume a constant amount of storage. +The first ring buffer only tracks `HISTORICAL_ROOTS_MODULUS` worth of roots and so for all possible timestamp values would consume a constant amount of storage. However, this design opens the precompile to an attack where a skipped slot that has the same value modulo the ring buffer length would return an old root value, rather than the most recent one. @@ -149,6 +149,12 @@ To nullify this attack while retaining a fixed memory footprint, this EIP keeps ring buffer and verifies the timestamp matches the one originally used to write the root data when being read. Given the fixed size of storage slots (only 32 bytes), the requirement to store a pair of values necessitates two ring buffers, rather than just one. +### Size of ring buffers + +The ring buffer data structures are sized to hold 8192 roots from the consensus layer at current slot timings (`SECONDS_PER_SLOT` is 12 seconds on mainnet, and `8192 * 12 == 98304`). +At mainnet values, 8192 roots provides about a day of coverage of the chain which gives users plenty of time to make a transaction with a verification against a given root in +the chain and get the transaction included. + ## Backwards Compatibility No issues.