Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update EIP-4788: favor stateful precompile over opcode #7065

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions EIPS/eip-4788.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ created: 2022-02-10

Commit to the hash tree root of each beacon chain block in the corresponding execution payload header.

Store each of these roots in a contract that lives in the execution state and add a new opcode that reads this contract.
Store each of these roots in a stateful precompile.

## Motivation

Expand All @@ -29,28 +29,23 @@ restaking constructions, smart contract bridges, MEV mitigations and more.
|--- |--- |---
| `FORK_TIMESTAMP` | TBD |
| `HISTORY_STORAGE_ADDRESS` | `0xfffffffffffffffffffffffffffffffffffffffd` |
| `OPCODE_VALUE` | `0x4A` |
| `G_beacon_root` | 20 | gas
| `G_beacon_root` | 2100 | gas
| `SLOTS_PER_HISTORICAL_ROOT` | 8192 | slot(s)

### Background

The high-level idea is that each execution block contains the parent beacon block root. Even in the event of missed slots since the previous block root does not change,
we only need a constant amount of space to represent this "oracle" in each execution block. To improve the usability of this oracle, block roots are stored
in a canonical place in the execution state analogous to a `SSTORE` in the given contract's storage for each update.
Roots are keyed by the slot(s) they pertain to.
we only need a constant amount of space to represent this "oracle" in each execution block. To improve the usability of this oracle, a small history of block roots
are stored in a stateful precompile.
To bound the amount of storage this construction consumes, a ring buffer is used that mirrors a block root accumulator on the consensus layer.
The method for exposing the root data via opcode is inspired by [EIP-2935](./eip-2935.md).

### Block structure and validity

Beginning at the execution timestamp `FORK_TIMESTAMP`, execution clients **MUST**:

1. set 32 bytes of the execution block header after the `excess_data_gas` to the 32 byte [hash tree root](https://github.com/ethereum/consensus-specs/blob/fa09d896484bbe240334fa21ffaa454bafe5842e/ssz/simple-serialize.md#merkleization) of the parent beacon block.

2. set the 8 bytes after the previous 32 byte extension to the slot number as a big-endian uint64 of the current slot.

*NOTE*: these fields are appended to the current block header structure with this EIP so that the size of the header grows after (and including) the `FORK_TIMESTAMP`.
*NOTE*: this field is appended to the current block header structure with this EIP so that the size of the header grows after (and including) the `FORK_TIMESTAMP`.

### EVM changes

Expand Down Expand Up @@ -90,26 +85,31 @@ sstore(HISTORY_STORAGE_ADDRESS, SLOTS_PER_HISTORICAL_ROOT, end_slot)

When using any slot value as a key to the storage, the value under consideration must be converted to 32 bytes with big-endian encoding.

#### New opcode
#### New stateful precompile

Beginning at the execution timestamp `FORK_TIMESTAMP`, the code and storage at `HISTORY_STORAGE_ADDRESS` constitute a "stateful" precompile.

Callers of the precompile should provide the `slot` they are querying encoding a 64-bit unsigned integer as 256-bit big-endian data.
Recall this `slot` number should be reduced modulo the `SLOTS_PER_HISTORICAL_ROOT` constant to derive the correct key into the ring buffer structure.

Beginning at the execution timestamp `FORK_TIMESTAMP`, introduce a new opcode `BEACON_ROOT` at `OPCODE_VALUE`.
This opcode consumes one word from the stack encoding the slot number for the desired root under big-endian discipline.
The opcode has a gas cost of `G_beacon_state_root`.
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 root is returned as 32 bytes in the caller's provided return buffer.

The result of executing this opcode leaves one word on the stack corresponding to a read of the history contract's storage; in pseudocode:
In pseudocode:

```python
slot = evm.stack.pop()
sload(HISTORY_STORAGE_ADDRESS, slot % SLOTS_PER_HISTORICAL_ROOT)
slot = to_uint64(evm.calldata[:32])
root = sload(HISTORY_STORAGE_ADDRESS, slot % SLOTS_PER_HISTORICAL_ROOT)
evm.returndata[:32].set(root)
```

If there is no root stored at the requested slot number, the opcode follows the existing EVM semantics of `sload` returning `0`.

## Rationale

### Gas cost of opcode
### Gas cost of precompile

The suggested gas cost is just using the value for the `BLOCKHASH` opcode as `BEACON_ROOT` is an analogous operation.
The suggested gas cost reflects a cold `SLOAD` analogous to the operation performed while executing the precompile's logic.

### Why not repurpose `BLOCKHASH`?

Expand Down