Summary
We currently cache Ethereum execution state in four separate places, causing significant code duplication, a latent timing race between blocks, and an expensive round-trip in the hot path of parallel execution. The goal of this refactor is to reduce to two authoritative caches:
VersionMap / VersionedIO — intra-block, per-transaction, revertable MVCC memory. Workers write here; snapshots and reverts operate here. This is the source of truth during a block's execution.
SharedDomains — confirmed inter-block state. After a block is fully validated and ordered, its net writes are applied here. Workers for the next block read from this as their baseline.
Everything else — stateObject, rs.accounts / StateV3Buffered, and BufferedWriter — is redundant scaffolding that can be eliminated once the two-cache model is properly plumbed.
Background: The Current 4-Cache Architecture
During parallel block execution, a state write touches up to four distinct stores:
| Cache |
Type |
Location |
Role |
VersionedIO / WriteSet |
Field-granular MVCC map |
versionmap.go, versionedio.go |
Per-tx write set; conflict detection; BAL hash |
stateObject |
Per-address struct |
state_object.go |
EVM read/write buffer inside IBS; snapshot substrate |
StateV3Buffered.accounts (rs.accounts) |
Address-keyed in-memory map |
rw_v3.go |
Cross-tx/cross-block read cache; workaround for timing hole |
SharedDomains |
Persistent in-memory B-tree |
shared_domains.go |
Block-to-block state; written to MDBX on commit |
The Dual-Path Round-Trip
The most acute form of duplication is in finalize() inside exec3_parallel.go. After a transaction is validated and ordered, the code reconstructs an IntraBlockState from VersionedWrites, then immediately tears it back down into StateUpdates for domain application:
VersionedWrites (field-granular)
→ ApplyVersionedWrites → new IBS (stateObjects rebuilt from scratch)
→ FinalizeTx / MakeWriteSet → StateUpdates (account-granular)
→ BufferedWriter → SharedDomains + rs.accounts
VersionedWrites and StateUpdates carry the same information in different formats. VersionedWrites has BalancePath → uint256.Int, NoncePath → uint64, AddressPath → *accounts.Account, etc. StateUpdates has writeSet BTreeG[Address, *accountWrite] with full *accounts.Account structs. The AddressPath entry in VersionedWrites already carries the full account, so StateUpdates can be derived directly without the IBS round-trip.
The Timing Hole
In execLoop(), block N+1's workers are scheduled as soon as block N's blockExecutor.result is complete:
blockExecutor.applyResults <- blockResult // async
// block N+1 workers start HERE:
blockExecutor, ok = pe.blockExecutors[blockResult.BlockNum+1]
if ok {
blockExecutor.scheduleExecution(ctx, pe)
}
Block N+1 workers immediately begin reading state. But applyResults is an async channel — the exec() goroutine may not have applied block N's writes to SharedDomains yet. rs.accounts (StateV3Buffered.accounts) patches this by being populated synchronously in finalize() / nextResult() before applyResults is sent. This is a workaround for the race, not a fix. Eliminating rs.accounts requires making the domain apply synchronous (in execLoop(), before scheduling N+1).
stateObject Eliminability
stateObject currently serves three roles:
- Write buffer —
data.Balance, data.Nonce, dirtyStorage, dirtyCode, selfdestructed. Fully duplicated by VersionedWrites when versionMap != nil.
- Read cache —
code []byte, originStorage (pre-tx values for EIP-1283/2929), blockOriginStorage. Replaceable by a lightweight codeCache map[Address][]byte and committedStorage map[Address]map[StorageKey]uint256.Int on IBS.
- Snapshot/revert substrate — journal entries already contain
prev + wasCommited booleans sufficient to revert VersionedWrites independently of stateObject. The one exception is resetObjectChange (for CreateAccount overwriting an existing account), which currently snapshots the whole *stateObject and needs to be migrated to snapshot map[AccountKey]*VersionedWrite.
Since we are decommissioning the serial execution path, versionMap will always be non-nil in IBS. This makes the stateObject write-buffer role immediately redundant.
The newlyCreated flag (EIP-6780, SELFDESTRUCT semantics) is derivable from VersionedWrites by checking whether IncarnationPath is present with a write from the current transaction.
Phased Refactoring Plan
Phase 1 — Establish a Test Baseline
Before any structural changes, add tests that will catch regressions across all subsequent phases.
New tests in execution/state/intra_block_state_test.go:
TestVersionedWritesMatchStateObjects — for a randomized sequence of EVM operations, assert that every field in every stateObject is reflected in VersionedWrites with the same value.
TestSnapshotRandomWithVersionMap — extend the existing TestSnapshotRandom to always set versionMap, and verify that revert produces identical state in both stateObjects and VersionedWrites.
TestCommittedStateWithVersionMap — verify that GetCommittedState returns pre-tx values (EIP-1283) correctly when reading through versionMap at txIndex-1.
TestStateUpdatesEquivalence (initially skipped) — given the same tx sequence, assert that StateUpdatesFromVersionedWrites() (not yet implemented) produces identical StateUpdates to the existing BufferedWriter path. Unskip in Phase 2.
New tests in execution/stagedsync/exec3_parallel_test.go:
TestCrossBlockStateReadConsistency — two sequential blocks; block N+1 workers must read block N's committed writes. Assert no stale reads.
TestCrossBlockTimingRace — stress test: inject artificial delay in exec() goroutine's ApplyTxState; verify block N+1 workers still see correct state. This test should be designed to fail on the current code without rs.accounts, to demonstrate the hole.
TestDomainApplyFromVersionedWrites — assert that applying StateUpdates derived from VersionedWrites to a test SharedDomains instance produces the same domain state as the existing path.
Phase 2 — Derive StateUpdates Directly from VersionedWrites
Goal: Eliminate the VersionedWrites → IBS → StateUpdates round-trip in finalize().
Changes:
- Add
StateUpdatesFromVersionedWrites(writes VersionedWrites, rules *chain.Rules) StateUpdates in versionedio.go (or a new file). Iterate writes, reconstruct *accounts.Account from AddressPath (full account), BalancePath, NoncePath, IncarnationPath, CodePath, StoragePath, SelfDestructPath.
- In
finalize(), replace ApplyVersionedWrites → IBS → FinalizeTx/MakeWriteSet → BufferedWriter with a direct call to StateUpdatesFromVersionedWrites.
- Keep
rs.accounts update in place (still needed for timing hole workaround — removed in Phase 3).
Tests to un-skip/add:
TestStateUpdatesEquivalence — un-skip; must pass.
TestStateUpdatesFromVersionedWritesSelfDestruct — verify selfdestruct is represented correctly (account deletion vs. zeroing).
TestStateUpdatesFromVersionedWritesIncarnation — verify that a recreated contract (new incarnation) produces correct domain writes.
Phase 3 — Fix the Timing Hole; Eliminate rs.accounts
Goal: Make domain apply synchronous so SharedDomains is always up-to-date before block N+1 workers start.
Changes:
- In
execLoop(), after blockExecutor.result is complete, call applyBlockWritesToDomains(blockResult) synchronously before scheduleExecution(blockResult.BlockNum+1). This replaces the async applyResults channel send for the domain-apply portion (the channel may still be used for other coordination).
- Remove
StateV3Buffered.accounts (the rs.accounts map) and bufferedReader. Workers now read from SharedDomains directly (via ReaderV3) for cross-block state.
- Remove
BufferedWriter's rs.accounts update side effect; it is now dead code.
Tests:
TestCrossBlockTimingRace — must now pass without rs.accounts.
TestCrossBlockApplyOrdering — assert that applying block N synchronously before scheduling N+1 means N+1 workers never see stale state, even under goroutine scheduling pressure.
TestDomainConsistencyAfterBlockApply — after applyBlockWritesToDomains, assert the SharedDomains snapshot matches what VersionedWrites predicted.
TestNoRsAccountsRace — run the race detector (-race) over a multi-block parallel execution scenario; confirm no data races on the eliminated rs.accounts map.
Phase 4 — Journal Reverts via VersionedWrites Only; Migrate resetObjectChange
Goal: Decouple journal revert from stateObject, so Phase 5 can remove stateObject entirely.
Changes:
- In all journal entries (
balanceChange, nonceChange, storageChange, addSlotToAccessListChange, etc.), remove the stateObject field mutations from the revert() method. The VersionedWrites revert path (already present in the if s.versionMap != nil block) is the only one that matters.
- Migrate
resetObjectChange: instead of storing *stateObject, store map[AccountKey]*VersionedWrite (a snapshot of the pre-CreateAccount writes for that address). Revert restores those entries.
- Remove
balanceInc map from IBS (only used in serial path).
- Verify that
newlyCreated derivation from VersionedWrites is correct for EIP-6780.
Tests:
TestJournalRevertWithVersionMap — revert every journal entry type and assert VersionedWrites contains the correct previous value.
TestResetObjectChangeMigrateWrites — create an account, write to it, then CreateAccount over it, then revert; assert the pre-creation writes are fully restored in VersionedWrites.
TestSnapshotRandomWithVersionMapExtended — extend Phase 1 test with more operations including CreateAccount over existing accounts, code sets, self-destructs. Must pass with journal reverts driven solely by VersionedWrites.
Phase 5 — Remove stateObject
Goal: IntraBlockState holds no stateObjects map. All reads go through versionMap/storage reader; all writes go to VersionedWrites.
Changes:
- Add
codeCache map[Address][]byte and committedStorage map[Address]map[StorageKey]uint256.Int to IntraBlockState for the read-cache role of stateObject.
GetCommittedState(addr, key) reads from committedStorage if present; otherwise reads versionMap at txIndex-1 (pre-tx value for EIP-1283 gas calculation); populates committedStorage on first access.
GetCode(addr) reads from codeCache; populates from storage reader on miss.
- Remove
stateObjects, stateObjectsDirty, nilAccounts, dirtyStorage, originStorage, blockOriginStorage from IBS.
- Remove
state_object.go (or reduce to an empty shim for the compilation cycle).
Tests:
TestGetCommittedStateNoStateObject — assert EIP-1283 original values are correct when reading through committedStorage/versionMap.
TestCreatedContractFlagFromVersionedWrites — assert newlyCreated is correctly derived from presence of IncarnationPath write in current tx.
TestSSTOREGasCalcNoStateObject — end-to-end SSTORE gas calculation for cold/warm/original value cases, with no stateObject.
- Full execution-spec-tests (
TestExecutionSpecBlockchain) — run the full EEST fixture suite; all tests must pass.
TestDeleteRecreateSlots, TestDeleteCreateRevert — existing chain-level tests; must continue to pass.
Summary Table
| Phase |
Goal |
Key Change |
Gate Tests |
| 1 |
Baseline |
Add tests that will catch regressions |
TestVersionedWritesMatchStateObjects, TestCrossBlockTimingRace, TestSnapshotRandomWithVersionMap |
| 2 |
Eliminate round-trip |
StateUpdatesFromVersionedWrites, remove IBS reconstruction in finalize() |
TestStateUpdatesEquivalence, TestStateUpdatesFromVersionedWritesSelfDestruct |
| 3 |
Fix timing hole |
Synchronous domain apply in execLoop, remove rs.accounts |
TestCrossBlockTimingRace (now passing), TestNoRsAccountsRace |
| 4 |
Decouple journal from stateObject |
Revert only via VersionedWrites; migrate resetObjectChange |
TestJournalRevertWithVersionMap, TestResetObjectChangeMigrateWrites |
| 5 |
Remove stateObject |
codeCache, committedStorage on IBS; delete state_object.go |
TestGetCommittedStateNoStateObject, TestSSTOREGasCalcNoStateObject, EEST suite |
Files Affected
execution/state/intra_block_state.go — major (Phases 2–5)
execution/state/state_object.go — removed in Phase 5
execution/state/journal.go — Phase 4
execution/state/versionedio.go — Phase 2 (StateUpdatesFromVersionedWrites)
execution/state/rw_v3.go — Phase 3 (remove StateV3Buffered.accounts, bufferedReader, BufferedWriter side-effect)
execution/stagedsync/exec3_parallel.go — Phases 2 and 3 (simplify finalize(), synchronize domain apply in execLoop)
execution/exec/state.go — Phase 3 (worker state reader simplified)
Summary
We currently cache Ethereum execution state in four separate places, causing significant code duplication, a latent timing race between blocks, and an expensive round-trip in the hot path of parallel execution. The goal of this refactor is to reduce to two authoritative caches:
VersionMap/VersionedIO— intra-block, per-transaction, revertable MVCC memory. Workers write here; snapshots and reverts operate here. This is the source of truth during a block's execution.SharedDomains— confirmed inter-block state. After a block is fully validated and ordered, its net writes are applied here. Workers for the next block read from this as their baseline.Everything else —
stateObject,rs.accounts/StateV3Buffered, andBufferedWriter— is redundant scaffolding that can be eliminated once the two-cache model is properly plumbed.Background: The Current 4-Cache Architecture
During parallel block execution, a state write touches up to four distinct stores:
VersionedIO/WriteSetversionmap.go,versionedio.gostateObjectstate_object.goStateV3Buffered.accounts(rs.accounts)rw_v3.goSharedDomainsshared_domains.goThe Dual-Path Round-Trip
The most acute form of duplication is in
finalize()insideexec3_parallel.go. After a transaction is validated and ordered, the code reconstructs anIntraBlockStatefromVersionedWrites, then immediately tears it back down intoStateUpdatesfor domain application:VersionedWritesandStateUpdatescarry the same information in different formats.VersionedWriteshasBalancePath → uint256.Int,NoncePath → uint64,AddressPath → *accounts.Account, etc.StateUpdateshaswriteSet BTreeG[Address, *accountWrite]with full*accounts.Accountstructs. TheAddressPathentry inVersionedWritesalready carries the full account, soStateUpdatescan be derived directly without the IBS round-trip.The Timing Hole
In
execLoop(), block N+1's workers are scheduled as soon as block N'sblockExecutor.resultis complete:Block N+1 workers immediately begin reading state. But
applyResultsis an async channel — theexec()goroutine may not have applied block N's writes toSharedDomainsyet.rs.accounts(StateV3Buffered.accounts) patches this by being populated synchronously infinalize()/nextResult()beforeapplyResultsis sent. This is a workaround for the race, not a fix. Eliminatingrs.accountsrequires making the domain apply synchronous (inexecLoop(), before scheduling N+1).stateObjectEliminabilitystateObjectcurrently serves three roles:data.Balance,data.Nonce,dirtyStorage,dirtyCode,selfdestructed. Fully duplicated byVersionedWriteswhenversionMap != nil.code []byte,originStorage(pre-tx values for EIP-1283/2929),blockOriginStorage. Replaceable by a lightweightcodeCache map[Address][]byteandcommittedStorage map[Address]map[StorageKey]uint256.Inton IBS.prev+wasCommitedbooleans sufficient to revertVersionedWritesindependently ofstateObject. The one exception isresetObjectChange(forCreateAccountoverwriting an existing account), which currently snapshots the whole*stateObjectand needs to be migrated to snapshotmap[AccountKey]*VersionedWrite.Since we are decommissioning the serial execution path,
versionMapwill always be non-nil in IBS. This makes the stateObject write-buffer role immediately redundant.The
newlyCreatedflag (EIP-6780, SELFDESTRUCT semantics) is derivable fromVersionedWritesby checking whetherIncarnationPathis present with a write from the current transaction.Phased Refactoring Plan
Phase 1 — Establish a Test Baseline
Before any structural changes, add tests that will catch regressions across all subsequent phases.
New tests in
execution/state/intra_block_state_test.go:TestVersionedWritesMatchStateObjects— for a randomized sequence of EVM operations, assert that every field in everystateObjectis reflected inVersionedWriteswith the same value.TestSnapshotRandomWithVersionMap— extend the existingTestSnapshotRandomto always setversionMap, and verify that revert produces identical state in both stateObjects and VersionedWrites.TestCommittedStateWithVersionMap— verify thatGetCommittedStatereturns pre-tx values (EIP-1283) correctly when reading through versionMap attxIndex-1.TestStateUpdatesEquivalence(initially skipped) — given the same tx sequence, assert thatStateUpdatesFromVersionedWrites()(not yet implemented) produces identicalStateUpdatesto the existingBufferedWriterpath. Unskip in Phase 2.New tests in
execution/stagedsync/exec3_parallel_test.go:TestCrossBlockStateReadConsistency— two sequential blocks; block N+1 workers must read block N's committed writes. Assert no stale reads.TestCrossBlockTimingRace— stress test: inject artificial delay inexec()goroutine'sApplyTxState; verify block N+1 workers still see correct state. This test should be designed to fail on the current code withoutrs.accounts, to demonstrate the hole.TestDomainApplyFromVersionedWrites— assert that applyingStateUpdatesderived fromVersionedWritesto a testSharedDomainsinstance produces the same domain state as the existing path.Phase 2 — Derive
StateUpdatesDirectly fromVersionedWritesGoal: Eliminate the
VersionedWrites → IBS → StateUpdatesround-trip infinalize().Changes:
StateUpdatesFromVersionedWrites(writes VersionedWrites, rules *chain.Rules) StateUpdatesinversionedio.go(or a new file). Iterate writes, reconstruct*accounts.AccountfromAddressPath(full account),BalancePath,NoncePath,IncarnationPath,CodePath,StoragePath,SelfDestructPath.finalize(), replaceApplyVersionedWrites → IBS → FinalizeTx/MakeWriteSet → BufferedWriterwith a direct call toStateUpdatesFromVersionedWrites.rs.accountsupdate in place (still needed for timing hole workaround — removed in Phase 3).Tests to un-skip/add:
TestStateUpdatesEquivalence— un-skip; must pass.TestStateUpdatesFromVersionedWritesSelfDestruct— verify selfdestruct is represented correctly (account deletion vs. zeroing).TestStateUpdatesFromVersionedWritesIncarnation— verify that a recreated contract (new incarnation) produces correct domain writes.Phase 3 — Fix the Timing Hole; Eliminate
rs.accountsGoal: Make domain apply synchronous so
SharedDomainsis always up-to-date before block N+1 workers start.Changes:
execLoop(), afterblockExecutor.resultis complete, callapplyBlockWritesToDomains(blockResult)synchronously beforescheduleExecution(blockResult.BlockNum+1). This replaces the asyncapplyResultschannel send for the domain-apply portion (the channel may still be used for other coordination).StateV3Buffered.accounts(thers.accountsmap) andbufferedReader. Workers now read fromSharedDomainsdirectly (viaReaderV3) for cross-block state.BufferedWriter'srs.accountsupdate side effect; it is now dead code.Tests:
TestCrossBlockTimingRace— must now pass withoutrs.accounts.TestCrossBlockApplyOrdering— assert that applying block N synchronously before scheduling N+1 means N+1 workers never see stale state, even under goroutine scheduling pressure.TestDomainConsistencyAfterBlockApply— afterapplyBlockWritesToDomains, assert theSharedDomainssnapshot matches whatVersionedWritespredicted.TestNoRsAccountsRace— run the race detector (-race) over a multi-block parallel execution scenario; confirm no data races on the eliminatedrs.accountsmap.Phase 4 — Journal Reverts via
VersionedWritesOnly; MigrateresetObjectChangeGoal: Decouple journal revert from
stateObject, so Phase 5 can removestateObjectentirely.Changes:
balanceChange,nonceChange,storageChange,addSlotToAccessListChange, etc.), remove thestateObjectfield mutations from therevert()method. TheVersionedWritesrevert path (already present in theif s.versionMap != nilblock) is the only one that matters.resetObjectChange: instead of storing*stateObject, storemap[AccountKey]*VersionedWrite(a snapshot of the pre-CreateAccountwrites for that address). Revert restores those entries.balanceIncmap from IBS (only used in serial path).newlyCreatedderivation fromVersionedWritesis correct for EIP-6780.Tests:
TestJournalRevertWithVersionMap— revert every journal entry type and assertVersionedWritescontains the correct previous value.TestResetObjectChangeMigrateWrites— create an account, write to it, thenCreateAccountover it, then revert; assert the pre-creation writes are fully restored inVersionedWrites.TestSnapshotRandomWithVersionMapExtended— extend Phase 1 test with more operations includingCreateAccountover existing accounts, code sets, self-destructs. Must pass with journal reverts driven solely byVersionedWrites.Phase 5 — Remove
stateObjectGoal:
IntraBlockStateholds nostateObjectsmap. All reads go throughversionMap/storage reader; all writes go toVersionedWrites.Changes:
codeCache map[Address][]byteandcommittedStorage map[Address]map[StorageKey]uint256.InttoIntraBlockStatefor the read-cache role ofstateObject.GetCommittedState(addr, key)reads fromcommittedStorageif present; otherwise readsversionMapattxIndex-1(pre-tx value for EIP-1283 gas calculation); populatescommittedStorageon first access.GetCode(addr)reads fromcodeCache; populates from storage reader on miss.stateObjects,stateObjectsDirty,nilAccounts,dirtyStorage,originStorage,blockOriginStoragefrom IBS.state_object.go(or reduce to an empty shim for the compilation cycle).Tests:
TestGetCommittedStateNoStateObject— assert EIP-1283 original values are correct when reading throughcommittedStorage/versionMap.TestCreatedContractFlagFromVersionedWrites— assertnewlyCreatedis correctly derived from presence ofIncarnationPathwrite in current tx.TestSSTOREGasCalcNoStateObject— end-to-end SSTORE gas calculation for cold/warm/original value cases, with nostateObject.TestExecutionSpecBlockchain) — run the full EEST fixture suite; all tests must pass.TestDeleteRecreateSlots,TestDeleteCreateRevert— existing chain-level tests; must continue to pass.Summary Table
TestVersionedWritesMatchStateObjects,TestCrossBlockTimingRace,TestSnapshotRandomWithVersionMapStateUpdatesFromVersionedWrites, remove IBS reconstruction infinalize()TestStateUpdatesEquivalence,TestStateUpdatesFromVersionedWritesSelfDestructexecLoop, removers.accountsTestCrossBlockTimingRace(now passing),TestNoRsAccountsRaceVersionedWrites; migrateresetObjectChangeTestJournalRevertWithVersionMap,TestResetObjectChangeMigrateWritesstateObjectcodeCache,committedStorageon IBS; deletestate_object.goTestGetCommittedStateNoStateObject,TestSSTOREGasCalcNoStateObject, EEST suiteFiles Affected
execution/state/intra_block_state.go— major (Phases 2–5)execution/state/state_object.go— removed in Phase 5execution/state/journal.go— Phase 4execution/state/versionedio.go— Phase 2 (StateUpdatesFromVersionedWrites)execution/state/rw_v3.go— Phase 3 (removeStateV3Buffered.accounts,bufferedReader,BufferedWriterside-effect)execution/stagedsync/exec3_parallel.go— Phases 2 and 3 (simplifyfinalize(), synchronize domain apply inexecLoop)execution/exec/state.go— Phase 3 (worker state reader simplified)