Context
kvcache.Coherent becomes the default state cache for the standalone rpcdaemon in #22269 (the --state.cache 128MB default), stacked on #22532 (the kvcache split of #21293) which fixes the announced-vs-committed PlainStateVersion parity — so canonical cache roots are actually served to readers for the first time. The state-change producers were never hardened for a consumer that trusts batches as a complete statement of what changed. Four gaps:
Gaps
- Forward account deletions are never announced.
Writer.DeleteAccount (execution/state/rw_v3.go) deletes from AccountsDomain but its accumulator.DeleteAccount call is commented out ("basically this will always panic. keeping this out should be fine anyway"). A selfdestructed (EIP-6780 same-tx) or cleared account keeps its old value in any cache fed by the batches.
- Incarnation re-creation wipes code and storage without announcing it. The del-before-create branch of
Writer.UpdateAccountData does DomainDel(Code) + DomainDelPrefix(Storage) but only emits ChangeAccount.
- Unwind never restores code.
unwindExec3State (execution/stagedsync/stage_execute.go) emits account and storage restores but carries //TODO: why we don't call accumulator.ChangeCode???.
- Unwind emits spurious deletions across step boundaries. In-source comment in the same function: values for previous steps "will pass nil values here which will look like a delete" — consumers receive REMOVE actions for accounts that still exist.
Consequences
With cross-block carry-over in the cache, gaps 1–3 produce indefinite staleness (no later batch corrects an untouched key) and gap 4 caches false absence markers. As mitigation, #22532 disables cross-block carry-over in kvcache.Coherent.advanceRoot (each canonical root starts from its own batch), bounding any producer gap to one version at the cost of cross-block cache reuse.
Acceptance
- Fix the four producer sites (each with its own test).
- Add a reorg/deletion round-trip test at the execmodule level: insert → cache serves state → selfdestruct + unwind/re-org → cache serves the restored/deleted state.
- Then re-enable cross-block cloning in
kvcache.Coherent.advanceRoot (the workaround comment there references this issue) to recover cross-block hit rates.
Nice-to-have while in the area: kvcache.Element.Size() counts only len(K)+len(V), so configured cache budgets undercount real RSS (no btree/struct overhead).
Context
kvcache.Coherent becomes the default state cache for the standalone rpcdaemon in #22269 (the
--state.cache128MBdefault), stacked on #22532 (the kvcache split of #21293) which fixes the announced-vs-committedPlainStateVersionparity — so canonical cache roots are actually served to readers for the first time. The state-change producers were never hardened for a consumer that trusts batches as a complete statement of what changed. Four gaps:Gaps
Writer.DeleteAccount(execution/state/rw_v3.go) deletes fromAccountsDomainbut itsaccumulator.DeleteAccountcall is commented out ("basically this will always panic. keeping this out should be fine anyway"). A selfdestructed (EIP-6780 same-tx) or cleared account keeps its old value in any cache fed by the batches.Writer.UpdateAccountDatadoesDomainDel(Code)+DomainDelPrefix(Storage)but only emitsChangeAccount.unwindExec3State(execution/stagedsync/stage_execute.go) emits account and storage restores but carries//TODO: why we don't call accumulator.ChangeCode???.Consequences
With cross-block carry-over in the cache, gaps 1–3 produce indefinite staleness (no later batch corrects an untouched key) and gap 4 caches false absence markers. As mitigation, #22532 disables cross-block carry-over in
kvcache.Coherent.advanceRoot(each canonical root starts from its own batch), bounding any producer gap to one version at the cost of cross-block cache reuse.Acceptance
kvcache.Coherent.advanceRoot(the workaround comment there references this issue) to recover cross-block hit rates.Nice-to-have while in the area:
kvcache.Element.Size()counts onlylen(K)+len(V), so configured cache budgets undercount real RSS (no btree/struct overhead).