Skip to content

[#4887] Let the host serve BLOCKHASH from the EIP-2935 history contract - #89

Closed
isaacdoidge wants to merge 3 commits into
feature/evm_automationfrom
task/issue-4887
Closed

isaacdoidge wants to merge 3 commits into
feature/evm_automationfrom
task/issue-4887

Conversation

@isaacdoidge

@isaacdoidge isaacdoidge commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • cfg::BlockHashSource selects where Host::block_hash reads from. Database is the default and calls Database::block_hash, as upstream does. HistoryContract { floor } reads slot n % HISTORY_SERVE_WINDOW of the EIP-2935 history contract at HISTORY_STORAGE_ADDRESS. Cfg gains block_hash_source(), and CfgEnv gains the field plus set_block_hash_source / with_block_hash_source.
  • With HistoryContract { floor }:
    • a non-zero slot is the hash;
    • a zero slot below floor returns zero, because no EVM block recorded that height;
    • a zero slot at or above floor is a height the contract should hold but does not, and is referred to Database::block_hash, whose result or error is the answer. A database error is stored as ContextError::Db and surfaces as EVMError::Database. The node's database serves no block hashes, so on SupraEVM this fails the block, as every database error does.
  • JournalTr::storage_value_unwarmed returns a slot's present value without loading or warming the account or slot and without a journal entry. It reads the journal's state first, then treats an account created in the journal as having empty storage (as sload does), then falls back to the database. The journal is read first because the current block's history write stays in the journal until the block's state is finalized.
  • The blockhash opcode, its 256-block range and its 20 gas are unchanged. The read does not warm the slot, so later accesses to the history contract are priced as before. This keeps Ethereum mainnet's BLOCKHASH cost rather than EIP-7709's (Draft) extra storage charge.

Motivation

smr-moonshot#4887: serve BLOCKHASH from the history ring that smr-moonshot#4885 writes before every block, so that the opcode no longer depends on the node's certified-block table and its pruning floor. The node selects HistoryContract with the height of the block that applied the EVM genesis as floor. The node-side change is smr-moonshot#5001, which pins this branch; its pin moves to the tag this PR is released in.

Notes for review

  • JournalTr::storage_value_unwarmed is a required method. It is implemented for Journal and for the Backend in examples/cheatcode_inspector.
  • history_storage.rs helpers are now pub(crate) so the new test module can reuse them.

Test plan

  • cargo fmt --all --check
  • RUSTFLAGS=-Dwarnings cargo clippy --workspace --all-targets --all-features
  • cargo check --target riscv32imac-unknown-none-elf --no-default-features (and -p op-revm)
  • cargo check --no-default-features -p revm --features=serde
  • RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo doc --workspace --all-features --no-deps --document-private-items
  • cargo nextest run --workspace: all-features 569 passed; default features 567; --no-default-features 567
  • cargo test --workspace --all-features --doc
  • New ee-tests (history_block_hash):
    • a hash written by the System write earlier in the same journal is served while the database holds a different value for that slot;
    • a database-only hash is served;
    • after BLOCKHASH, the history contract's account and slot are still cold (the first get costs 4500 gas more than the second);
    • zero below the floor succeeds;
    • zero at or above the floor returns the database's hash, or EVMError::Database when the database's block_hash fails, including across the slot wrap;
    • the default source still calls Database::block_hash;
    • the current block, future blocks, distance 257 and block 0 do not reach the host.
  • Journal unit tests for each case of storage_value_unwarmed.
  • Mutation checks: a database-only read fails the same-journal test; reading with load_account + sload fails both not-warming checks.
  • Not run locally: cargo hack --feature-powerset, zepter, the kzg-rs no_std entry, forge/genesis-bytecode jobs (no features or Solidity changed).

🤖 Generated with Claude Code

isaacdoidge and others added 3 commits November 13, 2026 13:02
Add `BlockHashSource` to the EVM configuration (`Cfg::block_hash_source`,
`CfgEnv::block_hash_source`). `Database`, the default, keeps the upstream
read through `Database::block_hash`. `HistoryContract { floor }` reads slot
`n % HISTORY_SERVE_WINDOW` of the history contract instead: a non-zero slot
is the hash, a zero slot below `floor` is a zero hash, and a zero slot at or
above `floor` is a host error (`ContextError::Custom`).

The slot is read with the new `JournalTr::storage_value_unwarmed`, which
returns the journal's present value before falling back to the database,
because the current block's history write stays in the journal until the
block is finalized. The read neither warms nor journals the account or the
slot, so BLOCKHASH keeps its fixed 20 gas (Ethereum mainnet's pricing, not
EIP-7709's added storage charge) and leaves the gas of later reads unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A probe contract executes BLOCKHASH(n) and then calls the history
contract's `get(n)` twice, measuring the gas of each call. The tests cover
a hash written by the same block's history write while it is still in the
journal (with a different stale value in the database), a hash in the
database only, a zero slot below and at the floor, the default database
source, and the heights the opcode answers without a host read. The gas of
the two `get` calls shows that the BLOCKHASH read warmed neither the
contract's account nor its slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hash.

With `BlockHashSource::HistoryContract { floor }`, a zero slot for a block
at or above `floor` is a height the history contract should hold but does
not. The host now refers it to `Database::block_hash`, whose result or
error is the answer; a database error is stored as `ContextError::Db` and
fails the transaction as `EVMError::Database`. A host whose database
cannot serve block hashes thereby fails the transaction with a typed
database error. A zero slot below `floor` is still a zero hash.

The tests check both outcomes of the database read, at the floor, above
it, and for a slot that wraps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
};
if !value.is_zero() {
Some(B256::from(value))
} else if requested_number < floor {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if requested_number < floor { can be done early to avoid journal read.

@isaacdoidge

Copy link
Copy Markdown
Collaborator Author

Closed unmerged. Entropy-Foundation/smr-moonshot#5001 serves BLOCKHASH from the EIP-2935 history contract through the existing Database::block_hash hook in smr-moonshot's EvmDB, so no revm change is needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants