Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5824a53
execution, db: state-cache review follow-ups; wire frozen-block catch…
yperbasis Aug 5, 2026
b05da15
execution/cache: name the nil-aggregator case in BindAggregator's assert
yperbasis Aug 5, 2026
5fa7d08
execution/cache, execution/execmodule: absorb snapshot publication in…
yperbasis Aug 5, 2026
75728a2
execution/execmodule: evict the code store on the catch-up prune path
yperbasis Aug 5, 2026
72bbd6b
execution/cache: count addr-codehash seed admission outcomes
yperbasis Aug 5, 2026
67177e0
execution/cache: keep the fill counters off the hot read line
yperbasis Aug 5, 2026
91ccda8
db/state/execctx: assert the apply-only miss path as a difference
yperbasis Aug 5, 2026
1890f34
execution/cache: assert only eviction-safe indices in the chunk-bound…
yperbasis Aug 5, 2026
9a48884
execution/cache: ApplyAll no longer rewrites the caller's slice
yperbasis Aug 5, 2026
8a48529
db/state/execctx: Flush on a cache-attached SD panics like the neighb…
yperbasis Aug 5, 2026
c1dd19e
execution/cache: count fill attempts dying on an inexact frontier
yperbasis Aug 5, 2026
00cec03
execution/cache: make AggregatorBound as nil-safe as BindAggregator
yperbasis Aug 5, 2026
5991d05
execution/commitment: add the missed BranchCache absorb unit test
yperbasis Aug 5, 2026
eb6a44f
Revert "execution/commitment: add the missed BranchCache absorb unit …
yperbasis Aug 5, 2026
9ca2938
execution/cache: cover the third counter in the layout test; cache-dr…
yperbasis Aug 5, 2026
f730805
execution/cache, execution/execmodule: three review nits
yperbasis Aug 5, 2026
fa71731
Merge remote-tracking branch 'origin/main' into yperbasis/statecache-…
yperbasis Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/integration/commands/stages.go
Original file line number Diff line number Diff line change
Expand Up @@ -851,9 +851,9 @@ func execBlocksBatch(ctx context.Context, db kv.TemporalRwDB, st *stagedsync.Syn
}
defer doms.Close()
doms.SetInMemHistoryReads(false)
stateCache.BindAggregator(db)
doms.SetStateCache(stateCache)
doms.SetCodeStore(codeStore)
execctx.GuardAggregatorForCache(db, stateCache)

s, err := st.StageState(stages.Execution, tx, initialCycle, false)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions db/kv/kv_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,9 @@ type TemporalRwDB interface {
BeginTemporalRw(ctx context.Context) (TemporalRwTx, error)
BeginTemporalRwNosync(ctx context.Context) (TemporalRwTx, error)
UpdateTemporal(ctx context.Context, f func(tx TemporalRwTx) error) error
// Agg returns the DB's state-files aggregator as `any` (the concrete type
// lives above the kv layer); nil when the DB has none.
Agg() any
}

// ---- non-important utilities
Expand Down
2 changes: 2 additions & 0 deletions db/kv/membatchwithdb/memory_mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,8 @@ func (td temporaldb) BeginTemporalRwNosync(ctx context.Context) (kv.TemporalRwTx
return td.memoryMutation, nil
}

func (td temporaldb) Agg() any { return nil }

func (td temporaldb) Debug() kv.TemporalDebugDB {
panic("not implemented")
}
Expand Down
2 changes: 1 addition & 1 deletion db/seg/decompress.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ type Decompressor struct {
readAheadRefcnt atomic.Int32 // ref-counter: allow enable/disable read-ahead from goroutines. only when refcnt=0 - disable read-ahead once

residency atomic.Pointer[residencyBitmap] // page-residency bitmap for the async-io gate; nil unless enabled
residencyOnce sync.Once
residencyOnce sync.Once //nolint:unused // Used by the Linux residency gate.
}

