Skip to content

execution/cache: stale read-view fills can resurrect deleted state #22356

Description

@yperbasis

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.CacheViewSharedDomains.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)

  1. Reader (an RPC request or a read-ahead goroutine): begins a read-only tx. Its frontier is N.
  2. Exec: at txNum M >= N deletes key K and commits. The post-commit apply deletes K from the cache — the slot is now empty.
  3. Reader: GetLatest(K) — misses the empty cache, falls back to its own older tx, gets the pre-delete value.
  4. Reader: its fill-on-miss (PutIfAbsent) re-inserts that value. The slot is empty, so it lands.
  5. 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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions