feat(eip7928): BAL-aware access tracking for Amsterdam (Glamsterdam)#19
feat(eip7928): BAL-aware access tracking for Amsterdam (Glamsterdam)#19Gabriel-Trintinalia wants to merge 10 commits intoConsensys:mainfrom
Conversation
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>
| return false; | ||
| } | ||
| return self.warm_addresses.isCold(address); | ||
| } |
There was a problem hiding this comment.
isAddressCold ignores precompiles and access-list warmth
High Severity
When an address is in evm_state with an old transaction_id, isAddressCold only special-cases the coinbase address (lines 991-993) and returns cold for everything else. But loadAccountMutOptionalCode uses warm_addresses.isCold(address) which also checks precompiles and access-list entries. This mismatch causes opcode handlers to overcharge gas (COLD instead of WARM) for precompiles and access-list entries loaded in a prior transaction within the same block.
Additional Locations (1)
- 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>
| .chain = self.chain, | ||
| .ctx_error = ContextError.ok, | ||
| }; | ||
| } |
There was a problem hiding this comment.
withCfg drops setSpecId call on journaled state
Low Severity
withCfg replaced the old self.journaled_state.setSpecId(new_cfg.spec()) call with a no-op _ = new_cfg.spec(). The returned context will have a cfg with the new spec but a journaled_state whose internal spec ID remains stale. The same regression affects modifyCfgChained (line 282). The mutable modifyCfg variant correctly preserves the setSpecId call.
…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>
JournalInner now maintains its own always-on BAL tracking state: - bal_pre_accounts / bal_pre_storage: permanent pre-block snapshots - bal_pending_accounts / bal_pending_storage: per-tx staging - bal_committed_changed: slots dirty at any tx boundary - bal_untracked: OOG phantom addresses commitTx() absorbs committed-changed detection (was in mainnet_builder) and flushes pending → pre. discardTx() clears pending without touching permanent state. Account/storage pre-states are recorded inline in loadAccountMutOptionalCode and sload at first access. Journal(DB) wrapper gains unconditional untrackAddress, forceTrackAddress, isTrackedAddress, and takeAccessLog methods. All 9 @hasDecl-guarded tracking callbacks (snapshotFrame, commitFrame, revertFrame, commitTracking, discardTracking, notifyStorageSlotCommit, notifyStorageRead + the 2 untrack/forceTrack guards) are removed. Frame tracking is eliminated entirely: EIP-7928 keeps reverted accesses so commit==revert for frames. DB is now a pure 4-method storage backend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Journal(DB)/Context(DB)/EvmFor(DB) comptime generics with a concrete Database vtable (ptr + fn pointers). Journal, Context, and Evm are now single concrete types — no more per-DB monomorphization. The @hasDecl pattern for optional DB methods is eliminated entirely. The one remaining optional capability (hasNonZeroStorageForAddress for CREATE collision detection) lives in the Database vtable as a nullable fn ptr. Also fixes phantom BAL entries at the opcode level: CALL now uses a worst-case getCallGasCost pre-check (assumes account non-existent) before loading the target, EXTCODECOPY moves all gas charges before codeInfo(), SELFDESTRUCT adds a worst-case pre-check before h.selfdestruct(), and CREATE moves the Amsterdam balance check before js.loadAccount(new_addr). untrackAddress/forceTrackAddress and bal_untracked are fully removed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fix" This reverts commit 67605de.
Previously, addresses could be loaded into the Journal (and thus the EIP-7928 BAL) during gas calculation and then immediately untracked when the operation ran OOG. This is replaced with worst-case pre-checks that abort before any DB load. CALL: replace access_cost pre-check with getCallGasCost(spec, pre_is_cold, transfers_value, false) worst-case check before loading the target account. EXTCODECOPY: charge warm/cold + copy + memory gas before h.codeInfo(). SELFDESTRUCT: worst-case pre-check (cold + G_NEWACCOUNT) before h.selfdestruct(). CREATE: move Amsterdam balance check before js.loadAccount(new_addr). Since phantoms can no longer enter the BAL, remove the entire untracking machinery: untrackAddress, forceTrackAddress, bal_untracked from JournalInner; the corresponding Journal(DB) wrapper methods; and untrackAddress/forceTrackAddress from the JournalVTable and Host. isTrackedAddress simplified (no untracked set to consult). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>


