[FVM] Long-lived derived data (programs) cache can rot — add bounded entry lifetime and staleness detection
@janezpodhostnik is already working on this.
Since Jun 11, 2026.
- Dominant language
- Go
- Stars
- 575
- Forks
- 217
- Avg merge
- 4d 20h
- Merged PRs (30d)
- 9
Description
Problem
Parsed and checked Cadence contracts are kept in the derived data cache (fvm/storage/derived/), alongside derived execution parameters. The cache is intentionally long-lived since contracts change infrequently — but entry lifetime is effectively unbounded: on each new block, DerivedDataTable.NewChildTable copies all parent entries into the child table, reusing the same in-memory *runtime.Program objects. An entry survives until explicit invalidation or node restart — potentially the node's entire uptime.
Invalidation (fvm/environment/derived_data_invalidator.go) is the sole freshness mechanism. If it has a gap, or a long-lived in-memory object silently degrades, the cached data rots and FVM execution produces incorrect results — with no bound on how long the bad entry keeps being served.
The impact of a rotted entry differs by execution path:
- Block execution: VNs re-derive programs from scratch per chunk (
module/chunks/chunkVerifier.go), so a rotted EN cache should fail verification — a liveness incident rather than wrong sealed state, but still an incident. - Script execution: the EN query executor and the AN/ON program cache (
--program-cache-size, experimental) serve results directly to users with no verification safety net — stale results here are silent.
Findings from initial investigation
- Eviction is consensus-neutral. Cache hits replay the stored
ExecutionSnapshotonto the transaction state, so hit vs miss yields identical execution results (VNs already run every chunk cold and match warm-cache EN results). Dropping entries can only affect performance. - Naive per-entry random eviction is unsafe. Per the invariant documented in
fvm/storage/derived/table.go, the table must always return the same*Programobject for a key, because dependents embed Cadence types from that specific object. Eviction must drop dependency-closed sets — the semanticsProgramInvalidatoralready implements for contract updates. - Hashing for detection is feasible for inputs, partial for objects. Storing a hash of the (registerID, value) pairs read during derivation would let a sampled background checker re-read those registers against committed state — detecting invalidation gaps independently of the invalidator code. Hashing the cached object itself only covers the AST (canonical JSON); the Elaboration is a pointer graph with no canonical serialization.
Proposed direction
- Bounded lifetime via periodic full flush (recommended starting point): with small probability per block — or every N blocks / T hours — create the child
DerivedBlockDataempty instead of copying the parent. Trivially safe: identical to the routinely-exercised post-restart state; cost is a brief warm-up. - Input-hash staleness detection: store a hash of read register values per entry; background-sample and verify against committed state, invalidating (dependency-closed) and alerting on mismatch.
- Operational hygiene: an admin command to flush
DerivedChainDataon a live node, and a cache entry-age metric (today only hit/miss counters exist).
Relevant code
fvm/storage/derived/derived_chain_data.go— block-level LRUfvm/storage/derived/table.go—NewChildTablecarry-forward, OCC validation, object-identity invariantfvm/environment/derived_data_invalidator.go— invalidation rules (dependency-closed)fvm/storage/snapshot/execution_snapshot.go— per-entry derivation snapshot (ReadSethas no values today)module/chunks/chunkVerifier.go— VN cold-cache re-execution (the safety net for block execution)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.