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

BEP-440: Implement EIP-2935: Serve historical block hashes from state #440

Merged
merged 2 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
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
250 changes: 250 additions & 0 deletions BEPs/BEP-440.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<pre>
BEP: 440
Title: Implement EIP-2935: Serve historical block hashes from state
Status: Review
Type: Standards
Created: 2024-09-24
Description: Store and serve last 8192 block hashes as storage slots of a system contract to allow for stateless execution
</pre>


# BEP-440: Implement EIP-2935: Serve historical block hashes from state

- [BEP-440: Implement EIP-2935: Serve historical block hashes from state](#bep-440-implement-eip-2935-serve-historical-block-hashes-from-state)
- [Abstract](#abstract)
- [Motivation](#motivation)
- [Specification](#specification)
- [Block processing](#block-processing)
- [EVM Changes](#evm-changes)
- [Block hash history contract](#block-hash-history-contract)
- [`get`](#get)
- [`set`](#set)
- [Bytecode](#bytecode)
- [Deployment](#deployment)
- [EIP-161 handling](#eip-161-handling)
- [Gas costs](#gas-costs)
- [Rationale](#rationale)
- [Inserting the parent block hash](#inserting-the-parent-block-hash)
- [Backwards Compatibility](#backwards-compatibility)
- [Test Cases](#test-cases)
- [Security Considerations](#security-considerations)
- [Copyright](#copyright)


## Abstract

Store last `HISTORY_SERVE_WINDOW` historical block hashes in the storage of a system contract as part of the block processing logic. Furthermore this EIP has no impact on `BLOCKHASH` resolution mechanism (and hence its range/costs etc).

## Motivation

EVM implicitly assumes the client has the recent block (hashes) at hand. This assumption is not future-proof given the prospect of stateless clients. Including the block hashes in the state will allow bundling these hashes in the witness provided to a stateless client. This is already possible in the MPT and will become more efficient post-Verkle.

Extending the range of blocks which `BLOCKHASH` can serve (`BLOCKHASH_SERVE_WINDOW`) would have been a semantics change. Using extending that via this contract storage would allow a soft-transition. Rollups can benefit from the longer history window through directly querying this contract.

A side benefit of this approach could be that it allows building/validating proofs related to last `HISTORY_SERVE_WINDOW` ancestors directly against the current state.

## Specification

| Parameter | Value |
| - | - |
| `FORK_TIMESTAMP` | TBD |
| `BLOCKHASH_SERVE_WINDOW` | `256` |
| `HISTORY_SERVE_WINDOW` | `8192` |
| `SYSTEM_ADDRESS` | `0xfffffffffffffffffffffffffffffffffffffffe` |
| `HISTORY_STORAGE_ADDRESS` | `0x0aae40965e6800cd9b1f4b05ff21581047e3f91e`|

This EIP specifies for storing last `HISTORY_SERVE_WINDOW` block hashes in a ring buffer storage of `HISTORY_SERVE_WINDOW` length. Note that `HISTORY_SERVE_WINDOW` > `BLOCKHASH_SERVE_WINDOW` (which remains unchanged).

### Block processing

At the start of processing any block where `block.timestamp >= FORK_TIMESTAMP` (ie. before processing any transactions), call to `HISTORY_STORAGE_ADDRESS` as `SYSTEM_ADDRESS` with the 32-byte input of `block.parent.hash`, a gas limit of `30_000_000`, and `0` value. This will trigger the `set()` routine of the history contract. This is a system operation following the same convention as [EIP-4788](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4788.md) and therefore:

* the call must execute to completion
* the call does not count against the block's gas limit
* the call does not follow the [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) burn semantics - no value should be transferred as part of the call
* if no code exists at `HISTORY_STORAGE_ADDRESS`, the call must fail silently

Note: Alternatively clients can choose to directly write to the storage of the contract but EVM calling the contract remains preferred. Refer to the rationale for more info.

Note that, it will take `HISTORY_SERVE_WINDOW` blocks after `FORK_TIMESTAMP` to completely fill up the ring buffer. The contract will only contain the parent hash of the fork block and no hashes prior to that.


### EVM Changes

The `BLOCKHASH` opcode semantics remains the same as before.

### Block hash history contract

The history contract has two operations: `get` and `set`. The `set` operation is invoked only when the `caller` is equal to the `SYSTEM_ADDRESS` as per [EIP-4788](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4788.md). Otherwise the `get` operation is performed.

#### `get`

It is used from the EVM for looking up block hashes.

* Callers provide the block number they are querying in a big-endian encoding.
* If calldata is bigger than 2^64-1, revert.
* For any output outside the range of [block.number-`HISTORY_SERVE_WINDOW`, block.number-1] return 0.

#### `set`

* Caller provides `block.parent.hash` as calldata to the contract.
* Set the storage value at `block.number-1 % HISTORY_SERVE_WINDOW` to be `calldata[0:32]`.

#### Bytecode

Exact evm assembly that can be used for the history contract:

```
// if system call then jump to the set operation
caller
push20 0xfffffffffffffffffffffffffffffffffffffffe
eq
push1 0x57
jumpi

// check if input > 8 byte value and revert if this isn't the case
// the check is performed by comparing the biggest 8 byte number with
// the call data, which is a right-padded 32 byte number.
push8 0xffffffffffffffff
push0
calldataload
gt
push1 0x53
jumpi

// check if input > blocknumber-1 then return 0
push1 0x1
number
sub
push0
calldataload
gt
push1 0x4b
jumpi

// check if blocknumber > input + 8192 then return 0, no overflow expected for input of < max 8 byte value
push0
calldataload
push2 0x2000
add
number
gt
push1 0x4b
jumpi

// mod 8192 and sload
push2 0x1fff
push0
calldataload
and
sload

// load into mem and return 32 bytes
push0
mstore
push1 0x20
push0
return

// 0x4b: return 0
jumpdest
push0
push0
mstore
push1 0x20
push0
return

// 0x53: revert
jumpdest
push0
push0
revert

// 0x57: set op - sstore the input to number-1 mod 8192
jumpdest
push0
calldataload
push2 0x1fff
push1 0x1
number
sub
and
sstore

stop
```

Corresponding bytecode:
`0x3373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35611fff60014303165500`

#### Deployment

The above bytecode will be deployed at the HISTORY_STORAGE_ADDRESS once the fork is enabled, in a manner similar to the deployment of system contracts. After deployment, the nonce value will be set to 1 immediately.


Some activation scenarios:

* For the fork to be activated at genesis, no history is written to the genesis state, and at the start of block `1`, genesis hash will be written as a normal operation to slot `0`.
* for activation at block `1`, only genesis hash will be written at slot `0`.
* for activation at block `32`, block `31`'s hash will be written to slot `31`. Every other slot will be `0`.

### [EIP-161](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-161.md) handling

The bytecode above will be deployed à la [EIP-4788](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4788.md). As such the account at `HISTORY_STORAGE_ADDRESS` will have code and a nonce of 1, and will be exempt from EIP-161 cleanup.

### Gas costs

The system update at the beginning of the block, i.e. `process_block_hash_history` (or via system call to the contract with `SYSTEM_ADDRESS` caller), will not warm the `HISTORY_STORAGE_ADDRESS` account or its storage slots as per [EIP-2929](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2929.md) rules. As such the first call to the contract will pay for warming up the account and storage slots it accesses. To clarify further any contract call to the `HISTORY_STORAGE_ADDRESS` will follow normal EVM execution semantics.

Since `BLOCKHASH` semantics doesn't change, this EIP has no impact on `BLOCKHASH` mechanism and costs.

## Rationale

Very similar ideas were proposed before. This EIP is a simplification, removing two sources of needless complexity:

1. Having a tree-like structure with multiple layers as opposed to a single list
2. Writing the EIP in EVM code
3. Serial unbounded storage of hashes for a deep access to the history

However after weighing pros and cons, we decided to go with just a limited ring buffer to only serve the requisite `HISTORY_SERVE_WINDOW` as [EIP-4788](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4788.md) and beacon state accumulators allow (albeit a bit more complex) proof against any ancestor since merge.

Second concern was how to best transition the BLOCKHASH resolution logic post fork by:

1. Either waiting for `HISTORY_SERVE_WINDOW` blocks for the entire relevant history to persist
2. Storing of all last `HISTORY_SERVE_WINDOW` block hashes on the fork block.

We choose to go with the former. It simplifies the logic greatly. It will take roughly a day to bootstrap the contract. Given that this is a new way of accessing history and no contract depends on it, it is deemed a favorable tradeoff.

### Inserting the parent block hash

Clients have generally two options for inserting the parent block hash into state:

1. Performing a system call to `HISTORY_STORAGE_ADDRESS` and letting that handle the storing in state.
2. Avoid EVM processing and directly write to the state trie.

The latter option is as follows:

```python
def process_block_hash_history(block: Block, state: State):
if block.timestamp >= FORK_TIMESTAMP:
state.insert_slot(HISTORY_STORAGE_ADDRESS, (block.number-1) % HISTORY_SERVE_WINDOW , block.parent.hash)
```

The first option is recommended until the Verkle fork, to stay consistent with [EIP-4788](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4788.md) and to issues for misconfigured networks where this EIP is activated but history contract hasn't been deployed. The recommendation may be reconsidered at the Verkle fork if filtering the system contract code chunks is deemed too complex.

## Backwards Compatibility

This EIP introduces backwards incompatible changes to the block validation rule set. But neither of these changes break anything related to current user activity and experience.

## Test Cases

TBD

## Security Considerations

Having contracts (system or otherwise) with hot update paths (branches) poses a risk of "branch" poisioning attacks where attacker could sprinkle trivial amounts of eth around these hot paths (branches). But it has been deemed that cost of attack would escalate significantly to cause any meaningful slow down of state root updates.

## Copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Here is the list of subjects of BEPs:
| [BEP-404](./BEPs/BEP-404.md) | Clear Miner History when Switching Validator Set | Standards | Candidate |
| [BEP-410](./BEPs/BEP-410.md) | Add Agent for Validators | Standards | Draft |
| [BEP-414](./BEPs/BEP-414.md) | EOA based Paymaster API Spec | Standards | Draft |
| [BEP-440](./BEPs/BEP-440.md) | Implement EIP-2935: Serve historical block hashes from state | Standards | Review |



Expand Down