Skip to content

Commit 4c6e3cb

Browse files
committed
use two ring buffers to avoid collision attacks
1 parent c90aa70 commit 4c6e3cb

File tree

1 file changed

+42
-16
lines changed

1 file changed

+42
-16
lines changed

EIPS/eip-4788.md

+42-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,63 @@ 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 routine and first checks if
88+
the `timestamp` recorded in the ring buffer matches the one supplied by the caller.
8389

84-
The parent beacon block root for the given timestamp is returned as 32 bytes in the caller's provided return buffer.
90+
If the `timestamp` **does NOT** match, the client **MUST** return the "zero" word -- the 32-byte value where each byte is `0x00`.
91+
92+
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.
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 = timestamp_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` to reflect the two (2) implicit `SLOAD`s from
112+
the precompile's state.
97113

98114
## Rationale
99115

@@ -115,6 +131,16 @@ be nonfavorable conditions.
115131
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,
116132
e.g. with a singleton state root contract that caches the proof per slot).
117133

134+
### Why two ring buffers?
135+
136+
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.
137+
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,
138+
rather than the most recent one.
139+
140+
To nullify this attack, this EIP keeps track of the pair of data `(parent_beacon_block_root, timestamp)` for each index into the
141+
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
142+
to store a pair of values necessitates two ring buffers, rather than just one.
143+
118144
## Backwards Compatibility
119145

120146
No issues.

0 commit comments

Comments
 (0)