erigontech / erigontech/erigon
Code and account domain writes should be atomic at the IBS/writer layer
- Dominant language
- Go
- Stars
- 3.6k
- Forks
- 1.5k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 455
Description
## Code and account domain writes should be atomic at the IBS / writer layer
### Summary
`CodeDomain` and `AccountsDomain` are written as two **independent, sequentially-applied** `DomainPut`s 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.
- **prevVal poisoning (fixed in #21386 by removing the bypass):** code writes pass `prevVal=nil`, so `DomainPut` resolves prevVal via `GetLatest(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 #21386, and #21675.)
- **read-path coupling (contained in #21386):** `GetCodeSize`/`GetCode` only 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) 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 `DomainPut`s 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
- #21386 — StateCache LRU + (txNum,epoch); removed the L2b bypass, documented the read-only contract.
- #21675 — gas-used mismatch surfaced while testing #21386.
Contributor guide
Research direction
Start in execution/state/rw_v3.go and trace the account and code apply paths, including the parallel-apply path and their DomainPut calls. Verify how the writer obtains the prior code value; done means account and code are coupled at the IBS/writer layer, code receives its explicit prevVal, and no independent code write leaves an account-ahead-of-code window.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100