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-2935: update for Prague devnet1 #8577

Merged
merged 4 commits into from
May 17, 2024
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
56 changes: 12 additions & 44 deletions EIPS/eip-2935.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@

## Abstract

Store last `HISTORY_SERVE_WINDOW` historical block hashes in a contract, and modify the `BLOCKHASH (0x40)` opcode to read and serve from this contract storage.
Store last `HISTORY_SERVE_WINDOW` historical block hashes in the storage of a system contract as part of the block processing logic.

## Motivation

Currently `BLOCKHASH` opcode accesses history to resolve hash of the block number in EVM. However a more stateless client friendly way is to maintain and serve these hashes from state.
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.

Although this is possible even in Merkle trie state, but Verkle trie state further allows bundling the `BLOCKHASH` witnesses (along with other witnesses) in an efficient manner making it worthwhile to have these in state.
Extending the range of blocks BLOCKHASH can serve is a semantics change. Using the contract storage would allow extending that range in 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.

Expand All @@ -33,38 +33,17 @@

This EIP specifies for storing last `HISTORY_SERVE_WINDOW` block hashes in a ring buffer storage of `HISTORY_SERVE_WINDOW` length.


At the start of processing any block where `block.timestamp >= FORK_TIMESTAMP` (ie. before processing any transactions), update the history in the following way:

```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)

# If this is the fork block, add the parent's direct `HISTORY_SERVE_WINDOW - 1` ancestors as well
if block.parent.timestamp < FORK_TIMESTAMP:
ancestor = block.parent
for i in range(HISTORY_SERVE_WINDOW - 1):
# stop at genesis block
if ancestor.number == 0:
break

ancestor = ancestor.parent
state.insert_slot(HISTORY_STORAGE_ADDRESS, ancestor.number % HISTORY_SERVE_WINDOW, ancestor.hash)
```

Note that if this is the fork block, then it persists the additional requisite history that could be needed while resolving `BLOCKHASH` opcode for all of the `HISTORY_SERVE_WINDOW` > `BLOCKHASH_OLD_WINDOW` ancestors (up until genesis).
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.

For resolving the `BLOCKHASH` opcode this fork onwards (`block.timestamp >= FORK_TIMESTAMP`), switch the logic to:

```python
def resolve_blockhash(block: Block, state: State, arg: uint64):
# check the wrap around range
if arg >= block.number or (arg + HISTORY_SERVE_WINDOW) < block.number
return 0

return state.load_slot(HISTORY_STORAGE_ADDRESS, arg % HISTORY_SERVE_WINDOW)
```
The `BLOCKHASH` opcode semantics remains the same as before.

### Contract Implementation

Expand Down Expand Up @@ -135,7 +114,7 @@

Note that the input contract read `32` bytes input as `calldataload`. Users and clients doing EVM call to this contract should left pad the `arg` correctly.

<!-- TODO: bytecode is based off on first version and will be updated once assembley is locked down as it changes contract sender and address -->

Check warning on line 117 in EIPS/eip-2935.md

View workflow job for this annotation

GitHub Actions / EIP Walidator

HTML comments are only allowed while `status` is one of: `Draft`, `Withdrawn`

warning[markdown-html-comments]: HTML comments are only allowed while `status` is one of: `Draft`, `Withdrawn` --> EIPS/eip-2935.md | 117 | <!-- TODO: bytecode is based off on first version and will be updated once assembley is locked down as it changes contract sender and address --> | = help: see https://ethereum.github.io/eipw/markdown-html-comments/

Corresponding bytecode:
`60203611603157600143035f35116029575f356120000143116029576120005f3506545f5260205ff35b5f5f5260205ff35b5f5ffd00`
Expand Down Expand Up @@ -170,21 +149,16 @@
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` as there is no additional history that needs to be persisted.
* for activation at block `32`, block `31`'s hash will be written to slot `31` and additonal history for `0..30`'s hashes will be persisted, so all in all `0..31`'s hashes.
* for activation at block `10000`, block `1808-9999`'s hashes will be persisted in the slot and `BLOCKHASH` for `1807` or less would resolve to `0` as only `HISTORY_SERVE_WINDOW` are persisted.
* 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-158](./eip-158.md) handling

This address is currently exempt from [EIP-158](./eip-158.md) cleanup in Kaustinen Verkle Testnet but we plan to address this in the following way:

* Deploy a contract à la [EIP-4788](./eip-4788.md) which just supports `get` method to resolve the BLOCKHASH as per the logic defined in `resolve_blockhash` (and use the generated address as the BLOCKHASH contract address).
* While the clients are expected to directly read from state (or maintain and serve from memory) to resolve BLOCKHASH opcode, this contract's `get` could be invoked by transaction (via another contract or directly) leading to a normal contract execution (and gas consumption) as per the semantics of the contract call.

The bytecode above will be deployed à la [EIP-4788](./eip-4788.md). It just supports a `get` method to resolve block hashes. As such the account at `HISTORY_STORAGE_ADDRESS` will have code and a nonce of 1, and will be exempt from EIP-158 cleanup.

### Gas costs and witnesses
### Gas costs

Since now `BLOCKHASH` is served from state, the clients now **additionally** charge the corresponding warm or cold `SLOAD` costs. For verkle based networks this would imply doing and bundling corresponding accesses (and gas charges) of `SLOAD`.
The gas cost of the `BLOCKHASH` opcode is unchanged. Importantly the processing at the beginning of the block, i.e. `process_block_hash_history`, will not warm the `HISTORY_STORAGE_ADDRESS` account or its storage slots as per [EIP-2929](./eip-2929.md) rules. As such the first call to the contract will pay for warming up the account and storage slots it accesses.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the first call to the contract is in tx context, not block context.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is your concern talking about the first call in this section at all? Or rather, how would you clarify this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"As such the first call to the contract will pay for warming up the account and storage slots it accesses."

It is not super clear that we are talking about the "first call in a tx to the contract" and not "first call in a block to the contract" (it could be misinterpreted). However, anyone with knowledge of EIP2929 will know that this is inside the tx context and not the block context.

So as nit, I would clarify it with "As such the first call within a transaction to the contract"...


## Rationale

Expand All @@ -201,22 +175,16 @@
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 later as it alleviates the need to detect fork activation height to transition to the new logic in backward compatible manner as the entire `BLOCKHASH` requisite history will be available from the first block of the fork itself.
The cost of doing so is marginal considering the `HISTORY_SERVE_WINDOW` being relatively limited. Most clients write this into their flat db/memory caches and just requires reading last `HISTORY_SERVE_WINDOW` from the chain history.
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.

## Backwards Compatibility

The behavior of `BLOCKHASH` opcode gets extended in backward compatible manner as the history it can serve will get extended upto `HISTORY_SERVE_WINDOW` on the fork block. However the gas charges will also get bumped as per the additional `SLOAD` costs.
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

## Reference Implementation

* PR 28878 of go-ethereum
* Active on verkle-gen-devnet-5 for its verkle implementation

## 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.
Expand Down
Loading