Skip to content

refactor: make Context and Evm generic over DB type#20

Merged
Gabriel-Trintinalia merged 7 commits intoConsensys:mainfrom
Gabriel-Trintinalia:feat/glamsterdam-context-generic
Apr 14, 2026
Merged

refactor: make Context and Evm generic over DB type#20
Gabriel-Trintinalia merged 7 commits intoConsensys:mainfrom
Gabriel-Trintinalia:feat/glamsterdam-context-generic

Conversation

@Gabriel-Trintinalia
Copy link
Copy Markdown

@Gabriel-Trintinalia Gabriel-Trintinalia commented Apr 3, 2026

Summary

Makes Context and Evm generic over the database type, enabling zevm to execute against any storage backend — not just the built-in InMemoryDB.

Changes

  • Context(comptime DB: type)Context is now a generic type parameterized on the database. DefaultContext = Context(InMemoryDB) is exported as an alias so all existing call sites are unchanged.
  • EvmFor(comptime DB: type)Evm is now a generic type parameterized on the database. Evm = EvmFor(InMemoryDB) preserves the existing concrete type.
  • Removes the FallbackFns vtable — the comptime DB dispatch makes it unnecessary.
  • Adds O(1) CREATE address collision check via the journal's warm set (replaces an O(n) scan).

Why a generic Context is necessary

On main, Context holds journaled_state: Journal(InMemoryDB) — the database type is hardcoded. This works for the standard execution path (blockchain tests, t8n) where all pre-state is loaded into InMemoryDB upfront.

zevm-stateless needs a different execution mode: stateless block execution. Instead of rebuilding the full pre-state in memory, it executes directly against a `WitnessDatabase` that serves every account/storage read by running a live MPT proof against the pre-state root:

// zevm-stateless/src/executor/main.zig
const witness_db = WitnessDatabase.init(node_index, pre_state_root, codes, block_hashes);
var ctx = Context(WitnessDatabase).new(witness_db, spec);

Every basic(address) / storage(address, key) call cryptographically verifies that the returned value is consistent with pre_state_root. This is the core security guarantee of stateless execution — without it, a prover could supply arbitrary account balances.

Using InMemoryDB for this would require pre-decoding all MPT witness nodes and loading them into a hashmap upfront, which defeats the purpose: there would be no proof verification and no way to detect a malicious witness.

Because Zig requires struct fields to have a compile-time-known type, `Context` must be parameterized on the DB type for this to work. `Journal(DB)` was already generic on `main`; this PR extends that to Context and Evm.

Impact on existing users

None. All existing code that used Context or Evm continues to work via the DefaultContext and Evm aliases which resolve to Context(InMemoryDB) and EvmFor(InMemoryDB) respectively.

Test plan

  • `zig build test` passes
  • All existing users of `Context` / `Evm` unaffected

🤖 Generated with Claude Code


Note

High Risk
High risk because it refactors core execution plumbing (Context, Evm, Host, journaling) and changes gas/account-access behavior around cold/warm checks, which can affect correctness of execution, state commits, and EIP-specific accounting.

Overview
Makes execution generic over the database backend. Context is replaced with Context(DB) and a DefaultContext = Context(InMemoryDB) alias, and handler Evm becomes EvmFor(DB) with the existing Evm preserved as the InMemoryDB specialization.

Reworks interpreter↔state integration to support typed DBs and correct access tracking. Host now type-erases the journal behind a JournalVTable, adds cold-check helpers, and updates call/create setup/finalization to snapshot/commit/revert per-frame tracking; the mainnet handler now warms access lists without eager DB loads and commits/discards per-tx tracking alongside commitTx.

Improves Block Access List (EIP-7928) correctness and CREATE collision checks. Opcodes (CALL, BALANCE, EXTCODE*, SLOAD, SELFDESTRUCT) now charge warm/cold gas before loading from DB and explicitly untrackAddress on OOG paths to avoid recording phantom accesses; Journal adds cold-check APIs, storage write revert metadata, and optional DB hooks, while InMemoryDB gains an O(1) hasNonZeroStorageForAddress index used for CREATE address-collision detection.

Written by Cursor Bugbot for commit b85262d. This will update automatically on new commits. Configure here.

Gabriel-Trintinalia and others added 6 commits April 2, 2026 12:07
Add the journal/database plumbing required for correct EIP-7928 Block
Access List construction:

- primitives: fix TX_GAS_LIMIT_CAP to 16777216 (EIP-7825 value)
- database: FallbackFns vtable gains commit_tx/discard_tx/snapshot_frame/
  commit_frame/revert_frame/untrack_address hooks so a stateless witness
  DB can track accesses with frame-accurate reverts
