Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
ec77c91
execution/cache, db/state/execctx: linearize snapshot read fills
yperbasis Jul 14, 2026
f99e10e
execution/cache, db/state: use exclusive snapshot frontiers
yperbasis Jul 15, 2026
421d0d3
execution/cache: partition admission locks by coherence group
yperbasis Jul 15, 2026
dc3ab95
db/state: deduplicate inverted index progress lookup
yperbasis Jul 15, 2026
a4f833d
Merge remote-tracking branch 'origin/main' into test/statecache-delet…
yperbasis Jul 15, 2026
4a38f91
execution/cache: derive cached code and its hash from the same copy i…
yperbasis Jul 15, 2026
cb2cdde
execution/cache: drop derived addr→codeHash on code deletion in Apply
yperbasis Jul 15, 2026
fdf367f
execution/cache: pin account-delete → stale code-fill rejection
yperbasis Jul 15, 2026
c557351
execution/cache: drop stale call-site inventory from PutAddrCodeHash …
yperbasis Jul 15, 2026
436ae1c
db, execution: collapse DomainProgressAndVisibleEnd to DomainVisibleEnd
yperbasis Jul 15, 2026
e976b63
execution/cache: unify admission into a single RWMutex
yperbasis Jul 15, 2026
82ee60d
execution/cache: avoid advancing code frontier on account delete
yperbasis Jul 16, 2026
1ff29c1
exec, execctx: fix negative cache unwind stamp
yperbasis Jul 16, 2026
616b269
Merge remote-tracking branch 'origin/main' into test/statecache-delet…
yperbasis Jul 16, 2026
fd1c8d1
db, execution: align cache frontier terminology
yperbasis Jul 16, 2026
87ddb96
db/kv/temporal: cache domain visible ends
yperbasis Jul 16, 2026
e1eb581
db/kv/temporal: compact visible end cache
yperbasis Jul 16, 2026
4ff164e
execution/cache, exec, execctx: centralize snapshot fills
yperbasis Jul 17, 2026
576b787
execution/cache: remove unreachable commitment guard
yperbasis Jul 17, 2026
7b4efea
db/state: simplify inverted index progress lookup
yperbasis Jul 17, 2026
60eecc1
execution/exec: remove redundant read-ahead temporary
yperbasis Jul 17, 2026
748f6f8
execution/exec: simplify read-ahead frontier check
yperbasis Jul 17, 2026
14d5f7a
execution/cache: inline address code hash fill
yperbasis Jul 17, 2026
d662dcd
execution/cache: reuse resolved cache for fills
yperbasis Jul 17, 2026
9fba079
execution/cache: make code fill modes explicit
yperbasis Jul 17, 2026
04e35ec
execution/exec: centralize read-ahead getter selection
yperbasis Jul 17, 2026
70ffa64
execution/exec: inline read-ahead getter construction
yperbasis Jul 17, 2026
ba58d75
execution/cache: reset applied frontiers directly
yperbasis Jul 17, 2026
33af7de
execution/cache: clamp applied frontiers on unwind
yperbasis Jul 17, 2026
28e36cf
execution/exec: embed read-ahead temporal getter
yperbasis Jul 17, 2026
0f1b26e
execution/exec: drop redundant warmup cache check
yperbasis Jul 17, 2026
57fbc1a
execution/cache: reuse domain delete path
yperbasis Jul 17, 2026
ca3f6ea
execution/exec: remove impossible frontier state
yperbasis Jul 17, 2026
6b784ea
Merge remote-tracking branch 'origin/main' into test/statecache-delet…
yperbasis Jul 17, 2026
bb25293
db/state/execctx: seed addr→codeHash mapping only from snapshot-sourc…
yperbasis Jul 17, 2026
d2ffe0f
db/kv/temporal: re-derive memoized domain frontiers on files-tx reopen
yperbasis Jul 17, 2026
446621d
db/kv/temporal: static-assert the visible-ends bitmask capacity
yperbasis Jul 17, 2026
653f289
db/kv/temporal: race-test the DomainVisibleEnd memo fast path
yperbasis Jul 17, 2026
f98dc13
execution/cache: inline StateCache.Put's sole remaining insert path
yperbasis Jul 17, 2026
6619446
db/state/execctx: run codehash routing tests in short mode
yperbasis Jul 17, 2026
25934ef
db, execution: rename snapshot to read view in fill admission
yperbasis Jul 28, 2026
cc3066b
Merge remote-tracking branch 'origin/main' into test/statecache-delet…
yperbasis Jul 28, 2026
3f7945f
execution/cache, db/kv: clarify fill-admission comments
yperbasis Jul 28, 2026
588b5f7
db/state/execctx: cache writable domain visible ends
yperbasis Jul 28, 2026
f53ee44
db/state/execctx: reset visible-end memo on unwind
yperbasis Jul 28, 2026
4004aaa
db/state/execctx: make writable visible-end memo concurrency-safe
yperbasis Jul 28, 2026
ba5263b
db/state/execctx: drop redundant unwind memo reset
yperbasis Jul 28, 2026
02d2eaa
db/state/execctx: keep visible-end memo coherent across views
yperbasis Jul 28, 2026
dcff899
db/kv/temporal: make memoized visible ends atomic
yperbasis Jul 28, 2026
58db5ab
Merge branch 'main' into test/statecache-delete-rpc-repro
yperbasis Jul 28, 2026
0d4ae69
Merge branch 'main' into test/statecache-delete-rpc-repro
yperbasis Aug 4, 2026
576b086
execution/cache, db/state/execctx, execution/exec: access StateCache …
yperbasis Aug 4, 2026
6ace158
execution/cache: package doc with the StateCache contract
yperbasis Aug 4, 2026
cc96203
execution/execmodule, db/state/execctx: build the RPC CacheView gette…
yperbasis Aug 4, 2026
30f2ac2
execution/cache: STATE_CACHE_FILLS switch for apply-only mode
yperbasis Aug 4, 2026
d34d775
execution/cache: keep the admission frontier across Clear
yperbasis Aug 4, 2026
91ccbe5
execution/cache, execution/execmodule: apply-only switch covers every…
yperbasis Aug 4, 2026
f3d8b0d
db/kv/temporal, db/state/execctx: name the visible-end memo bits
yperbasis Aug 4, 2026
15756de
execution/execmodule: drain read-ahead only before an actual unwind
yperbasis Aug 4, 2026
610e3e6
execution/execmodule: keep the FCU-entry drain, correct its comment
yperbasis Aug 4, 2026
e50f801
execution/cache: gate code fills on the accounts frontier too
yperbasis Aug 4, 2026
706b091
execution/cache: failing test for the unwind fill-readmission window
AskAlexSharov Aug 4, 2026
055812b
db/kv: document DomainProgress as best-effort, DomainVisibleEnd as exact
yperbasis Aug 4, 2026
b059321
Revert "execution/cache: failing test for the unwind fill-readmission…
yperbasis Aug 4, 2026
a3cc38c
execution/cache, execution/execmodule, db/state: thread the getter's …
yperbasis Aug 4, 2026
a0bd162
execution/cache, db/kv/temporal: state the frontier monotonicity inva…
yperbasis Aug 4, 2026
e3279d2
db/state, execution/cache, common/dbg: enforce the no-cache-with-visi…
yperbasis Aug 4, 2026
f8c965f
db/state, execution/cache, common/dbg: narrow and complete the visibi…
yperbasis Aug 4, 2026
d426502
db/state/execctx: correct the Flush cache contract
yperbasis Aug 4, 2026
2b9c831
execution/cache, execution/execmodule, db: comment precision pass
yperbasis Aug 4, 2026
07d03c7
db/state, execution/cache, execution/execmodule, cmd/integration: gua…
yperbasis Aug 4, 2026
e8a1422
db/state: gofmt aggregator.go
yperbasis Aug 4, 2026
4523761
common/dbg: restore setter block formatting
yperbasis Aug 4, 2026
4ab8a36
db/state/execctx, execution/execmodule, cmd/integration: one home for…
yperbasis Aug 4, 2026
bb75019
db/state/execctx: GuardAggregatorForCache mirrors SetStateCache's gate
yperbasis Aug 4, 2026
25c1e91
db/state/execctx, execution/execmodule: fail closed on guard shape mi…
yperbasis Aug 4, 2026
982cc8e
execution/execmodule, node/eth: module owns state-cache construction;…
yperbasis Aug 4, 2026
756cd2b
node/eth, execution/cache: release the exec module's cache on Stop
yperbasis Aug 4, 2026
7af4d0f
execution/cache, db/state: shorten state-cache lock windows; teardown…
yperbasis Aug 5, 2026
87321bb
execution, db/state/execctx: pin budget release on node close; post-c…
yperbasis Aug 5, 2026
a880acc
execution/tests: remove accidentally committed legacy-tests gitlink
yperbasis Aug 5, 2026
6aa368e
execution/engineapi: close the tester on assert failure in the budget…
yperbasis Aug 5, 2026
f3cb104
db/state: frontier never overstates values coverage; guard the histor…
yperbasis Aug 5, 2026
3adae16
db/state: drop the history-II segment in memory in the lowering test
yperbasis Aug 5, 2026
55bc6e6
db/state: a dependency-clamped values view has no exact frontier
yperbasis Aug 5, 2026
ae0a59c
execution/cache: qualify the Frontier stale-low contract
yperbasis Aug 5, 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
1 change: 1 addition & 0 deletions cmd/integration/commands/stages.go
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ func execBlocksBatch(ctx context.Context, db kv.TemporalRwDB, st *stagedsync.Syn
doms.SetInMemHistoryReads(false)
doms.SetStateCache(stateCache)
doms.SetCodeStore(codeStore)
execctx.GuardAggregatorForCache(db, stateCache)
Comment thread
yperbasis marked this conversation as resolved.

s, err := st.StageState(stages.Execution, tx, initialCycle, false)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions db/kv/kv_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,14 @@ type TemporalDebugTx interface {
// HistoryStartFrom return the earliest known txnum in history of a given domain
HistoryStartFrom(domainName Domain) uint64

// DomainProgress is a best-effort progress number for reporting: it mixes
// an exclusive files end with an inclusive DB txNum (so it is ±1 depending
// on which side wins) and falls back to step granularity when history is
// disabled. For an exact bound use DomainVisibleEnd.
DomainProgress(domain Domain) (txNum uint64)
// DomainVisibleEnd returns the exact exclusive txNum bound of the tx's
// domain read view. ok is false when the backend cannot provide an exact bound.
DomainVisibleEnd(domain Domain) (visibleEnd uint64, ok bool)
IIProgress(name InvertedIdx) (txNum uint64)
StepSize() uint64
// Retire retires frozen history files entirely below their
Expand Down
3 changes: 3 additions & 0 deletions db/kv/remotedb/kv_remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ func (tx *tx) Retire(ctx context.Context, cutoffs kv.RetireCutoffs) (int, error)
}
func (tx *tx) DomainFiles(domain ...kv.Domain) kv.VisibleFiles { panic("not implemented") }
func (tx *tx) DomainProgress(domain kv.Domain) uint64 { panic("not implemented") }
func (tx *tx) DomainVisibleEnd(domain kv.Domain) (uint64, bool) {
return 0, false
}
func (tx *tx) GetLatestFromDB(domain kv.Domain, k []byte) (v []byte, step kv.Step, found bool, err error) {
panic("not implemented")
}
Expand Down
72 changes: 72 additions & 0 deletions db/kv/temporal/kv_temporal.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"sync"
"sync/atomic"
"time"

"github.com/erigontech/erigon/db/datadir"
Expand Down Expand Up @@ -267,13 +268,71 @@ type tx struct {
type Tx struct {
kv.Tx
tx
visibleEnds domainVisibleEnds
}

type RwTx struct {
kv.RwTx
tx
}

type domainVisibleEnds struct {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why Cache need to know files progress?
GetLatest can read data from "db+files" - does not matter where is it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not files progress — it is this tx's own bound. The read view is two parts (pinned files + MDBX snapshot), so the exclusive txNum bound of what the tx can see is the max of the two ends; admission only compares that bound against appliedEnd, never caring where the bytes live. The files half is required because the DB half alone understates it: on a freshly snapshot-synced datadir the history tables are empty and the files carry the whole bound — a DB-only number would be ~0 and every fill from such a view would be rejected forever. And your "GetLatest reads db+files, does not matter where" is exactly why the max is stable: moving data db→files changes neither the bound nor the comparison.

// ends is atomic so a lock-free read can overlap a reset-and-reload of
// the same slot without a data race. A torn read (state bit from one
// generation, end from another) can only be stale-low, which merely
// over-rejects fills: a view's frontier never decreases in a process that
// fills a cache — the DB component is frozen at tx begin, and a files
// reopen only extends it, an invariant the aggregator enforces once a
// fill-enabled cache is wired over it (ForbidVisibilityLowering).
ends [kv.DomainLen]atomic.Uint64
mu sync.Mutex
state atomic.Uint32
}

// state packs two bits per domain into one word so a single atomic load
// returns a consistent (loaded, ok) pair: loadedBit says ends[domain] is
// memoized, okBit is the memoized ok answer of DomainVisibleEnd. The array
// size asserts at compile time that both halves fit in uint32.
var _ [32 - 2*int(kv.DomainLen)]struct{}

func visibleEndBits(domain kv.Domain) (loadedBit, okBit uint32) {
loadedBit = uint32(1) << uint32(domain)
return loadedBit, loadedBit << uint32(kv.DomainLen)
}

func (v *domainVisibleEnds) get(tx *Tx, domain kv.Domain) (uint64, bool) {
loadedBit, okBit := visibleEndBits(domain)
state := v.state.Load()
if state&loadedBit != 0 {
return v.ends[domain].Load(), state&okBit != 0
}
return v.load(tx, domain, loadedBit, okBit)
}

func (v *domainVisibleEnds) load(tx *Tx, domain kv.Domain, loadedBit, okBit uint32) (uint64, bool) {
v.mu.Lock()
defer v.mu.Unlock()

state := v.state.Load()
if state&loadedBit == 0 {
end, ok := tx.aggtx.DomainVisibleEnd(domain, tx.Tx)
v.ends[domain].Store(end)
state |= loadedBit
if ok {
state |= okBit
}
v.state.Store(state)
}
return v.ends[domain].Load(), state&okBit != 0
}

// reset takes mu so an in-flight load can't re-store pre-reset bits.
func (v *domainVisibleEnds) reset() {
v.mu.Lock()
defer v.mu.Unlock()
v.state.Store(0)
}

func (tx *tx) ForceReopenUnderlyingFilesTx() {
if tx.blocktx != nil {
tx.blocktx.Close()
Expand All @@ -284,6 +343,13 @@ func (tx *tx) ForceReopenUnderlyingFilesTx() {
}
tx.aggtx = tx.Agg().BeginFilesRo()
}

// ForceReopenUnderlyingFilesTx swaps in a fresh files view, which can extend
// the visible frontier — drop the memoized ends so they are re-derived.
func (tx *Tx) ForceReopenUnderlyingFilesTx() {
tx.tx.ForceReopenUnderlyingFilesTx()
tx.visibleEnds.reset()
}
func (tx *tx) FreezeInfo() kv.FreezeInfo { return tx.aggtx }

func (tx *tx) AggTx() any { return tx.aggtx }
Expand Down Expand Up @@ -724,6 +790,12 @@ func (tx *Tx) DomainProgress(domain kv.Domain) uint64 {
func (tx *RwTx) DomainProgress(domain kv.Domain) uint64 {
return tx.aggtx.DomainProgress(domain, tx.RwTx)
}
func (tx *Tx) DomainVisibleEnd(domain kv.Domain) (uint64, bool) {
return tx.visibleEnds.get(tx, domain)
}
func (tx *RwTx) DomainVisibleEnd(domain kv.Domain) (uint64, bool) {
return tx.aggtx.DomainVisibleEnd(domain, tx.RwTx)
}
func (tx *Tx) IIProgress(domain kv.InvertedIdx) uint64 {
return tx.aggtx.IIProgress(domain, tx.Tx)
}
Expand Down
127 changes: 127 additions & 0 deletions db/kv/temporal/kv_temporal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package temporal

import (
"encoding/binary"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -257,6 +258,132 @@ func TestTemporalTx_PinsBlockFilesView(t *testing.T) {
require.NotNil(t, roTx2.(*Tx).blocktx)
}

// DomainVisibleEnd's memo serves repeat readers lock-free while first loads
// run under the memo mutex. Fresh txs each round make the two paths
// interleave across goroutines; results must stay stable (run with -race).
func TestTemporalTx_DomainVisibleEndConcurrent(t *testing.T) {
t.Parallel()
ctx := t.Context()

mdbxDb := memdb.NewTestDB(t, dbcfg.ChainDB)
dirs := datadir.New(t.TempDir())
agg := state.NewTest(dirs).StepSize(1).MustOpen(ctx, mdbxDb)
defer agg.Close()
temporalDb, err := New(mdbxDb, agg, nil)
require.NoError(t, err)
defer temporalDb.Close()

acc := common.HexToAddress("0x1234567890123456789012345678901234567890")
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
storageK := append(append([]byte{}, acc[:]...), slot[:]...)

rwTtx, err := temporalDb.BeginTemporalRw(ctx)
require.NoError(t, err)
defer rwTtx.Rollback()
sd, err := execctx.NewSharedDomains(ctx, rwTtx, log.Root())
require.NoError(t, err)
defer sd.Close()
require.NoError(t, sd.DomainPut(kv.StorageDomain, rwTtx, storageK, []byte{1}, 1, nil))
require.NoError(t, sd.Flush(ctx, rwTtx))
require.NoError(t, rwTtx.Commit())

var expectedEnd [kv.DomainLen]uint64
var expectedOk [kv.DomainLen]bool
baseTtx, err := temporalDb.BeginTemporalRo(ctx)
require.NoError(t, err)
defer baseTtx.Rollback()
for d := range kv.DomainLen {
expectedEnd[d], expectedOk[d] = baseTtx.Debug().DomainVisibleEnd(d)
}
baseTtx.Rollback()
require.Equal(t, uint64(2), expectedEnd[kv.StorageDomain])
require.True(t, expectedOk[kv.StorageDomain])

for range 25 {
require.NoError(t, temporalDb.ViewTemporal(ctx, func(roTtx kv.TemporalTx) error {
var wg sync.WaitGroup
for range 8 {
wg.Go(func() {
for range 4 {
for d := range kv.DomainLen {
end, ok := roTtx.Debug().DomainVisibleEnd(d)
if end != expectedEnd[d] || ok != expectedOk[d] {
t.Errorf("domain %v: got (%d, %t), want (%d, %t)", d, end, ok, expectedEnd[d], expectedOk[d])
}
}
}
})
}
wg.Wait()
Comment thread
yperbasis marked this conversation as resolved.
return nil
}))
}
}

// A read-only temporal tx memoizes DomainVisibleEnd, while
// ForceReopenUnderlyingFilesTx swaps in a fresh files view that can extend the
// frontier — the memo must be re-derived after the swap.
func TestTemporalTx_ForceReopenRefreshesDomainVisibleEnd(t *testing.T) {
t.Parallel()
ctx := t.Context()

mdbxDb := memdb.NewTestDB(t, dbcfg.ChainDB)
dirs := datadir.New(t.TempDir())
agg := state.NewTest(dirs).StepSize(1).MustOpen(ctx, mdbxDb)
defer agg.Close()
temporalDb, err := New(mdbxDb, agg, nil)
require.NoError(t, err)
defer temporalDb.Close()

acc := common.HexToAddress("0x1234567890123456789012345678901234567890")
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
storageK := append(append([]byte{}, acc[:]...), slot[:]...)

rwTtx1, err := temporalDb.BeginTemporalRw(ctx)
require.NoError(t, err)
defer rwTtx1.Rollback()
sd, err := execctx.NewSharedDomains(ctx, rwTtx1, log.Root())
require.NoError(t, err)
defer sd.Close()
require.NoError(t, sd.DomainPut(kv.StorageDomain, rwTtx1, storageK, []byte{1}, 1, nil))
require.NoError(t, sd.Flush(ctx, rwTtx1))
require.NoError(t, rwTtx1.Commit())

roTtx, err := temporalDb.BeginTemporalRo(ctx)
require.NoError(t, err)
defer roTtx.Rollback()
end, ok := roTtx.Debug().DomainVisibleEnd(kv.StorageDomain)
require.True(t, ok)
require.Equal(t, uint64(2), end)

// Write past the RO tx's MVCC view and move the data into files, which are
// visible regardless of the DB read view.
for txNum := uint64(2); txNum <= 3; txNum++ {
rwTtx, err := temporalDb.BeginTemporalRw(ctx)
require.NoError(t, err)
defer rwTtx.Rollback()
require.NoError(t, sd.DomainPut(kv.StorageDomain, rwTtx, storageK, []byte{byte(txNum)}, txNum, nil))
require.NoError(t, sd.Flush(ctx, rwTtx))
require.NoError(t, rwTtx.Commit())
}
require.NoError(t, agg.BuildFiles(3))

freshRoTtx, err := temporalDb.BeginTemporalRo(ctx)
require.NoError(t, err)
defer freshRoTtx.Rollback()
filesEnd := freshRoTtx.Debug().TxNumsInFiles(kv.StorageDomain)
require.Greater(t, filesEnd, uint64(2), "the new files must extend past the memoized frontier")

end, ok = roTtx.Debug().DomainVisibleEnd(kv.StorageDomain)
require.True(t, ok)
require.Equal(t, uint64(2), end, "the pinned files view cannot see the new files before reopen")

roTtx.(*Tx).ForceReopenUnderlyingFilesTx()
end, ok = roTtx.Debug().DomainVisibleEnd(kv.StorageDomain)
require.True(t, ok)
require.Equal(t, filesEnd, end, "the frontier must reflect the fresh files view after reopen")
}

func TestTemporalTx_RangeAsOf_StorageDomain(t *testing.T) {
t.Parallel()
ctx := t.Context()
Expand Down
63 changes: 60 additions & 3 deletions db/state/aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,15 @@ type Aggregator struct {
oldestVisible *aggregatorVisible
// unaligned entities are left out of the shared visible-file ceiling while tooling
// regenerates them. Guarded by dirtyFilesLock.
unalignedDomain [kv.DomainLen]bool
unalignedIdx [kv.StandaloneIdxLen]bool
snapshotBuildSema *semaphore.Weighted
unalignedDomain [kv.DomainLen]bool
unalignedIdx [kv.StandaloneIdxLen]bool
// visibilityLoweringForbidden: a fill-enabled StateCache is wired over
// this aggregator, and its fill admission relies on view frontiers never
// decreasing. recalcVisibleFiles refuses to lower the cached state
// domains' visible ends while set; Close clears it (shutdown is not a
// fill window).
visibilityLoweringForbidden atomic.Bool
snapshotBuildSema *semaphore.Weighted

disableHistory bool
branchCacheDisabled bool
Expand Down Expand Up @@ -542,6 +548,17 @@ func (a *Aggregator) UnalignIdx(name kv.InvertedIdx) (realign func()) {
return func() {}
}

// ForbidVisibilityLowering marks this aggregator as backing a fill-enabled
// StateCache: from then on recalcVisibleFiles panics instead of lowering a
// cached state domain's visible end, whichever entry point caused it.
// Serialized with recalcVisibleFiles via dirtyFilesLock so "from then on"
// holds against a recalculation already in flight.
func (a *Aggregator) ForbidVisibilityLowering() {
a.dirtyFilesLock.Lock()
defer a.dirtyFilesLock.Unlock()
a.visibilityLoweringForbidden.Store(true)
}

func (a *Aggregator) setUnalignedDomain(d kv.Domain, v bool) {
a.dirtyFilesLock.Lock()
defer a.dirtyFilesLock.Unlock()
Expand Down Expand Up @@ -683,6 +700,9 @@ func (a *Aggregator) WaitForFiles() {
}

func (a *Aggregator) Close() {
a.dirtyFilesLock.Lock()
a.visibilityLoweringForbidden.Store(false) // shutdown is not a fill window
a.dirtyFilesLock.Unlock()
a.WaitForFiles()
if !a.background.BeginClose() { // idempotent: safe to call Close multiple times
return
Expand Down Expand Up @@ -1852,6 +1872,28 @@ func (a *Aggregator) recalcVisibleFiles(retired retiredFiles) {
}
next.minimaxTxNum = next.stateMinimaxTxNum()

if a.visibilityLoweringForbidden.Load() {
prev := a.visible.Load()
for _, d := range []kv.Domain{kv.AccountsDomain, kv.StorageDomain, kv.CodeDomain} {
if prev.d[d] == nil || next.d[d] == nil {
continue
}
prevEnd := visibleFiles(prev.d[d].files).EndTxNum()
nextEnd := visibleFiles(next.d[d].files).EndTxNum()
if nextEnd < prevEnd {
panic(fmt.Sprintf("assert: %s visible end lowered %d -> %d while a fill-enabled StateCache is wired — fill admission relies on view frontiers never decreasing", d, prevEnd, nextEnd))
}
if prev.dhii[d] == nil || next.dhii[d] == nil {
continue
}
prevII := prev.dhii[d].files.EndTxNum()
nextII := next.dhii[d].files.EndTxNum()
if nextII < prevII {
panic(fmt.Sprintf("assert: %s history-II visible end lowered %d -> %d while a fill-enabled StateCache is wired — DomainVisibleEnd derives view frontiers from it", d, prevII, nextII))
}
}
}

old := a.visible.Load()
old.retired = retired
old.next = next
Expand Down Expand Up @@ -2596,6 +2638,21 @@ func (at *AggregatorRoTx) DomainProgress(name kv.Domain, tx kv.Tx) uint64 {
}
return at.d[name].ht.iit.Progress(tx)
}
func (at *AggregatorRoTx) DomainVisibleEnd(name kv.Domain, tx kv.Tx) (uint64, bool) {
d := at.d[name]
if d.d.HistoryDisabled {
return 0, false
}
// A dependency checker can clamp the values view below the history-II end.
// Such a view has no exact frontier: reads mix fresh DB-resident keys with
// older file values for the gap, and raising the dependent file's
// visibility later reveals state without any cache apply — a fill made
// during the clamp would never be invalidated.
if d.files.EndTxNum() < d.ht.iit.files.EndTxNum() {
return 0, false
}
return d.ht.iit.visibleEnd(tx), true
}
func (at *AggregatorRoTx) IIProgress(name kv.InvertedIdx, tx kv.Tx) uint64 {
return at.searchII(name).Progress(tx)
}
Expand Down
Loading
Loading