erigontech / erigontech/erigon
execution, db: bind StateCache and BranchCache to explicit state views
- Dominant language
- Go
- Stars
- 3.6k
- Forks
- 1.5k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 455
Description
## Problem
`StateCache` and `BranchCache` are long-lived caches of latest state, while their callers read through transactions and `SharedDomains` instances pinned to particular database and immutable-file views. Their current coherence models are different and neither makes snapshot identity a required part of every state-dependent cache read.
- `StateCache` has frontier, state-version, read-view epoch, and per-entry unwind checks. PR #23005 strengthened fill admission, but `StateCache.ReadView` explicitly does not isolate reads: an older view may still consume a latest-applied hit.
- `BranchCache` uses per-entry transaction numbers, epochs, and an unwind floor. Its consumer API carries no transaction-view identity, and `NewSharedDomains` attaches it automatically through `AggTx()` unless a caller opts out.
This leaves two unsafe directions for state-dependent data:
- An older transaction can consume an account, storage value, address-to-code binding, negative entry, or commitment branch published from a newer state view.
- A delayed read from an older transaction can publish stale data into the current cache generation.
It also leaves several correctness boundaries governed by different mechanisms: forward commit, failed commit, canonical and speculative unwind, reset, immutable-file publication, read-ahead, adaptive branch pinning, and published `SharedDomains` readers.
PR #22198 contains the known request-owned commitment call sites by disabling their shared `BranchCache`. That is a correct interim measure, but safety remains dependent on constructor and call-site discipline. Similar StateCache hazards and partial fixes are tracked by #22463, #23005, #23028, and #22120.
## Goal
Eliminate mixed-view shared-cache bugs by construction. Both caches must be derived optimizations over explicit durable state views, never unversioned sources of truth.
## Required invariant
> Each shared mutable state cache represents one exact durable state generation and one compatible view of its backing files. A caller may read or fill it only while its bound generation remains published.
`StateCache` and `BranchCache` should use the same publication protocol but retain separate generation tokens and backing-file identities. They do not need to become one cache.
The shared caches may remain latest-only. A caller pinned to an older generation must miss them and continue through its own transaction or an unshared cache belonging to that view.
## Proposed design
### Generation identities
Give each cache generation an immutable identity containing:
- the `PlainStateVersion` of the durable database state;
- the exclusive visible file ends relevant to that cache;
- an opaque publication token whose identity changes whenever existing views must be revoked, even if the numeric fields repeat.
The relevant file identities differ:
- `StateCache`: accounts, storage, and address-keyed code values-file ends, including any history-coverage requirement needed to prove a coherent latest view;
- `BranchCache`: the commitment values-file end.
State-dependent negative entries and derived address-to-code-hash bindings belong to the `StateCache` generation. Data proven content-addressed, such as code bytes keyed by code hash, may use a separate generation-independent API if its immutability is enforced and no state-dependent address binding crosses that boundary.
State-publication identities proposed by #22494 should carry the durable cache views on which the published `SharedDomains` is based. They should identify in-flight state publications without making the process-global notion of "latest" part of cache correctness.
### View-bound read and fill APIs
Consumers obtain `StateCacheView` and `BranchCacheView` handles from their pinned transaction. Raw state-dependent consumer access such as an unbound cache `Get` or unrestricted read-fill `Put` should not remain available.
- A hit compares the generation token before and after the lock-free lookup. A revoked or mismatched view returns a miss.
- A fill checks its generation under the admission gate immediately before mutation. Publication revocation waits for fills already admitted under the previous generation.
- A stale view cannot be renewed by constructing another wrapper around the same transaction.
- StateCache negative values, address-to-code-hash mappings, and address-keyed code entries use the same state-view checks as positive account and storage entries.
- Branch preload and adaptive pinning use the same generation-bound admission path; they are not separate unversioned writers.
### Coordinated publication authority
Only canonical execution receives publication authority. RPCs, payload builders, historical readers, speculative validation, tools, and warmup workers receive reader views or local caches, but no authority to publish a shared generation.
Canonical publication should have one durable protocol for both caches:
- Collect state, branch, and adaptive-pin changes without mutating the shared caches.
- Commit the database transaction.
- Revoke the previous cache generations and drain fills admitted under them.
- Apply authoritative cache changes.
- Publish separate `StateCache` and `BranchCache` generations that both match the committed `PlainStateVersion` and their respective files views.
- Leave the previous generations unchanged if the database commit fails.
The publication coordinator must prevent a `SharedDomains` getter from binding the two caches to different durable state versions. During a transition, a cache whose matching generation is not yet published returns misses.
A canonical unwind should initially use the conservative rule: after the database commit, clear `BranchCache` and the state-dependent `StateCache` layers, then publish the rewound generations. Selective retention is an optional measured optimization tracked by #23139, not part of the initial correctness proof.
Speculative or local unwinds must not mutate shared cache state. Reset and immutable-file publication must use the same generation-revocation protocol. Files reconciliation remains tracked by #23047.
### Ownership and construction
Cache capabilities should be explicit in constructors or represented by distinct canonical and snapshot-owned context types. `NewSharedDomains` should not discover and attach a process-global cache implicitly, and a reader attachment must not also grant publication authority.
The type/API boundary should make these states unrepresentable:
- a snapshot-owned context with canonical publication authority;
- a state-dependent cache view without a backing generation identity;
- one `SharedDomains` using `StateCache` and `BranchCache` views from different durable state versions;
- a parallel commitment worker whose reader is newer than its parent computation view.
Parallel workers must clone the exact caller view instead of opening fresh latest transactions, as tracked by #22209. Published `SharedDomains` consumers should use stable publication handles with defined lifetime, as tracked by #22494.
### View-local performance
When a long-lived or request-owned view loses access to a latest-only shared cache, it may retain an unshared memo for that view. This preserves repeated reads within calls, proofs, simulations, witness generation, block building, and other one-shot computations without adding cross-transaction coherence.
## Migration plan
- Extract or restore the small generation-token and admission-gate primitive from #23095, with separate instances for `StateCache` and `BranchCache`.
- Extend the current StateCache view from fill-bound admission to generation-bound reads and fills. Retain the existing epoch/frontier protections until equivalent generation tests are green, then remove overlapping mechanisms rather than keeping both indefinitely.
- Add `BranchCacheView` and branch publication capabilities around the current root, trunk, pinned, and tail storage tiers.
- Derive both views from a transaction's `PlainStateVersion` and pinned files metadata, and reject a getter whose two views do not describe one durable state.
- Route canonical commit, canonical unwind, reset, files publication, read-ahead, branch preload, and adaptive pin publication through the coordinated publisher API.
- Convert snapshot-owned callers and parallel workers to explicit reader views or view-local caches.
- Remove automatic cache attachment and raw state-dependent consumer `Get`/`Put` access once all callers are migrated.
- Keep #22198's explicit RPC commitment isolation until the central generation-bound API has deterministic end-to-end coverage.
The complete generation/publication design in the closed, unmerged PR #23095 is the primary blueprint. The implementation can be split into smaller reviewable changes, but the final API must preserve one coherent invariant rather than adding another independent epoch or call-site convention.
## Acceptance criteria
- Deterministic red-to-green tests cover both cross-view directions for StateCache and BranchCache:
- an old transaction cannot consume data published after its snapshot;
- an old read completing after canonical publication cannot fill the new generation.
- Tests use synchronization barriers rather than timing sleeps.
- One `SharedDomains` cannot bind cache views from different `PlainStateVersion` values.
- Failed commits leave the prior generations and cache contents usable.
- Abandoned speculative unwinds cannot read from or fill shared generations.
- Canonical unwind, reset, relevant file extension, and file-visibility lowering revoke incompatible views.
- StateCache positive values, negative values, derived code-hash bindings, address-keyed code, and any generation-independent content-addressed layer have explicit tested rules.
- BranchCache root, trunk, pinned, and tail entries, plus preload and adaptive pinning, obey the same generation rules.
- Parallel commitment workers read the caller's exact state view.
- Published-SD readers cannot outlive or silently switch their state publication.
- Race-detector coverage exercises reads and fills against commit, abort, unwind, reset, warmup, and files publication.
- Shared cache-hit paths remain lock-free and allocation-free. Benchmark view construction, steady-state hits, FCU publication, shallow-reorg rewarming, and view-local memoization.
## Non-goals
- Multi-version process-global StateCache or BranchCache storage.
- Selective unwind retention before measurements justify its larger correctness proof.
- Merging `StateCache` and `BranchCache`; they share an invariant and publication protocol, not storage or file identity.
- Treating state-dependent address mappings as content-addressed data.
- Removing endpoint isolation before the central invariant is enforced.
## Related work
- #23095 — complete generation-bound design for StateCache and BranchCache; closed without merge.
- #23005 and #22463 — merged StateCache fill-admission and unwind hardening; this does not make StateCache reads snapshot-isolated.
- #23047 and #23028 — immutable-files publication and cache reconciliation.
- #23139 — compares full clearing with selective unwind-retention designs and recommends generation gating as the base invariant.
- #22198 — interim isolation for request-owned RPC commitment reads.
- #22211 and #23253 — BranchCache read-fill discipline and staged-unwind fills.
- #22120 and #22116 — StateCache coherence review and warmup generation capture.
- #22209 and #23301 — parallel commitment workers and the builder's interim sequential mitigation.
- #22214 and #22494 — published-SD reader audit and explicit state-publication identities.
- #19623 and #23140 — reducing overlapping state-cache layers and ownership models.
Contributor guide
Assessment
This issue has not been assessed yet.