What StateCache is
StateCache is one process-global in-memory cache of the latest committed values of three domains: accounts, storage, code. Its job is to skip the file-accessor/MDBX stack on repeated GetLatest reads. It is not a snapshot: for every key it holds one value — the newest one the process has applied.
Who touches it, and when:
| Actor |
Reads |
Writes |
When |
Canonical execution (SharedDomains.Commit) |
via GetLatest |
apply: put committed updates, physically delete deletions |
after the RwTx commit succeeds (FCU / batch commit), not per tx |
Embedded RPC (execmodule.CacheView → SharedDomains.AsGetter) |
via GetLatest |
fill: on a cache miss, offer the value read from its own tx |
during any request at latest |
Read-ahead warmup (blocks_read_ahead) |
— |
fill: same, to pre-warm keys for exec |
while exec runs |
Vocabulary, used consistently below and in #22444:
- A fill is a cache write performed on behalf of a database reader (fill-on-miss). An apply is the authoritative post-commit cache write done by
SharedDomains.Commit.
- A read view is the consistent view of state a read-only temporal tx holds — its MDBX read tx plus its pinned files view (cf.
BeginTemporalRo). Unrelated to snapshot (.seg) files.
- Every read view has an exact frontier: the exclusive txNum end of what it can see — a view with frontier N sees txNums < N.
Problem
Each read is consistent inside its own tx. The bug is between txs: a fill hands data from an older read view to the shared cache after execution has already applied newer state.
Reproduction (who does what)
- Reader (an RPC request or a read-ahead goroutine): begins a read-only tx. Its frontier is N.
- Exec: at txNum M >= N deletes key K and commits. The post-commit apply deletes K from the cache — the slot is now empty.
- Reader:
GetLatest(K) — misses the empty cache, falls back to its own older tx, gets the pre-delete value.
- Reader: its fill-on-miss (
PutIfAbsent) re-inserts that value. The slot is empty, so it lands.
- Anyone (exec included), on a fresh tx:
GetLatest(K) — cache hit. The deleted value is served as live.
Every reader whose tx predates the delete re-creates the problem if it touches K. Tests only make this interleaving deterministic by pausing the reader between step 3 and step 4.
Correctness requirement
Once canonical state has advanced beyond a reader's frontier, a fill from that reader must not make older state observable. In particular, a canonically deleted value must stay absent from later canonical reads.
The same requirement covers accounts, storage, code bindings, derived address→codeHash entries, and read-ahead fills. Checking freshness only when the reader or its view is created is not enough — an apply can land between the check and the fill. Admission must be checked at fill time, ordered against applies. The technical solution is #22444.
What
StateCacheisStateCacheis one process-global in-memory cache of the latest committed values of three domains: accounts, storage, code. Its job is to skip the file-accessor/MDBX stack on repeatedGetLatestreads. It is not a snapshot: for every key it holds one value — the newest one the process has applied.Who touches it, and when:
SharedDomains.Commit)GetLatestexecmodule.CacheView→SharedDomains.AsGetter)GetLatestlatestblocks_read_ahead)Vocabulary, used consistently below and in #22444:
SharedDomains.Commit.BeginTemporalRo). Unrelated to snapshot (.seg) files.Problem
Each read is consistent inside its own tx. The bug is between txs: a fill hands data from an older read view to the shared cache after execution has already applied newer state.
Reproduction (who does what)
GetLatest(K)— misses the empty cache, falls back to its own older tx, gets the pre-delete value.PutIfAbsent) re-inserts that value. The slot is empty, so it lands.GetLatest(K)— cache hit. The deleted value is served as live.Every reader whose tx predates the delete re-creates the problem if it touches K. Tests only make this interleaving deterministic by pausing the reader between step 3 and step 4.
Correctness requirement
Once canonical state has advanced beyond a reader's frontier, a fill from that reader must not make older state observable. In particular, a canonically deleted value must stay absent from later canonical reads.
The same requirement covers accounts, storage, code bindings, derived address→codeHash entries, and read-ahead fills. Checking freshness only when the reader or its view is created is not enough — an apply can land between the check and the fill. Admission must be checked at fill time, ordered against applies. The technical solution is #22444.