const (
Expand Down
68 changes: 26 additions & 42 deletions db/state/execctx/domain_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func (m *domainVisibleEndMemo) load(tx kv.TemporalTx, domain kv.Domain, viewID u
state = 0
m.viewID.Store(viewID)
}
end, ok := tx.Debug().DomainVisibleEnd(domain)
end, ok := debugDomainVisibleEnd(tx, domain)
m.ends[domain].Store(end)
state |= loadedBit
if ok {
Expand All @@ -159,7 +159,17 @@ func (sd *SharedDomains) domainVisibleEnd(tx kv.TemporalTx, domain kv.Domain) (u
if _, ok := tx.(kv.TemporalRwTx); ok {
return sd.visibleEnds.get(tx, domain)
}
return tx.Debug().DomainVisibleEnd(domain)
return debugDomainVisibleEnd(tx, domain)
}

// debugDomainVisibleEnd tolerates txs without a debug backend (MemoryMutation
// over a nil db): no exact frontier means no fills, reads still work.
func debugDomainVisibleEnd(tx kv.TemporalTx, domain kv.Domain) (uint64, bool) {
dbgTx := tx.Debug()
if dbgTx == nil {
return 0, false
}
return dbgTx.DomainVisibleEnd(domain)
}

// sdFrontier adapts one (SharedDomains, tx) pair to cache.Frontier: writable
Expand Down Expand Up @@ -934,6 +944,9 @@ func (sd *SharedDomains) SetStateCache(stateCache *cache.StateCache) {
if !dbg.UseStateCache || stateCache == nil {
return
}
if stateCache.FillsEnabled() && !stateCache.AggregatorBound() {
panic("assert: fill-enabled StateCache wired before BindAggregator — the visibility-lowering guard is not bound")
}
sd.BindStateCache(stateCache)
}

Expand All @@ -946,29 +959,6 @@ func (sd *SharedDomains) BindStateCache(stateCache *cache.StateCache) {
sd.cacheApplier.Initialize(sd.baseStateVersion)
}

// GuardAggregatorForCache forbids visibility lowering on db's aggregator when
// sc is a fill-enabled StateCache: fill admission relies on view frontiers
// never decreasing. This is the one place that binds the invariant — call it
// wherever a fill-enabled cache is wired over a DB. Duck-typed so the storage
// layer need not know the cache type (and vice versa) — but load-bearing, so
// a db that cannot produce its aggregator fails loudly instead of silently
// dropping the guard. A nil or apply-only cache needs no guard.
func GuardAggregatorForCache(db any, sc *cache.StateCache) {
if sc == nil || !sc.FillsEnabled() {
return
}
h, ok := db.(interface{ Agg() any })
if !ok {
panic(fmt.Sprintf("assert: fill-enabled StateCache wired over %T, which cannot produce its aggregator — the visibility-lowering guard would be silently dropped", db))
}
agg := h.Agg()
f, ok := agg.(interface{ ForbidVisibilityLowering() })
if !ok {
panic(fmt.Sprintf("assert: aggregator %T lacks ForbidVisibilityLowering — the visibility-lowering guard would be silently dropped", agg))
}
f.ForbidVisibilityLowering()
}

// SetCodeStore sets the persistent codehash-keyed code cache.
func (sd *SharedDomains) SetCodeStore(codeStore *cache.CodeStore) {
sd.codeStore = codeStore
Expand Down Expand Up @@ -1070,22 +1060,15 @@ func (sd *SharedDomains) Close() {
// admission.

// Flush writes the in-memory batch into tx without committing. It deliberately
// does NOT touch the caches: plain Flush leaves the commit to the caller (who
// may still roll back), so it must not warm a cache with state that could be
// rolled back. Cache entries are populated elsewhere — by Commit after a
// successful commit, and by reads (GetLatest) — each stamped with a
// conservative upper-bound txNum. It is that txNum stamp, not population
// timing, that keeps the cache correct: an unwind lowers the floor so every
// entry reflecting a now-dead fork is evicted, and mem-first masking means a
// later in-memory write shadows a stale cached read.
//
// An SD with an attached state cache must route every flush through Commit:
// Flush neither applies nor invalidates, so a populated cache would keep
// serving pre-flush values for the flushed keys after the caller's own
// commit — and Commit collects its cache updates only from its own flush, so
// an earlier plain Flush's keys would never be applied. Cache-less callers
// may Flush and commit themselves.
// does not touch the caches — the caller may still roll back. An SD with a
// state cache must route every flush through Commit: a plain Flush would
// leave the cache serving pre-flush values for the flushed keys forever, so
// it panics here, like the SetStateCache assert for the neighbouring wiring
// bug — an error return can be swallowed.
func (sd *SharedDomains) Flush(ctx context.Context, tx kv.RwTx) error {
if sd.stateCache != nil {
panic("assert: SharedDomains with a state cache must flush through Commit")
}
defer mxFlushTook.ObserveDuration(time.Now())
return sd.flushMem(ctx, tx)
}
Expand Down Expand Up @@ -1513,7 +1496,8 @@ func (sd *SharedDomains) getLatestMetered(domain kv.Domain, tx kv.TemporalTx, k
}

// A bounded read observes a staged unwind, not stable committed state.
if maxStep == kv.NoStepBound && sd.stateCache != nil && sd.stateCache.Caches(domain) {
// Apply-only mode skips frontier binding for fills that cannot happen.
if maxStep == kv.NoStepBound && sd.stateCache != nil && sd.stateCache.FillsEnabled() && sd.stateCache.Caches(domain) {
readTxNum := (uint64(step)+1)*sd.StepSize() - 1
fillView := view
if fillView.NeedsFrontier() {
Expand Down Expand Up @@ -1752,7 +1736,7 @@ func (sd *SharedDomains) codeHashForAddr(tx kv.TemporalTx, view cache.ReadView,
}

h, fromReadView := resolve()
if fromReadView && sd.stateCache != nil {
if fromReadView && sd.stateCache != nil && sd.stateCache.FillsEnabled() {
var fixed [32]byte
if len(h) == 32 {
copy(fixed[:], h)
Expand Down
Loading
Loading