From 52f99c8b93d4b1cdcf2259c5d8fec3c9558407dd Mon Sep 17 00:00:00 2001 From: sudeepdino008 Date: Mon, 10 Aug 2026 10:23:27 +0200 Subject: [PATCH 1/5] execution/state: make BlockStateCache committed storage reads lock-free committedStorage is a write-once, immutable pre-block view (like committedAccounts, already a sync.Map) but sat behind the shared BlockStateCache.mu: read under RLock and filled under a full Lock on every SLOAD first-touch, contending with the exec-loop's writeLog appends. Convert it to a sync.Map keyed by {addr,key}. GetCurrentStorage keeps the committed read under RLock so current+committed stay an atomic snapshot. benchstat (16-core, n=8), GetCommittedStorage/PutCommittedStorage: read_warm-16 47.8n -> 3.6n -92% fill_read-16 48.7n -> 2.8n -94% Uncontended (1 cpu) is ~25-50% slower (sync.Map overhead), but this cache is only used by the parallel executor, i.e. always under 8-16 way concurrency. --- .../block_cache_committed_storage_test.go | 147 ++++++++++++++++++ execution/state/rw_v3.go | 50 +++--- 2 files changed, 167 insertions(+), 30 deletions(-) create mode 100644 execution/state/block_cache_committed_storage_test.go diff --git a/execution/state/block_cache_committed_storage_test.go b/execution/state/block_cache_committed_storage_test.go new file mode 100644 index 00000000000..49d93722906 --- /dev/null +++ b/execution/state/block_cache_committed_storage_test.go @@ -0,0 +1,147 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package state + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/execution/types/accounts" +) + +// BenchmarkCommittedStorage prices the committed-storage cache path that every +// block worker hits on SLOAD. Run with -cpu=1,8,16 to see behaviour under the +// contention the parallel executor actually produces. +func BenchmarkCommittedStorage(b *testing.B) { + const nAddrs, nKeys = 64, 64 + addrs := make([]accounts.Address, nAddrs) + keys := make([]accounts.StorageKey, nKeys) + for i := range addrs { + addrs[i] = accounts.InternAddress(common.Address{byte(i), byte(i >> 8)}) + } + for i := range keys { + keys[i] = accounts.InternKey(common.Hash{byte(i), byte(i >> 8)}) + } + val := []byte{1, 2, 3, 4} + + // Warm read: all keys pre-filled, workers only read (the SLOAD cache-hit hot path). + b.Run("read_warm", func(b *testing.B) { + c := NewBlockStateCache() + for _, a := range addrs { + for _, k := range keys { + c.PutCommittedStorage(a, k, val) + } + } + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + c.GetCommittedStorage(addrs[i&(nAddrs-1)], keys[(i*7)&(nKeys-1)]) + i++ + } + }) + }) + + // Read-through fill: first touch misses then fills (the profiled PutCommittedStorage + // contention), subsequent touches hit. + b.Run("fill_read", func(b *testing.B) { + c := NewBlockStateCache() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + a, k := addrs[i&(nAddrs-1)], keys[(i*7)&(nKeys-1)] + if _, ok := c.GetCommittedStorage(a, k); !ok { + c.PutCommittedStorage(a, k, val) + } + i++ + } + }) + }) +} + +// committedStorage is a write-once, immutable pre-block view backed by a +// lock-free sync.Map. These pin the value semantics the reader relies on — in +// particular a cached empty slot (ok=true, nil value) must stay distinct from +// an uncached miss (ok=false). +func TestBlockStateCache_CommittedStorage_Semantics(t *testing.T) { + t.Parallel() + + cache := NewBlockStateCache() + addr := accounts.InternAddress(common.HexToAddress("0xc0ffee")) + key := accounts.InternKey(common.Hash{0x22}) + + got, ok := cache.GetCommittedStorage(addr, key) + require.False(t, ok, "uncached slot must miss") + require.Nil(t, got) + + cache.PutCommittedStorage(addr, key, []byte{0x01, 0x02}) + got, ok = cache.GetCommittedStorage(addr, key) + require.True(t, ok) + require.Equal(t, []byte{0x01, 0x02}, got) + + emptyKey := accounts.InternKey(common.Hash{0x33}) + cache.PutCommittedStorage(addr, emptyKey, nil) + got, ok = cache.GetCommittedStorage(addr, emptyKey) + require.True(t, ok, "a cached empty slot must report ok=true, not a miss") + require.Nil(t, got) + + got, ok = cache.GetCurrentStorage(addr, key) + require.True(t, ok, "GetCurrentStorage must fall back to committed when unwritten") + require.Equal(t, []byte{0x01, 0x02}, got) +} + +// All worker goroutines of a block share one cache and read committed storage +// concurrently; the lock-free path must be race-free and never lose a value. +func TestBlockStateCache_CommittedStorage_ConcurrentAccess(t *testing.T) { + t.Parallel() + + const nAddrs, nKeys = 8, 32 + addrList := make([]accounts.Address, nAddrs) + keyList := make([]accounts.StorageKey, nKeys) + for i := range addrList { + addrList[i] = accounts.InternAddress(common.Address{byte(i + 1)}) + } + for i := range keyList { + keyList[i] = accounts.InternKey(common.Hash{byte(i + 1)}) + } + // write-once value, deterministic in (addr,key) so reads can be checked. + val := func(ai, ki int) []byte { return []byte{byte(ai + 1), byte(ki + 1)} } + + cache := NewBlockStateCache() + var wg sync.WaitGroup + for g := 0; g < 16; g++ { + wg.Add(1) + go func(g int) { + defer wg.Done() + for i := 0; i < 2000; i++ { + ai := (g + i) % nAddrs + ki := (g*7 + i) % nKeys + a, k := addrList[ai], keyList[ki] + cache.PutCommittedStorage(a, k, val(ai, ki)) + if got, ok := cache.GetCommittedStorage(a, k); ok { + require.Equal(t, val(ai, ki), got) + } + cache.GetCurrentStorage(a, k) + } + }(g) + } + wg.Wait() +} diff --git a/execution/state/rw_v3.go b/execution/state/rw_v3.go index 7cfd0aee720..f023aed0815 100644 --- a/execution/state/rw_v3.go +++ b/execution/state/rw_v3.go @@ -989,10 +989,10 @@ type BlockStateCache struct { // committed holds pre-block state, lazily populated on first read. // These values are returned by CachedReaderV3 for GetCommittedState. - // committedAccounts is write-once-per-key (an immutable pre-block view), - // so it is a sync.Map for lock-free reads off the shared mu hot path. + // Both are write-once-per-key (an immutable pre-block view), so they are + // sync.Maps for lock-free reads off the shared mu hot path. committedAccounts sync.Map // accounts.Address -> *accounts.Account (nil ptr = absent) - committedStorage map[accounts.Address]map[accounts.StorageKey][]byte + committedStorage sync.Map // committedStorageKey -> []byte (nil slice = cached empty slot) // current holds the latest state including intra-block writes. // Updated by WriteAccount/WriteStorage. Read by block finalize. @@ -1021,6 +1021,12 @@ type BlockStateCache struct { writeLog []bcWriteOp } +// committedStorageKey is the sync.Map key for BlockStateCache.committedStorage. +type committedStorageKey struct { + addr accounts.Address + key accounts.StorageKey +} + // bcOpKind enumerates the operations recorded in BlockStateCache.writeLog. type bcOpKind uint8 @@ -1047,10 +1053,9 @@ type bcWriteOp struct { func NewBlockStateCache() *BlockStateCache { return &BlockStateCache{ - committedStorage: make(map[accounts.Address]map[accounts.StorageKey][]byte), - currentAccounts: make(map[accounts.Address][]byte), - currentStorage: make(map[accounts.Address]map[accounts.StorageKey][]byte), - currentCode: make(map[accounts.Address][]byte), + currentAccounts: make(map[accounts.Address][]byte), + currentStorage: make(map[accounts.Address]map[accounts.StorageKey][]byte), + currentCode: make(map[accounts.Address][]byte), } } @@ -1072,27 +1077,16 @@ func (c *BlockStateCache) PutCommittedAccount(addr accounts.Address, acc *accoun // GetCommittedStorage returns the pre-block storage value, or (nil, false) if not cached. func (c *BlockStateCache) GetCommittedStorage(addr accounts.Address, key accounts.StorageKey) ([]byte, bool) { - c.mu.RLock() - slots, addrOk := c.committedStorage[addr] - if !addrOk { - c.mu.RUnlock() + v, ok := c.committedStorage.Load(committedStorageKey{addr: addr, key: key}) + if !ok { return nil, false } - val, ok := slots[key] - c.mu.RUnlock() - return val, ok + return v.([]byte), true } // PutCommittedStorage caches a pre-block storage value. nil = empty slot. func (c *BlockStateCache) PutCommittedStorage(addr accounts.Address, key accounts.StorageKey, val []byte) { - c.mu.Lock() - slots, ok := c.committedStorage[addr] - if !ok { - slots = make(map[accounts.StorageKey][]byte) - c.committedStorage[addr] = slots - } - slots[key] = val - c.mu.Unlock() + c.committedStorage.Store(committedStorageKey{addr: addr, key: key}, val) } // --- Current (write buffer) methods --- @@ -1211,20 +1205,16 @@ func (c *BlockStateCache) GetCurrentAccount(addr accounts.Address) ([]byte, bool // Falls back to committed state if no write exists. Returns (nil, false) if not cached. func (c *BlockStateCache) GetCurrentStorage(addr accounts.Address, key accounts.StorageKey) ([]byte, bool) { c.mu.RLock() + defer c.mu.RUnlock() if slots, ok := c.currentStorage[addr]; ok { if val, ok := slots[key]; ok { - c.mu.RUnlock() return val, true } } - // Fall back to committed. - if slots, ok := c.committedStorage[addr]; ok { - if val, ok := slots[key]; ok { - c.mu.RUnlock() - return val, true - } + // Kept under RLock so current+committed stay one atomic snapshot. + if v, ok := c.committedStorage.Load(committedStorageKey{addr: addr, key: key}); ok { + return v.([]byte), true } - c.mu.RUnlock() return nil, false } From 22f41076dd4f88b161075de7670c4d54047c811e Mon Sep 17 00:00:00 2001 From: sudeepdino008 Date: Mon, 10 Aug 2026 14:45:54 +0530 Subject: [PATCH 2/5] execution/state: use range-over-int in committed storage test --- execution/state/block_cache_committed_storage_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/execution/state/block_cache_committed_storage_test.go b/execution/state/block_cache_committed_storage_test.go index 49d93722906..7b1c6cab176 100644 --- a/execution/state/block_cache_committed_storage_test.go +++ b/execution/state/block_cache_committed_storage_test.go @@ -127,11 +127,11 @@ func TestBlockStateCache_CommittedStorage_ConcurrentAccess(t *testing.T) { cache := NewBlockStateCache() var wg sync.WaitGroup - for g := 0; g < 16; g++ { + for g := range 16 { wg.Add(1) go func(g int) { defer wg.Done() - for i := 0; i < 2000; i++ { + for i := range 2000 { ai := (g + i) % nAddrs ki := (g*7 + i) % nKeys a, k := addrList[ai], keyList[ki] From f133c3bf0e5831045f0bcfe4e78d353667845a69 Mon Sep 17 00:00:00 2001 From: sudeepdino008 Date: Mon, 10 Aug 2026 11:19:14 +0200 Subject: [PATCH 3/5] execution/state: align GetCurrentStorage committed-fallback with GetCurrentAccount Release c.mu before the committed sync.Map read, matching GetCurrentAccount. committedStorage is a write-once immutable pre-block view, so the two reads need not be one atomic snapshot. --- execution/state/rw_v3.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/execution/state/rw_v3.go b/execution/state/rw_v3.go index f023aed0815..66f8a3b1a9f 100644 --- a/execution/state/rw_v3.go +++ b/execution/state/rw_v3.go @@ -1205,13 +1205,18 @@ func (c *BlockStateCache) GetCurrentAccount(addr accounts.Address) ([]byte, bool // Falls back to committed state if no write exists. Returns (nil, false) if not cached. func (c *BlockStateCache) GetCurrentStorage(addr accounts.Address, key accounts.StorageKey) ([]byte, bool) { c.mu.RLock() - defer c.mu.RUnlock() if slots, ok := c.currentStorage[addr]; ok { if val, ok := slots[key]; ok { + c.mu.RUnlock() return val, true } } - // Kept under RLock so current+committed stay one atomic snapshot. + c.mu.RUnlock() + // Committed fallback runs after releasing mu, matching GetCurrentAccount: + // committedStorage is a write-once immutable pre-block view (a sync.Map for + // lock-free reads), so a concurrent WriteStorage can only add a currentStorage + // entry we'd miss — which the RLock-then-fallback ordering can't prevent + // regardless — never tear a committed value. if v, ok := c.committedStorage.Load(committedStorageKey{addr: addr, key: key}); ok { return v.([]byte), true } From f411f736475065323a2b4c6d604521e790bf7256 Mon Sep 17 00:00:00 2001 From: sudeepdino008 Date: Mon, 10 Aug 2026 11:37:47 +0200 Subject: [PATCH 4/5] db/state: give CommitmentDomain its own lock in TemporalMemBatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TemporalMemBatch (sd.mem's latest-state layer) guarded every domain under one latestStateLock. The commitment calculator writes CommitmentDomain branches during the fold on its own goroutine, contending that lock with the exec workers' state reads (getLatest on Accounts/Storage/Code). Give CommitmentDomain its own lock: state ops keep latestStateLock, commitment ops use commitmentLock, and multi-domain ops (Flush/Unwind) take both in a fixed order (state before commitment) so there is no deadlock. Mutex profile, live chaintip mainnet — sd.mem latestStateLock contention: getLatestMetered (state reads) 16.3s -> 0.34s putLatest (writes) 13.5s -> 0.21s ~98% less sd.mem lock contention. End-to-end gas/s is unchanged (tip throughput is gated by a separate dispatch bottleneck) — this removes a latent scaling wall. -race coverage added for concurrent commitment-write vs state-read and the both-lock Unwind path. --- db/state/temporal_mem_batch.go | 60 ++++++++++++++++++++--------- db/state/temporal_mem_batch_test.go | 57 +++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/db/state/temporal_mem_batch.go b/db/state/temporal_mem_batch.go index 5f642e846b8..b7decf90590 100644 --- a/db/state/temporal_mem_batch.go +++ b/db/state/temporal_mem_batch.go @@ -64,8 +64,13 @@ type TemporalMemBatch struct { inMemHistoryReads bool latestStateLock sync.RWMutex - domains [kv.DomainLen]map[string][]dataWithTxNum - storage *btree2.Map[string, []dataWithTxNum] // TODO: replace hardcoded domain name to per-config configuration of available Guarantees/AccessMethods (range vs get) + // commitmentLock guards domains[kv.CommitmentDomain] only. Commitment branches + // are written by the calculator goroutine during the fold; keeping them off + // latestStateLock lets worker state reads run without contending those writes. + // Multi-domain ops (Flush/Unwind) take both, always latestStateLock first. + commitmentLock sync.RWMutex + domains [kv.DomainLen]map[string][]dataWithTxNum + storage *btree2.Map[string, []dataWithTxNum] // TODO: replace hardcoded domain name to per-config configuration of available Guarantees/AccessMethods (range vs get) domainWriters [kv.DomainLen]*DomainBufferedWriter iiWriters []*InvertedIndexBufferedWriter @@ -151,9 +156,24 @@ func (sd *TemporalMemBatch) putHistory(domain kv.Domain, k, v []byte, txNum uint // putLatest reports whether this write is a same-txNum update of the key, // replacing the key's last entry in place instead of appending a version. +// lockFor returns the latest-state lock guarding a single domain: CommitmentDomain +// has its own lock, everything else shares latestStateLock. +func (sd *TemporalMemBatch) lockFor(domain kv.Domain) *sync.RWMutex { + if domain == kv.CommitmentDomain { + return &sd.commitmentLock + } + return &sd.latestStateLock +} + +// lockBoth/unlockBoth guard operations spanning all domains (Flush, Unwind). +// Order is fixed — state before commitment — to avoid deadlock. +func (sd *TemporalMemBatch) lockBoth() { sd.latestStateLock.Lock(); sd.commitmentLock.Lock() } +func (sd *TemporalMemBatch) unlockBoth() { sd.commitmentLock.Unlock(); sd.latestStateLock.Unlock() } + func (sd *TemporalMemBatch) putLatest(domain kv.Domain, key string, val []byte, txNum uint64) (sameTxNumUpdate bool) { - sd.latestStateLock.Lock() - defer sd.latestStateLock.Unlock() + l := sd.lockFor(domain) + l.Lock() + defer l.Unlock() var updateMetrics = func(domain kv.Domain, putKeySize int, putValueSize int) { sd.metrics.Lock() @@ -234,8 +254,9 @@ func (sd *TemporalMemBatch) putLatest(domain kv.Domain, key string, val []byte, } func (sd *TemporalMemBatch) GetLatest(domain kv.Domain, key []byte) (v []byte, step kv.Step, ok bool) { - sd.latestStateLock.RLock() - defer sd.latestStateLock.RUnlock() + l := sd.lockFor(domain) + l.RLock() + defer l.RUnlock() return sd.getLatest(domain, key) } @@ -290,8 +311,9 @@ func (sd *TemporalMemBatch) GetAsOf(domain kv.Domain, key []byte, ts uint64) (v if !sd.inMemHistoryReads && domain != kv.ReceiptDomain { return nil, false, errors.New("GetAsOf called on TemporalMemBatch with inMemHistoryReads disabled") } - sd.latestStateLock.RLock() - defer sd.latestStateLock.RUnlock() + l := sd.lockFor(domain) + l.RLock() + defer l.RUnlock() // unwoundLatest returns the pre-unwound-block value for a key that was // modified by the unwound block. Only fires when ts is at-or-after the @@ -363,8 +385,9 @@ func (sd *TemporalMemBatch) SizeEstimate() uint64 { } func (sd *TemporalMemBatch) IteratePrefix(domain kv.Domain, prefix []byte, roTx kv.Tx, it func(k []byte, v []byte) (cont bool, err error)) error { - sd.latestStateLock.RLock() - defer sd.latestStateLock.RUnlock() + l := sd.lockFor(domain) + l.RLock() + defer l.RUnlock() var ramIter btree2.MapIter[string, []dataWithTxNum] if domain == kv.StorageDomain { ramIter = sd.storage.Iter() @@ -420,8 +443,9 @@ func (sd *TemporalMemBatch) HasPrefix(domain kv.Domain, prefix []byte, roTx kv.T // for the given domain whose key starts with prefix. It never touches disk or // segment files — only the in-memory btree (StorageDomain) or the domain map. func (sd *TemporalMemBatch) HasPrefixInRAM(domain kv.Domain, prefix []byte) bool { - sd.latestStateLock.RLock() - defer sd.latestStateLock.RUnlock() + l := sd.lockFor(domain) + l.RLock() + defer l.RUnlock() if domain == kv.StorageDomain { prefixStr := common.ToStringZeroCopy(prefix) @@ -526,8 +550,8 @@ func (sd *TemporalMemBatch) GetDiffset(tx kv.RwTx, blockHash common.Hash, blockN // Unwind drops [unwindToTxNum, ∞) func (sd *TemporalMemBatch) Unwind(unwindToTxNum uint64, changeset *[kv.DomainLen][]kv.DomainEntryDiff) { - sd.latestStateLock.Lock() - defer sd.latestStateLock.Unlock() + sd.lockBoth() + defer sd.unlockBoth() sd.unwindToTxNum = unwindToTxNum @@ -766,8 +790,8 @@ func (sd *TemporalMemBatch) flushLocked(ctx context.Context, tx kv.RwTx) error { // ahead of MDBX. Runs under latestStateLock so the callback's snapshot matches // flush-time state. func (sd *TemporalMemBatch) Flush(ctx context.Context, tx kv.RwTx, opts ...kv.FlushOption) error { - sd.latestStateLock.Lock() - defer sd.latestStateLock.Unlock() + sd.lockBoth() + defer sd.unlockBoth() if err := sd.flushLocked(ctx, tx); err != nil { return err @@ -809,8 +833,8 @@ func (sd *TemporalMemBatch) Flush(ctx context.Context, tx kv.RwTx, opts ...kv.Fl // FlushWithCommitmentCallback flushes the batch then invokes cb per // commitment-domain tuple under the lock. func (sd *TemporalMemBatch) FlushWithCommitmentCallback(ctx context.Context, tx kv.RwTx, cb execctx.CommitmentFlushCallback) error { - sd.latestStateLock.Lock() - defer sd.latestStateLock.Unlock() + sd.lockBoth() + defer sd.unlockBoth() if err := sd.flushLocked(ctx, tx); err != nil { return err diff --git a/db/state/temporal_mem_batch_test.go b/db/state/temporal_mem_batch_test.go index 9aa8689897b..bfdcb64a7d2 100644 --- a/db/state/temporal_mem_batch_test.go +++ b/db/state/temporal_mem_batch_test.go @@ -17,13 +17,17 @@ package state import ( + "sync" "testing" + btree2 "github.com/tidwall/btree" + "github.com/stretchr/testify/require" "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/db/kv" "github.com/erigontech/erigon/db/state/changeset" + "github.com/erigontech/erigon/db/state/kvmetrics" ) // A reorg unwind restores domain values from the diffset, so a domain missing @@ -45,3 +49,56 @@ func TestGetDiffsetCoversAllDomains(t *testing.T) { require.NotEmpty(t, diffs[d], "domain %s missing from GetDiffset", d) } } + +// CommitmentDomain is on its own lock, so the calculator can write commitment +// branches while workers read the state domains without contending. Run under +// -race: commitment write vs state read, state read/write, commitment +// read/write, and the both-lock Unwind path (deadlock-freedom). +func TestSplitLock_ConcurrentCommitmentWriteVsStateRead(t *testing.T) { + sd := &TemporalMemBatch{ + stepSize: 1, + storage: btree2.NewMap[string, []dataWithTxNum](128), + metrics: kvmetrics.NewDomainMetrics(), + } + for i := range sd.domains { + sd.domains[i] = map[string][]dataWithTxNum{} + } + const nKeys = 512 + mkKey := func(i int) string { return string([]byte{byte(i), byte(i >> 8)}) } + for i := range nKeys { + sd.putLatest(kv.AccountsDomain, mkKey(i), []byte{1}, uint64(i)) + } + + stop := make(chan struct{}) + var writers, readers sync.WaitGroup + + loopUntilStop := func(f func(i int)) { + defer writers.Done() + for i := 0; ; i++ { + select { + case <-stop: + return + default: + f(i) + } + } + } + writers.Add(3) + go loopUntilStop(func(i int) { sd.putLatest(kv.CommitmentDomain, mkKey(i&(nKeys-1)), []byte{byte(i)}, uint64(i)) }) + go loopUntilStop(func(i int) { sd.putLatest(kv.AccountsDomain, mkKey(i&(nKeys-1)), []byte{byte(i)}, uint64(i)) }) + go loopUntilStop(func(i int) { sd.Unwind(uint64(1_000_000+i), nil) }) // both-lock path; txNum far above data so nothing is pruned + + readers.Add(8) + for range 8 { + go func() { + defer readers.Done() + for i := range 20_000 { + sd.GetLatest(kv.AccountsDomain, []byte(mkKey(i&(nKeys-1)))) + sd.GetLatest(kv.CommitmentDomain, []byte(mkKey(i&(nKeys-1)))) + } + }() + } + readers.Wait() + close(stop) + writers.Wait() +} From 773c938988163c617c170269948c1c57194dc8ca Mon Sep 17 00:00:00 2001 From: sudeepdino008 Date: Tue, 11 Aug 2026 10:34:31 +0530 Subject: [PATCH 5/5] db/state: fix stale lock docs in TemporalMemBatch --- db/state/temporal_mem_batch.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/db/state/temporal_mem_batch.go b/db/state/temporal_mem_batch.go index b7decf90590..79136f761b2 100644 --- a/db/state/temporal_mem_batch.go +++ b/db/state/temporal_mem_batch.go @@ -154,8 +154,6 @@ func (sd *TemporalMemBatch) putHistory(domain kv.Domain, k, v []byte, txNum uint return sd.domainWriters[domain].PutWithPrev(k, v, txNum, preval) } -// putLatest reports whether this write is a same-txNum update of the key, -// replacing the key's last entry in place instead of appending a version. // lockFor returns the latest-state lock guarding a single domain: CommitmentDomain // has its own lock, everything else shares latestStateLock. func (sd *TemporalMemBatch) lockFor(domain kv.Domain) *sync.RWMutex { @@ -170,6 +168,8 @@ func (sd *TemporalMemBatch) lockFor(domain kv.Domain) *sync.RWMutex { func (sd *TemporalMemBatch) lockBoth() { sd.latestStateLock.Lock(); sd.commitmentLock.Lock() } func (sd *TemporalMemBatch) unlockBoth() { sd.commitmentLock.Unlock(); sd.latestStateLock.Unlock() } +// putLatest reports whether this write is a same-txNum update of the key, +// replacing the key's last entry in place instead of appending a version. func (sd *TemporalMemBatch) putLatest(domain kv.Domain, key string, val []byte, txNum uint64) (sameTxNumUpdate bool) { l := sd.lockFor(domain) l.Lock() @@ -261,8 +261,8 @@ func (sd *TemporalMemBatch) GetLatest(domain kv.Domain, key []byte) (v []byte, s } // getLatest is the lock-free implementation of GetLatest. -// The caller must already hold latestStateLock (either RLock or Lock), -// e.g. from within an IteratePrefix callback. +// The caller must already hold the domain's lock (see lockFor), either RLock or +// Lock, e.g. from within an IteratePrefix callback. func (sd *TemporalMemBatch) getLatest(domain kv.Domain, key []byte) (v []byte, step kv.Step, ok bool) { var unwoundLatest = func(domain kv.Domain, key string) (v []byte, step kv.Step, ok bool) { if sd.unwindChangeset != nil { @@ -787,8 +787,8 @@ func (sd *TemporalMemBatch) flushLocked(ctx context.Context, tx kv.RwTx) error { // Flush writes the mem-batch to tx. With kv.WithFlushCallback options, the // registered per-domain callback is invoked for every (key, value, step, txNum) // tuple after the MDBX write succeeds, so a downstream cache can never be left -// ahead of MDBX. Runs under latestStateLock so the callback's snapshot matches -// flush-time state. +// ahead of MDBX. Runs under both latest-state locks so the callback's snapshot +// matches flush-time state. func (sd *TemporalMemBatch) Flush(ctx context.Context, tx kv.RwTx, opts ...kv.FlushOption) error { sd.lockBoth() defer sd.unlockBoth()