Summary
This PR implements the EIP-7928 Block Access List (BAL) tracking infrastructure required for Glamsterdam (Amsterdam devnet-3). It is a clean rebase of the previously-reviewed work onto current
main, with all Amsterdam hardfork EIPs (#10–#14) already merged separately.Background
EIP-7928 requires that the block-level access list be constructed with frame-accurate semantics: an address or storage slot must appear in the BAL if and only if it was genuinely accessed by a transaction. The previous implementation had two classes of correctness bugs:
Phantom DB reads on OOG: opcodes like
BALANCE,EXTCODESIZE,EXTCODEHASH,EXTCODECOPY,SLOAD, andCALLloaded accounts/slots from the database before checking whether there was enough gas to pay the access cost. A stateless witness DB that intercepts these reads would record phantom accesses in the BAL even though the opcode subsequently halted without_of_gas.Missing frame isolation: database-level access tracking had no concept of call frames. Accesses inside a reverted sub-frame were never un-tracked, so the BAL could contain addresses/slots that were touched only inside a revert.
Changes
src/primitives/main.zigTX_GAS_LIMIT_CAPto16777216(correct EIP-7825 value; was incorrectly30000000).src/state/main.zigwas_written: boolfield toEvmStorageSlot. Used by the journal to correctly revert thewas_writtenflag when aSSTOREentry is rolled back (needed for EIP-7928 slot-creation tracking).src/context/journal.zigisAddressCold(addr)— query address warmth without triggering a DB load.isStorageCold(addr, key)— query slot warmth without triggering a DB load.untrackAddress(addr)— remove a previously-recorded address access from the DB fallback (used when OOG is detected after the access was recorded).forceTrackAddress(addr)— explicitly record an address in the DB fallback (used for EIP-7702 delegation targets that execute but are not in the witness).warmAccessList(map)— lazy pre-warming: records addresses/slots as warm in the journal access set without loading them from the DB. Replaces the previous eagerloadAccountWithCode+sloadloop in the EIP-2929 access-list pre-warming path.StorageChangedentry gainsold_was_written— the revert path now restoresslot.was_writtento its pre-SSTORE value, not just the value itself.snapshotFrame/commitFrame/revertFrameforwarded to the database, so a witness DB can maintain a frame-accurate access stack.isAddressColdpreviously treated all previously-loaded addresses as cross-tx warm (like coinbase). Corrected to only special-case the coinbase address per EIP-3651, avoiding unintended gas discounts for precompiles and access-list entries.src/database/main.zigFallbackFnsvtable gains new hooks:snapshot_frame,commit_frame,revert_frame,untrack_address,force_track_address,notify_storage_slot_commit,notify_storage_read. A stateless witness database can implement these to maintain a frame-accurate access log.InMemoryDBgainsfallback: ?FallbackFnsandoog_addresses: AutoHashMapfields. All lookup methods chain to the fallback. Frame lifecycle and tracking calls are forwarded.oog_addresses— set of addresses that went OOG during the current transaction. Used to suppress re-tracking when an address is accessed again after its first OOG.src/handler/mainnet_builder.zigwarmAccessList()(lazy) instead of eagerly loading every account and storage slot. This avoids recording accesses that the transaction itself never made.snapshotFrame/commitFrame/revertFramecalled around the top-level CALL value-transfer checkpoint, and forwarded through the full frame lifecycle.commitTx: iterates all changed storage slots and callsnotifyStorageSlotCommit()so the witness DB can record only slots that survived to commit.commitTracking()/discardTracking()called at transaction commit/discard boundaries.src/interpreter/host.zigisAddressCold,isStorageCold,untrackAddress,forceTrackAddress,isAddressLoadedto opcode implementations.setupCall: callsdb.snapshotFrame()before entering each CALL sub-frame;finalizeCallcallsdb.commitFrame()on success anddb.revertFrame()on failure/revert.setupCreate: records whether the CREATE target was a non-existent (empty) account. On all early-exit failure paths (insufficient balance, collision, OOG before init code, code-too-large, 0xEF prefix, deposit OOG), callsdb.untrackAddress(new_addr)if the target was phantom (had no pre-state).db.snapshotFrame()called before init code execution;db.commitFrame()/db.revertFrame()on success/failure.src/interpreter/opcodes/host_ops.zigGas is now charged before the database load for all Berlin+ opcodes:
BALANCEEXTCODESIZEEXTCODECOPYEXTCODEHASHSLOADFor
EXTCODECOPY,untrackAddressis called on all OOG paths after the access has been recorded (memory overflow, copy-cost OOG, memory-expansion OOG).For
SELFDESTRUCT,untrackAddress(target)is called when the regular-gas spend fails (OOG before the beneficiary was genuinely accessed). It is not called on state-gas OOG — at that point regular gas has already been charged, meaning the access was real.src/interpreter/opcodes/call.zigThe OOG check for CALL-type opcodes is split into two stages to match the EELS
check_gaslogic:EIP-7702 delegation cost is now computed with
isAddressCold(del_addr)instead ofaccountInfo(del_addr), so the delegation target is not loaded from the DB (and thus not tracked in the BAL) during gas calculation.Test plan
make test)Note
High Risk
High risk because it refactors core
Context/Evmtyping and rewires interpreterHost+ journal semantics around gas checks, storage journaling, and CREATE/CALL behavior, which can affect consensus-critical execution and state access accounting.Overview
Implements EIP-7928 Block Access List tracking by recording pre-block account/storage snapshots, tracking committed-changed slots at tx boundaries, and exposing
takeAccessLog()plus coldness queries (isAddressCold/isStorageCold) in the journal.Refactors execution to be database-parameterized:
Context(DB)andEvmFor(DB)become generic, withDefaultContext/defaultEvmremainingInMemoryDBaliases; the interpreterHostis rewritten to use a type-erased journal pointer + vtable so opcode handlers stay non-generic while supporting multiple DB backends.Updates CALL/CREATE-related flows to avoid phantom BAL entries (pre-check gas using coldness without DB loads, lazy access-list warming, and faster CREATE collision checks via
InMemoryDB.hasNonZeroStorageForAddress), and adjusts storage journaling to restorewas_writtenon revert via anold_was_writtenfield inStorageChangedentries.Written by Cursor Bugbot for commit fbdf8db. This will update automatically on new commits. Configure here.