- context/journal: lazy access-list pre-warming (warmAccessList / setAccessList)
  replaces eager loadAccountWithCode+sload; adds isAddressCold/isStorageCold
  predicates; StorageChanged journal entry now records old_was_written for
  correct revert; snapshotFrame/commitFrame/revertFrame forwarded to db
- handler/mainnet_builder: use warmAccessList for EIP-2929 pre-warming;
  call snapshotFrame/revertFrame around top-level CALL value transfer
- interpreter/host: propagate snapshotFrame/commitFrame/revertFrame through
  CALL/CREATE frame lifecycle
- interpreter/opcodes/call + host_ops: use isAddressCold/isStorageCold for
  pre-call gas checks without triggering DB loads; untrack_address on OOG
- state: storage slot gains was_written flag for revert correctness

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove DBG prints from InMemoryDB.basic and JournalInner.setCodeWithHash
- Fix isAddressCold: only treat coinbase as cross-tx warm (EIP-3651),
  not all previously-loaded addresses, to avoid unintended gas discounts
  for precompiles and access-list entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split the CALL instruction OOG check into two stages to match EELS:
1. If remaining < call_cost_no_delegation: untrack target (oog_before_target_access)
2. If remaining < base_cost (includes delegation): halt OOG without untracking target
   (oog_after_target_access and oog_success_minus_1 — target IS in BAL, delegation NOT)

Also add unconditional untrackAddress for OOG in EXTCODECOPY and CALL-type
opcodes when gas is exhausted before the target is accessed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove FallbackFns C-style vtable and InMemoryDB.fallback field
- Make Context generic: Context(comptime DB: type); DefaultContext = Context(InMemoryDB)
- Add Journal(DB) tracking wrappers guarded by @hasDecl (snapshotFrame, commitFrame,
  revertFrame, commitTracking, discardTracking, notifyStorageSlotCommit,
  hasNonZeroStorageForAddress, untrackAddress, forceTrackAddress)
- Replace Host.ctx with type-erased JournalVTable (18 entries); Host.init(DB, ctx, prec)
  and Host.fromCtx(ctx, prec) constructors; opcode handlers unchanged
- Make Evm generic: EvmFor(comptime DB: type); Evm = EvmFor(InMemoryDB) alias
- Make MainnetHandler.* functions accept anytype evm for zero-cost duck typing
- Fix getDb() dangling pointer: change self: @this() → self: *const @this()
- Update test files to use Host.fromCtx() instead of Host{ .ctx = ... } literal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ecute

- Add nonzero_storage_count index to InMemoryDB maintained by new private
  putStorage helper; hasNonZeroStorageForAddress is now O(1) instead of
  O(n) storage scan on every CREATE
- Drop explicit `comptime DB: type` parameter from Frame.execute; infer DB
  via anytype ctx to remove parameter sprawl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ransfer failure

- withCfg and modifyCfgChained now call setSpecId on the returned
  context's journaled_state so the journal spec stays in sync
- setupCallCore now calls revertFrame() before checkpointRevert() on
  the two early-return paths when value transfer fails, matching the
  pattern in mainnet_builder.zig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

return false;
}
return self.warm_addresses.isCold(address);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

isAddressCold misses access-list check causing false OOG

High Severity

The new isAddressCold only checks coinbase for addresses in evm_state with a stale transaction_id, but loadAccountMutOptionalCode (unchanged) checks the full warm_addresses.isCold(address) which also covers precompiles and access-list entries. In multi-tx block execution, an access-list address loaded in a prior tx will be reported as cold by isAddressCold but warm by accountInfo. The CALL opcode pre-check at line 138–145 uses isAddressCold and will incorrectly halt with OOG when gas is between WARM_ACCOUNT_ACCESS (100) and COLD_ACCOUNT_ACCESS (2600).

Additional Locations (1)
Fix in Cursor Fix in Web

@garyschulte
Copy link
Copy Markdown
Collaborator

garyschulte commented Apr 13, 2026

What is the reasoning behind using a witness db in zevm-stateless (and lazily decode) versus using an in-memory db where we pre-load with witness (and use a hash pool to prevent unecessary re-hashing) ?

It seems fine to have a generic db interface, I am wondering what the reasoning is behind using a witness db though. If it is performance considerations, do you have performance numbers that show the witness db to be faster?

moreover, I think this statement makes a false claim:

Using InMemoryDB for this would require pre-decoding all MPT witness nodes and loading them into a hashmap upfront, which defeats the purpose: there would be no proof verification and no way to detect a malicious witness.

Comment on lines +145 to +146
/// Inner context.
chain: void,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what is chain with type void? should it be in the interface if it has no type and no constraints?

it looks like a pre-existing vestige. We can leave it I guess, but for a stateless client, all of the chain methods and the void type are just weird baggage.

@Gabriel-Trintinalia Gabriel-Trintinalia merged commit 87e34fb into Consensys:main Apr 14, 2026
6 checks passed
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