Code and account domain writes should be atomic at the IBS / writer layer
Summary
CodeDomain and AccountsDomain are written as two independent, sequentially-applied DomainPuts for the same address (e.g. execution/state/rw_v3.go writes the account record, then the code). Because they are not atomic, there is a transient window — between the account write and the code write — where the account record already carries the new codeHash but CodeDomain does not yet hold the code for that address. That window is the root cause of a class of cache-on/parallel hazards, not just the individual symptoms patched in #21386.
Why it bites
The account-codeHash → code resolution (used by the now-removed L2b GetLatest bypass, and still by the read-only GetCodeSize/GetCode fast paths) is content-addressed and shared across every address with the same code. In the window above, resolving an address's code via its (already-advanced) account codeHash returns the new code while CodeDomain still holds the old/empty value.
The durable fix
If the IBS/writer layer never put code independently of its account — wrote (account, code) as one unit, supplying the code's prevVal (which the writer knows) so DomainPut never resolves it from a possibly-advanced account — then:
- the account-ahead-of-code window does not exist;
GetLatest's internal prevVal read cannot time-travel — the lost-write is structurally impossible;
- the account→codeHash shortcut becomes safe on every path (reads,
EXTCODESIZE, and prevVal), so the dedup fast path no longer needs the "read-only only" caveat and the removed L2b bypass could even return.
This generalizes @sudeepdino008's "writers pass explicit prevVal for CodeDomain puts" (option 3) into an invariant: code is a dependent of its account and is never written independently.
Scope
Writer-side change in execution/state (the rw_v3.go apply paths and the parallel-apply path), coupling the account + code DomainPuts and threading the prior code as prevVal. Not a blocker for #21386 (whose contained fixes are correct and sufficient), but the principled root fix.
Refs
Code and account domain writes should be atomic at the IBS / writer layer
Summary
CodeDomainandAccountsDomainare written as two independent, sequentially-appliedDomainPuts for the same address (e.g.execution/state/rw_v3.gowrites the account record, then the code). Because they are not atomic, there is a transient window — between the account write and the code write — where the account record already carries the newcodeHashbutCodeDomaindoes not yet hold the code for that address. That window is the root cause of a class of cache-on/parallel hazards, not just the individual symptoms patched in #21386.Why it bites
The account-codeHash → code resolution (used by the now-removed L2b
GetLatestbypass, and still by the read-onlyGetCodeSize/GetCodefast paths) is content-addressed and shared across every address with the same code. In the window above, resolving an address's code via its (already-advanced) accountcodeHashreturns the new code whileCodeDomainstill holds the old/empty value.prevVal=nil, soDomainPutresolves prevVal viaGetLatest(CodeDomain, addr). With the bypass, a later EIP-7702 authority delegating to an already-seen delegate read the about-to-be-written designator as its "previous" value →bytes.Equal(prevVal, v)dropped the write → wrong trie root / gas-used mismatch. (Full bisection: @sudeepdino008 in execution/cache: StateCache LRU + Mode + (txNum,epoch) lazy unwind #21386, and State-cache L2b code-by-hash bypass is unsound under parallel (OCC) execution #21675.)GetCodeSize/GetCodeonly avoid this because they are pure getters that never feed a prevVal and the EVM only ever reads applied/consistent state — a constraint that has to be documented and preserved by hand.The durable fix
If the IBS/writer layer never put code independently of its account — wrote
(account, code)as one unit, supplying the code's prevVal (which the writer knows) soDomainPutnever resolves it from a possibly-advanced account — then:GetLatest's internal prevVal read cannot time-travel — the lost-write is structurally impossible;EXTCODESIZE, and prevVal), so the dedup fast path no longer needs the "read-only only" caveat and the removed L2b bypass could even return.This generalizes @sudeepdino008's "writers pass explicit prevVal for CodeDomain puts" (option 3) into an invariant: code is a dependent of its account and is never written independently.
Scope
Writer-side change in
execution/state(therw_v3.goapply paths and the parallel-apply path), coupling the account + codeDomainPuts and threading the prior code as prevVal. Not a blocker for #21386 (whose contained fixes are correct and sufficient), but the principled root fix.Refs