Skip to content

Commit 7aa0b29

Browse files
committed
use two ring buffers to avoid collision attacks
1 parent 20d7767 commit 7aa0b29

File tree

1 file changed

+34
-16
lines changed

1 file changed

+34
-16
lines changed

EIPS/eip-4788.md

+34-16
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ restaking constructions, smart contract bridges, MEV mitigations and more.
2929
|--- |--- |---
3030
| `FORK_TIMESTAMP` | TBD |
3131
| `HISTORY_STORAGE_ADDRESS` | `Bytes20(0xB)` |
32-
| `G_beacon_root` | 2100 | gas
32+
| `G_beacon_root` | 4200 | gas
3333
| `HISTORICAL_ROOTS_LENGTH` | 98304 |
3434

3535
### Background
@@ -53,47 +53,65 @@ Validity is guaranteed from the consensus layer, much like how withdrawals are h
5353
At the start of processing any execution block where `block.timestamp >= FORK_TIMESTAMP` (i.e. before processing any transactions),
5454
write the parent beacon root provided in the block header into the storage of the contract at `HISTORY_STORAGE_ADDRESS`.
5555

56-
The timestamp (a 64-bit unsigned integer value) of the header is used as a key into the contract's storage.
57-
To map the timestamp to the correct key, the timestamp as a number is reduced modulo `HISTORICAL_ROOTS_LENGTH` and
58-
this resulting 64-bit unsigned integer should be encoded as 32 bytes in big-endian format when writing to the storage.
56+
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
57+
the latest timestamp at a given index.
5958

60-
The 32 bytes of the `parent_beacon_block_root` (as provided) are the
61-
value to write in the contract's storage.
59+
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`.
60+
To derive the index `timestamp_index` into the timestamp ring buffer, add `HISTORICAL_ROOTS_LENGTH` to the index into the root ring buffer.
61+
Both resulting 64-bit unsigned integers should be encoded as 32 bytes in big-endian format when writing to the storage.
62+
63+
The 32 bytes of the `parent_beacon_block_root` (as provided) are the value to write behind the `root_index`.
64+
The timestamp from the header, encoded as 32 bytes in big-endian format, is the value to write behind the `timestamp_index`.
6265

6366
In Python pseudocode:
6467

6568
```python
6669
timestamp_reduced = block_header.timestamp % HISTORICAL_ROOTS_LENGTH
67-
key = to_uint256_be(timestamp_reduced)
70+
timestamp_extended = timestamp_reduced + HISTORICAL_ROOTS_LENGTH
71+
root_index = to_uint256_be(timestamp_reduced)
72+
timestamp_index = to_uint256_be(timestamp_extended)
6873

6974
parent_beacon_block_root = block_header.parent_beacon_block_root
75+
timestamp_as_uint256 = to_uint256_be(block_header.timestamp)
7076

71-
sstore(HISTORY_STORAGE_ADDRESS, key, parent_beacon_block_root)
77+
sstore(HISTORY_STORAGE_ADDRESS, root_index, parent_beacon_block_root)
78+
sstore(HISTORY_STORAGE_ADDRESS, timestamp_index, timestamp_as_uint256)
7279
```
7380

7481
#### New stateful precompile
7582

7683
Beginning at the execution timestamp `FORK_TIMESTAMP`, a "stateful" precompile is deployed at `HISTORY_STORAGE_ADDRESS`.
7784

7885
Callers of the precompile should provide the `timestamp` they are querying encoded as 32 bytes in big-endian format.
79-
This `timestamp` is reduced in the same way to point to a unique storage location into the ring buffer from any given block.
8086

81-
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
82-
the precompile's state.
87+
Given this input, the precompile reduces the `timestamp` in the same way during the write routines and first checks if
88+
the `timestamp` at the ring buffer index matches the one supplied by the caller.
89+
90+
If the `timestamp` **does NOT** match, the client **MUST** return the "zero" word -- the 32-byte value where each byte is `0x00`.
8391

84-
The parent beacon block root for the given timestamp is returned as 32 bytes in the caller's provided return buffer.
92+
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.
8593

8694
In pseudocode:
8795

8896
```python
8997
timestamp = evm.calldata[:32]
9098
timestamp_reduced = to_uint64_be(timestamp) % HISTORICAL_ROOTS_LENGTH
91-
key = to_uint32_be(timestamp_reduced)
92-
root = sload(HISTORY_STORAGE_ADDRESS, key)
93-
evm.returndata[:32].set(root)
99+
timestamp_extended = timetsamp_reduced + HISTORICAL_ROOTS_LENGTH
100+
timestamp_index = to_uint256_be(timestamp_extended)
101+
102+
recorded_timestamp = sload(HISTORY_STORAGE_ADDRESS, timestamp_index)
103+
if recorded_timestamp != timestamp:
104+
evm.returndata[:32].set(0x0000000000000000000000000000000000000000000000000000000000000000)
105+
else:
106+
root_index = to_uint256_be(timestamp_reduced)
107+
root = sload(HISTORY_STORAGE_ADDRESS, root_index)
108+
evm.returndata[:32].set(root)
94109
```
95110

96-
If there is no timestamp stored at the given root, the opcode follows the existing EVM semantics of `SLOAD` returning `0`.
111+
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
112+
the precompile's state.
113+
114+
If there is no root stored at the given timestamp, the opcode follows the existing EVM semantics of `SLOAD` returning `0`.
97115

98116
## Rationale
99117

0 commit comments

Comments
 (0)