diff --git a/cl/beacon/handler/block_production_test.go b/cl/beacon/handler/block_production_test.go
index 4af9fa45a24..6baf1f72ec6 100644
--- a/cl/beacon/handler/block_production_test.go
+++ b/cl/beacon/handler/block_production_test.go
@@ -517,7 +517,7 @@ func TestCaplinBlockProductionWithWithdrawalRequest(t *testing.T) {
)
require.NoError(t, err)
gen.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -649,7 +649,7 @@ func TestCaplinBlockProductionGlamsterdamSlotNumber(t *testing.T) {
)
require.NoError(t, err)
gen.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
diff --git a/cmd/rpcdaemon/rpcdaemontest/test_util.go b/cmd/rpcdaemon/rpcdaemontest/test_util.go
index f56ea8a0f3f..81017d15dd8 100644
--- a/cmd/rpcdaemon/rpcdaemontest/test_util.go
+++ b/cmd/rpcdaemon/rpcdaemontest/test_util.go
@@ -37,6 +37,7 @@ import (
"github.com/erigontech/erigon/common/log/v3"
"github.com/erigontech/erigon/common/u256"
"github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/db/state/execctx"
"github.com/erigontech/erigon/execution/abi/bind"
"github.com/erigontech/erigon/execution/abi/bind/backends"
"github.com/erigontech/erigon/execution/builder"
@@ -113,11 +114,11 @@ func genTestChainOnce(t *testing.T) {
defer contractBackend.Close()
var err error
- testOrphanedChain, err = blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 5, func(i int, block *blockgen.BlockGen) {})
+ testOrphanedChain, err = blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 5, func(i int, block *blockgen.BlockGen) {}, m.PublishedSD())
if err != nil {
t.Fatalf("rpcdaemontest: failed to generate orphaned chain: %v", err)
}
- testChain, err = generateChain(&addresses, m.ChainConfig, m.Genesis, m.Engine, m.DB, contractBackend)
+ testChain, err = generateChain(&addresses, m.ChainConfig, m.Genesis, m.Engine, m.DB, contractBackend, m.PublishedSD())
if err != nil {
t.Fatalf("rpcdaemontest: failed to generate chain: %v", err)
}
@@ -166,6 +167,7 @@ func generateChain(
engine rules.Engine,
db kv.TemporalRwDB,
contractBackend *backends.SimulatedBackend,
+ readSD ...*execctx.SharedDomains,
) (*blockgen.ChainPack, error) {
var (
key = addresses.key
@@ -349,7 +351,7 @@ func generateChain(
block.AddTx(txn)
}
contractBackend.Commit()
- })
+ }, readSD...)
}
func computeMappingStorageKey(addr common.Address, slot uint64) common.Hash {
@@ -569,7 +571,7 @@ func CreateTestExecModuleForTraces(t *testing.T) *execmoduletester.ExecModuleTes
tx, _ := types.SignTx(types.NewTransaction(0, a2,
&u256.Num0, 50000, &u256.Num1, []byte{0x01, 0x00, 0x01, 0x00}), *types.LatestSignerForChainID(nil), key)
b.AddTx(tx)
- })
+ }, m.PublishedSD())
if err != nil {
t.Fatalf("generate blocks: %v", err)
}
@@ -678,7 +680,7 @@ func CreateTestExecModuleForTracesCollision(t *testing.T) *execmoduletester.Exec
tx, _ = types.SignTx(types.NewTransaction(2, bb,
&u256.Num0, 100000, &u256.Num1, nil), *types.LatestSignerForChainID(nil), key)
b.AddTx(tx)
- })
+ }, m.PublishedSD())
if err != nil {
t.Fatalf("generate blocks: %v", err)
}
diff --git a/cmd/utils/app/import_cmd.go b/cmd/utils/app/import_cmd.go
index 78bb0fef479..e57609d08f5 100644
--- a/cmd/utils/app/import_cmd.go
+++ b/cmd/utils/app/import_cmd.go
@@ -268,8 +268,10 @@ func missingBlocks(chainDB kv.RwDB, blocks []*types.Block, blockReader dbservice
})
for i, block := range blocks {
- // If we're behind the chain head, only check block, state is available at head
- if headBlock.NumberU64() > block.NumberU64() {
+ // No durable head yet (e.g. only genesis, or the head's commit is still
+ // in flight under background commit): nothing is behind us, so fall
+ // through to the per-block presence check.
+ if headBlock != nil && headBlock.NumberU64() > block.NumberU64() {
if !ChainHasBlock(chainDB, block) {
return blocks[i:]
}
@@ -445,6 +447,11 @@ func InsertChain(ethereum *eth.Ethereum, chain *blockgen.ChainPack, setHead bool
}
}
+ // UpdateForkChoice commits in the background; drain it so this file's
+ // header/TD/canonical state is durable in the raw DB before the next file's
+ // side-chain TD/head reads (and the final head write) observe it.
+ ethereum.ExecutionModule().WaitCommitsDrained()
+
return ethereum.ChainDB().Update(ethereum.SentryCtx(), func(tx kv.RwTx) error {
rawdb.WriteHeadBlockHash(tx, lvh)
return nil
diff --git a/db/kv/kv_interface.go b/db/kv/kv_interface.go
index 78c4fdd76ef..4c0c188c7d0 100644
--- a/db/kv/kv_interface.go
+++ b/db/kv/kv_interface.go
@@ -587,6 +587,10 @@ type TemporalMemBatch interface {
ClearRam()
IndexAdd(table InvertedIdx, key []byte, txNum uint64) (err error)
IteratePrefix(domain Domain, prefix []byte, roTx Tx, it func(k []byte, v []byte) (cont bool, err error)) error
+ RangeAsOf(ctx context.Context, domain Domain, fromKey, toKey []byte, ts uint64, asc order.By, limit int, roTx Tx) (stream.KV, error)
+ HistoryRange(ctx context.Context, domain Domain, fromTs, toTs int, asc order.By, limit int, roTx Tx) (stream.KV, error)
+ IndexRange(name InvertedIdx, k []byte, fromTs, toTs int, asc order.By, limit int, roTx Tx) (stream.U64, error)
+ HistorySeek(domain Domain, key []byte, ts uint64) (v []byte, ok bool, err error)
HasPrefix(domain Domain, prefix []byte, roTx Tx) ([]byte, []byte, bool, error)
HasPrefixInRAM(domain Domain, prefix []byte) bool
SizeEstimate() uint64
@@ -595,7 +599,6 @@ type TemporalMemBatch interface {
DiscardWrites(domain Domain)
Unwind(txNumUnwindTo uint64, changeset *[DomainLen][]DomainEntryDiff)
GetAsOf(domain Domain, key []byte, ts uint64) (v []byte, ok bool, err error)
- HistorySeek(domain Domain, key []byte, ts uint64) (v []byte, ok bool, err error)
SetInMemHistoryReads(v bool)
InMemHistoryReads() bool
}
diff --git a/db/kv/membatchwithdb/memory_mutation.go b/db/kv/membatchwithdb/memory_mutation.go
index 1d24f6ce070..1c8df10b5c8 100644
--- a/db/kv/membatchwithdb/memory_mutation.go
+++ b/db/kv/membatchwithdb/memory_mutation.go
@@ -42,6 +42,13 @@ type DomainReader interface {
HistorySeek(name kv.Domain, k []byte, ts uint64) ([]byte, bool, error)
}
+// domainLatestReader is the subset of SharedDomains needed for GetLatest/HasPrefix
+// reads from an OverlayTemporalReadView. Implemented by SharedDomains.
+type domainLatestReader interface {
+ GetLatest(domain kv.Domain, tx kv.TemporalTx, k []byte) ([]byte, kv.Step, error)
+ HasPrefix(domain kv.Domain, prefix []byte, roTx kv.Tx) ([]byte, []byte, bool, error)
+}
+
type MemoryMutation struct {
// mu protects concurrent access to the mutation's maps and backing tx.
// Read methods (GetOne, Has) acquire RLock; write methods (Put, Delete,
@@ -1141,13 +1148,25 @@ func (v *OverlayTemporalReadView) Apply(_ context.Context, f func(tx kv.Tx) erro
return f(v)
}
-// Temporal methods — delegate to the independent temporal tx.
+// Temporal methods — check the installed SD chain before the committed tx.
func (v *OverlayTemporalReadView) GetLatest(name kv.Domain, k []byte) ([]byte, kv.Step, error) {
+ // The installed reader walks the whole generation chain and falls through to
+ // v.temporalTx itself, so its value and error are authoritative — a nil here
+ // is a genuine tombstone, not a cue to re-read the committed tx.
+ if dr, ok := v.MemoryMutation.DomainReader.(domainLatestReader); ok {
+ return dr.GetLatest(name, v.temporalTx, k)
+ }
return v.temporalTx.GetLatest(name, k)
}
func (v *OverlayTemporalReadView) HasPrefix(name kv.Domain, prefix []byte) ([]byte, []byte, bool, error) {
+ // Authoritative for the same reason as GetLatest: the reader already honours
+ // parent-generation tombstones and the committed tx, so a false result must
+ // not be overridden by re-checking committed storage the overlay cleared.
+ if dr, ok := v.MemoryMutation.DomainReader.(domainLatestReader); ok {
+ return dr.HasPrefix(name, prefix, v.temporalTx)
+ }
return v.temporalTx.HasPrefix(name, prefix)
}
diff --git a/db/snapshotsync/freezeblocks/block_reader.go b/db/snapshotsync/freezeblocks/block_reader.go
index 8534f757f56..765b677e778 100644
--- a/db/snapshotsync/freezeblocks/block_reader.go
+++ b/db/snapshotsync/freezeblocks/block_reader.go
@@ -1436,6 +1436,9 @@ func (r *BlockReader) CurrentBlock(db kv.Tx) (*types.Block, error) {
if err != nil {
return nil, fmt.Errorf("failed HeaderNumber: %w", err)
}
+ if headNumber == nil {
+ return nil, nil
+ }
block, _, err := r.blockWithSenders(context.Background(), db, headHash, *headNumber, true)
return block, err
}
diff --git a/db/snapshotsync/freezeblocks/dump_test.go b/db/snapshotsync/freezeblocks/dump_test.go
index cfe6572946f..8ec4325029f 100644
--- a/db/snapshotsync/freezeblocks/dump_test.go
+++ b/db/snapshotsync/freezeblocks/dump_test.go
@@ -291,7 +291,7 @@ func createDumpTestKV(t *testing.T, chainConfig *chain.Config, chainSize int) *e
t.Fatalf("failed to create tx: %v", txErr)
}
b.AddTx(tx)
- })
+ }, m.PublishedSD())
if err != nil {
t.Fatal(err)
}
diff --git a/db/state/execctx/domain_shared.go b/db/state/execctx/domain_shared.go
index 8398bacf452..f29cedc7f30 100644
--- a/db/state/execctx/domain_shared.go
+++ b/db/state/execctx/domain_shared.go
@@ -22,6 +22,7 @@ import (
"errors"
"fmt"
"runtime"
+ "slices"
"sync"
"sync/atomic"
"time"
@@ -34,6 +35,7 @@ import (
"github.com/erigontech/erigon/db/kv/membatchwithdb"
"github.com/erigontech/erigon/db/kv/order"
"github.com/erigontech/erigon/db/kv/rawdbv3"
+ "github.com/erigontech/erigon/db/kv/stream"
"github.com/erigontech/erigon/db/state/changeset"
"github.com/erigontech/erigon/db/state/kvmetrics"
"github.com/erigontech/erigon/db/state/statecfg"
@@ -228,6 +230,14 @@ type SharedDomains struct {
// to read from the FCU's published SD without writing to it.
parent *SharedDomains
+ // readCoordinator, when set, opens a base RO tx coordinated with the
+ // background-commit generation set: opened while holding the commit mutex so
+ // the tx's committed snapshot reflects every generation the parent chain has
+ // dropped as committed. Readers that need an underlying-DB base call
+ // BeginCoordinatedRo instead of opening an ad-hoc BeginTemporalRo, so a datum
+ // is always in the mem chain or this tx — never neither.
+ readCoordinator func(context.Context) (kv.TemporalTx, error)
+
// stateCache is an optional cache for state data (accounts, storage, code);
// cacheApplier is its authoritative writer handle (commit/unwind only).
stateCache *cache.StateCache
@@ -793,7 +803,29 @@ func (sd *SharedDomains) InMemHistoryReads() bool { return sd.mem.InMem
// SetParent sets a parent SD for read-through domain chaining. Domain reads
// that miss in the local mem batch will check the parent's mem batch before
// falling through to the underlying tx/aggregator.
-func (sd *SharedDomains) SetParent(parent *SharedDomains) { sd.parent = parent }
+func (sd *SharedDomains) SetParent(parent *SharedDomains) {
+ sd.parent = parent
+ // Inherit the parent's read coordinator so a child SD built for a read pass
+ // opens coordinated base txns without the caller re-wiring it.
+ if parent != nil && sd.readCoordinator == nil {
+ sd.readCoordinator = parent.readCoordinator
+ }
+}
+
+// SetReadCoordinator wires the coordinated base-tx opener (see readCoordinator).
+func (sd *SharedDomains) SetReadCoordinator(fn func(context.Context) (kv.TemporalTx, error)) {
+ sd.readCoordinator = fn
+}
+
+// BeginCoordinatedRo opens a base RO tx coordinated with the generation set (see
+// readCoordinator). Falls back to a plain tx from db when no coordinator is set
+// (non-bg-commit setups), so callers can use it unconditionally.
+func (sd *SharedDomains) BeginCoordinatedRo(ctx context.Context, db kv.TemporalRoDB) (kv.TemporalTx, error) {
+ if sd.readCoordinator != nil {
+ return sd.readCoordinator(ctx)
+ }
+ return db.BeginTemporalRo(ctx)
+}
// BlockOverlay returns the in-memory overlay for block-level metadata (headers, bodies,
// canonical hashes, TD, stage progress, forkchoice markers). Callers can use this
@@ -807,15 +839,29 @@ func (sd *SharedDomains) CloseBlockOverlay() {
}
}
-// BlockOverlayTemporalTx returns a read-only temporal view of the block overlay.
-// This allows consumers (RPC, shutter) to read uncommitted block data with
+// OverlayTemporalTx returns a read-only temporal view of the overlay. This
+// allows consumers (RPC, shutter) to read uncommitted overlay data with
// temporal (state history) support. Returns nil if no overlay is active.
-func (sd *SharedDomains) BlockOverlayTemporalTx(roTx kv.TemporalTx) kv.TemporalTx {
+func (sd *SharedDomains) OverlayTemporalTx(roTx kv.TemporalTx) kv.TemporalTx {
+ // Chain the read view through the parent generations' block overlays, oldest
+ // ancestor first, bottoming out at roTx — NewTemporalReadView rebinds the
+ // fallthrough base, so the ancestor chain must be built explicitly here. This
+ // mirrors the domain-mem parent chain: block data written by an uncommitted
+ // ancestor generation stays visible until its commit lands.
+ base := roTx
+ if sd.parent != nil {
+ if pv := sd.parent.OverlayTemporalTx(roTx); pv != nil {
+ base = pv
+ }
+ }
overlay := sd.blockOverlay.Load()
if overlay == nil {
+ if base != kv.TemporalTx(roTx) {
+ return base
+ }
return nil
}
- return overlay.NewTemporalReadView(roTx)
+ return overlay.NewTemporalReadView(base)
}
// InitBlockOverlay creates (or replaces) the block-level metadata overlay backed by
@@ -943,12 +989,84 @@ func (sd *SharedDomains) InlineTouchKeyDisabled() bool {
return sd.disableInlineTouchKey
}
+// collectPrefixCandidates unions the candidate keys under prefix from the leaf
+// batch (leaf RAM + committed DB) and every parent generation's RAM, returned in
+// key order. Collection runs inside each mem's read lock but only appends keys;
+// callers resolve values afterwards (getLatest re-locks), so no read lock is
+// ever held across a second acquisition — see the recursive-RLock hazard on
+// sync.RWMutex. The whole candidate set is materialized before resolution, so a
+// self-destruct enumerating a large contract's storage holds all its slot keys.
+func (sd *SharedDomains) collectPrefixCandidates(domain kv.Domain, prefix []byte, roTx kv.Tx) ([]string, error) {
+ seen := make(map[string]struct{})
+ var keys []string
+ collect := func(k, _ []byte) (bool, error) {
+ ks := string(k)
+ if _, dup := seen[ks]; !dup {
+ seen[ks] = struct{}{}
+ keys = append(keys, ks)
+ }
+ return true, nil
+ }
+ for p := sd; p != nil; p = p.parent {
+ if err := p.mem.IteratePrefix(domain, prefix, roTx, collect); err != nil {
+ return nil, err
+ }
+ }
+ slices.Sort(keys)
+ return keys, nil
+}
+
+// HasPrefix reports the first live key/value under prefix in key order, chain-
+// correctly across the generation chain: a candidate cleared by a nearer
+// generation is skipped and one written only in a parent is still found.
func (sd *SharedDomains) HasPrefix(domain kv.Domain, prefix []byte, roTx kv.Tx) ([]byte, []byte, bool, error) {
- return sd.mem.HasPrefix(domain, prefix, roTx)
+ tx, ok := roTx.(kv.TemporalTx)
+ if sd.parent == nil || !ok {
+ return sd.mem.HasPrefix(domain, prefix, roTx)
+ }
+ keys, err := sd.collectPrefixCandidates(domain, prefix, roTx)
+ if err != nil {
+ return nil, nil, false, err
+ }
+ for _, ks := range keys {
+ k := []byte(ks)
+ v, _, err := sd.getLatestMetered(domain, tx, k, nil, sd.cacheReader())
+ if err != nil {
+ return nil, nil, false, err
+ }
+ if len(v) > 0 {
+ return k, bytes.Clone(v), true, nil
+ }
+ }
+ return nil, nil, false, nil
}
+// IteratePrefix emits every live key under prefix across the whole generation
+// chain in key order, newest-generation-wins with tombstone shadowing.
func (sd *SharedDomains) IteratePrefix(domain kv.Domain, prefix []byte, roTx kv.Tx, it func(k []byte, v []byte) (cont bool, err error)) error {
- return sd.mem.IteratePrefix(domain, prefix, roTx, it)
+ tx, ok := roTx.(kv.TemporalTx)
+ if sd.parent == nil || !ok {
+ return sd.mem.IteratePrefix(domain, prefix, roTx, it)
+ }
+ keys, err := sd.collectPrefixCandidates(domain, prefix, roTx)
+ if err != nil {
+ return err
+ }
+ for _, ks := range keys {
+ k := []byte(ks)
+ v, _, err := sd.getLatestMetered(domain, tx, k, nil, sd.cacheReader())
+ if err != nil {
+ return err
+ }
+ if len(v) == 0 {
+ continue
+ }
+ cont, err := it(k, v)
+ if err != nil || !cont {
+ return err
+ }
+ }
+ return nil
}
func (sd *SharedDomains) Close() {
@@ -1244,9 +1362,12 @@ func (sd *SharedDomains) getLatestMetered(domain kv.Domain, tx kv.TemporalTx, k
maxStep = step
}
- // Check parent's mem batch (read-through chaining for child SDs)
- if sd.parent != nil {
- if v, step, ok := sd.parent.mem.GetLatest(domain, k); ok {
+ // Check the parent chain's mem batches (read-through chaining for child
+ // SDs). Walk every ancestor generation, not just the immediate parent, so a
+ // value written by an uncommitted block several generations back is still
+ // visible — the parent link is a chain, not a single hop.
+ for p := sd.parent; p != nil; p = p.parent {
+ if v, step, ok := p.mem.GetLatest(domain, k); ok {
if dbg.KVReadLevelledMetrics {
wm.UpdateCacheReads(domain, start)
}
@@ -1503,8 +1624,8 @@ func (sd *SharedDomains) codeHashForAddr(tx kv.TemporalTx, view cache.ReadView,
if v, _, ok := sd.mem.GetLatest(kv.AccountsDomain, addr); ok {
return accounts.DeserialiseV3CodeHash(v)
}
- if sd.parent != nil {
- if v, _, ok := sd.parent.mem.GetLatest(kv.AccountsDomain, addr); ok {
+ for p := sd.parent; p != nil; p = p.parent {
+ if v, _, ok := p.mem.GetLatest(kv.AccountsDomain, addr); ok {
return accounts.DeserialiseV3CodeHash(v)
}
}
@@ -1639,11 +1760,45 @@ func (sd *SharedDomains) DomainLogMetrics() map[kv.Domain][]any {
}
func (sd *SharedDomains) GetAsOf(domain kv.Domain, key []byte, ts uint64) (v []byte, ok bool, err error) {
- return sd.mem.GetAsOf(domain, key, ts)
+ if v, ok, err = sd.mem.GetAsOf(domain, key, ts); ok || err != nil {
+ return v, ok, err
+ }
+ for p := sd.parent; p != nil; p = p.parent {
+ if v, ok, err = p.mem.GetAsOf(domain, key, ts); ok || err != nil {
+ return v, ok, err
+ }
+ }
+ return nil, false, nil
+}
+
+// RangeAsOf returns domain values over [fromKey, toKey) as of ts, merging the
+// in-flight in-memory history with committed DB+files.
+func (sd *SharedDomains) RangeAsOf(ctx context.Context, domain kv.Domain, fromKey, toKey []byte, ts uint64, asc order.By, limit int, roTx kv.Tx) (stream.KV, error) {
+ return sd.mem.RangeAsOf(ctx, domain, fromKey, toKey, ts, asc, limit, roTx)
}
-func (sd *SharedDomains) HistorySeek(domain kv.Domain, key []byte, ts uint64) (v []byte, ok bool, err error) {
- return sd.mem.HistorySeek(domain, key, ts)
+// HistoryRange returns keys changed in [fromTs, toTs) with their pre-range
+// value, merging in-flight in-memory history with committed.
+func (sd *SharedDomains) HistoryRange(ctx context.Context, domain kv.Domain, fromTs, toTs int, asc order.By, limit int, roTx kv.Tx) (stream.KV, error) {
+ return sd.mem.HistoryRange(ctx, domain, fromTs, toTs, asc, limit, roTx)
+}
+
+// IndexRange returns the txNums at which key k changed in [fromTs, toTs),
+// merging in-flight in-memory history with the committed inverted index.
+func (sd *SharedDomains) IndexRange(name kv.InvertedIdx, k []byte, fromTs, toTs int, asc order.By, limit int, roTx kv.Tx) (stream.U64, error) {
+ return sd.mem.IndexRange(name, k, fromTs, toTs, asc, limit, roTx)
+}
+
+// HistorySeek returns the in-flight in-memory historical value of key as of ts,
+// walking the whole generation chain; (nil, false) means no generation has it,
+// so the caller falls back to committed history.
+func (sd *SharedDomains) HistorySeek(domain kv.Domain, key []byte, ts uint64) ([]byte, bool, error) {
+ for p := sd; p != nil; p = p.parent {
+ if v, ok, err := p.mem.HistorySeek(domain, key, ts); err != nil || ok {
+ return v, ok, err
+ }
+ }
+ return nil, false, nil
}
// DomainPut
diff --git a/db/state/execctx/domain_shared_test.go b/db/state/execctx/domain_shared_test.go
index 0e6969625d8..60fd21575c7 100644
--- a/db/state/execctx/domain_shared_test.go
+++ b/db/state/execctx/domain_shared_test.go
@@ -1486,6 +1486,89 @@ func TestSharedDomain_HasPrefix_StorageDomain(t *testing.T) {
}
}
+// TestSharedDomain_HasPrefix_ParentChain pins the background-commit generation
+// chain: a child SD's prefix reads must see storage written only in an
+// uncommitted parent generation, and must NOT resurrect committed storage that
+// a parent generation cleared.
+func TestSharedDomain_HasPrefix_ParentChain(t *testing.T) {
+ if testing.Short() {
+ t.Skip("slow test")
+ }
+ t.Parallel()
+
+ ctx, cancel := context.WithCancel(t.Context())
+ t.Cleanup(cancel)
+
+ db := newTestDb(t, uint64(1))
+
+ acc1 := common.HexToAddress("0x1234567890123456789012345678901234567890")
+ acc1slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
+ storageK1 := append(append([]byte{}, acc1[:]...), acc1slot[:]...)
+ acc2 := common.HexToAddress("0x1234567890123456789012345678901234567891")
+ acc2slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000002")
+ storageK2 := append(append([]byte{}, acc2[:]...), acc2slot[:]...)
+
+ // Commit acc1's storage so it lives in the DB (the "already committed" state a
+ // parent generation may later clear).
+ rwTx0, err := db.BeginTemporalRw(ctx)
+ require.NoError(t, err)
+ t.Cleanup(rwTx0.Rollback)
+ base, err := execctx.NewSharedDomains(ctx, rwTx0, log.New())
+ require.NoError(t, err)
+ require.NoError(t, base.DomainPut(kv.StorageDomain, rwTx0, storageK1, []byte{1}, 1, nil))
+ require.NoError(t, base.Flush(ctx, rwTx0))
+ require.NoError(t, rwTx0.Commit())
+ base.Close()
+
+ // Parent generation (uncommitted): write acc2's storage and clear acc1's.
+ rwTxP, err := db.BeginTemporalRw(ctx)
+ require.NoError(t, err)
+ t.Cleanup(rwTxP.Rollback)
+ parent, err := execctx.NewSharedDomains(ctx, rwTxP, log.New())
+ require.NoError(t, err)
+ t.Cleanup(parent.Close)
+ require.NoError(t, parent.DomainPut(kv.StorageDomain, rwTxP, storageK2, []byte{2}, 2, nil))
+ require.NoError(t, parent.DomainDelPrefix(kv.StorageDomain, rwTxP, acc1[:], 3))
+
+ // Child generation chained onto the parent, reading a committed snapshot.
+ roTx, err := db.BeginTemporalRo(ctx)
+ require.NoError(t, err)
+ t.Cleanup(roTx.Rollback)
+ child, err := execctx.NewSharedDomains(ctx, roTx, log.New())
+ require.NoError(t, err)
+ t.Cleanup(child.Close)
+ child.SetParent(parent)
+
+ // Item 3c: storage written only in the parent generation is visible.
+ firstKey, firstVal, ok, err := child.HasPrefix(kv.StorageDomain, acc2[:], roTx)
+ require.NoError(t, err)
+ require.True(t, ok, "parent-generation storage must be visible to the child")
+ require.Equal(t, storageK2, firstKey)
+ require.Equal(t, []byte{2}, firstVal)
+
+ var iterated [][]byte
+ require.NoError(t, child.IteratePrefix(kv.StorageDomain, acc2[:], roTx, func(k, _ []byte) (bool, error) {
+ iterated = append(iterated, bytes.Clone(k))
+ return true, nil
+ }))
+ require.Equal(t, [][]byte{storageK2}, iterated)
+
+ // Item 3b: committed storage cleared by the parent generation must not be
+ // resurrected from the committed tx.
+ firstKey, firstVal, ok, err = child.HasPrefix(kv.StorageDomain, acc1[:], roTx)
+ require.NoError(t, err)
+ require.False(t, ok, "storage cleared by the parent generation must stay cleared")
+ require.Nil(t, firstKey)
+ require.Nil(t, firstVal)
+
+ iterated = nil
+ require.NoError(t, child.IteratePrefix(kv.StorageDomain, acc1[:], roTx, func(k, _ []byte) (bool, error) {
+ iterated = append(iterated, bytes.Clone(k))
+ return true, nil
+ }))
+ require.Empty(t, iterated, "IteratePrefix must not yield parent-cleared storage")
+}
+
// TestDomainPut_HistoryCorrectness is a property test that verifies history invariants
// after random sequences of DomainPut calls:
// 1. GetAsOf returns the correct value at every txNum (history is complete and accurate)
@@ -1982,7 +2065,7 @@ func TestBlockOverlay_DomainReadsRegression(t *testing.T) {
require.Equal(t, value, gotValHist)
// --- Secondary path: overlay.NewTemporalReadView returns *OverlayTemporalReadView ---
- overlayTx := sd.BlockOverlayTemporalTx(tx)
+ overlayTx := sd.OverlayTemporalTx(tx)
require.NotNil(t, overlayTx)
gotVal2, ok, err := overlayTx.GetAsOf(kv.ReceiptDomain, key, txNum+1)
diff --git a/db/state/execctx/options.go b/db/state/execctx/options.go
index 853ad487c54..fb53306043b 100644
--- a/db/state/execctx/options.go
+++ b/db/state/execctx/options.go
@@ -26,6 +26,15 @@ type sharedDomainOptions struct {
// SharedDomainOption configures NewSharedDomains.
type SharedDomainOption func(*sharedDomainOptions)
+// WithoutSharedBranchCache detaches this SharedDomains from the aggregator-scope
+// commitment BranchCache. Use for speculative, discarded computations (e.g. the
+// block builder) that run concurrently with the live node: sharing the cache
+// would both race the node's writers and pollute it with speculative branches.
+// Reads fall through to sd.mem / the parent chain / MDBX instead.
+func WithoutSharedBranchCache() SharedDomainOption {
+ return func(o *sharedDomainOptions) { o.useSharedBranchCache = false }
+}
+
// WithTrieConfig replaces the trie configuration wholesale; the caller owns Variant.
func WithTrieConfig(cfg commitment.TrieConfig) SharedDomainOption {
return func(o *sharedDomainOptions) { o.trieCfg = cfg }
@@ -36,11 +45,6 @@ func WithoutDeferredBranchUpdates() SharedDomainOption {
return func(o *sharedDomainOptions) { o.trieCfg.DeferBranchUpdates = false }
}
-// WithoutSharedBranchCache keeps commitment reads within the transaction snapshot.
-func WithoutSharedBranchCache() SharedDomainOption {
- return func(o *sharedDomainOptions) { o.useSharedBranchCache = false }
-}
-
// WithSequentialCommitment forces the sequential HexPatriciaHashed trie regardless
// of the experimental parallel/concurrent flags — for one-shot / empty-DB paths
// (e.g. genesis) that wire no trie-context factory for the parallel trie.
diff --git a/db/state/temporal_mem_batch.go b/db/state/temporal_mem_batch.go
index 57b959e5e22..1ca4871cae7 100644
--- a/db/state/temporal_mem_batch.go
+++ b/db/state/temporal_mem_batch.go
@@ -25,6 +25,7 @@ import (
"fmt"
"maps"
"slices"
+ "sort"
"strings"
"sync"
@@ -32,6 +33,8 @@ import (
"github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/db/kv/order"
+ "github.com/erigontech/erigon/db/kv/stream"
"github.com/erigontech/erigon/db/rawdb"
"github.com/erigontech/erigon/db/state/changeset"
"github.com/erigontech/erigon/db/state/execctx"
@@ -69,6 +72,13 @@ type TemporalMemBatch struct {
domainWriters [kv.DomainLen]*DomainBufferedWriter
iiWriters []*InvertedIndexBufferedWriter
+ // iiMem is the queryable local copy of standalone inverted-index writes
+ // (index -> key -> ascending txNums). The ii writers are write-only etl
+ // buffers, so without this IndexRange can't answer in-flight queries for
+ // standalone indices (traces, logs) the way it can for domain-backed ones
+ // via the domain history. Populated by IndexAdd only when inMemHistoryReads
+ // is set — the same gate under which the domain history above is retained.
+ iiMem map[kv.InvertedIdx]map[string][]uint64
pastDomainWriters [kv.DomainLen][]*DomainBufferedWriter
pastIIWriters []*InvertedIndexBufferedWriter
@@ -105,6 +115,7 @@ func NewTemporalMemBatch(tx kv.TemporalTx, ioMetrics any) *TemporalMemBatch {
storage: btree2.NewMap[string, []dataWithTxNum](128),
metrics: ioMetrics.(*kvmetrics.DomainMetrics),
inMemHistoryReads: true,
+ iiMem: map[kv.InvertedIdx]map[string][]uint64{},
}
aggTx := AggTx(tx)
sd.stepSize = aggTx.StepSize()
@@ -348,8 +359,317 @@ func (sd *TemporalMemBatch) GetAsOf(domain kv.Domain, key []byte, ts uint64) (v
return unwoundLatest(domain, keyS)
}
-func (sd *TemporalMemBatch) HistorySeek(domain kv.Domain, key []byte, ts uint64) (v []byte, ok bool, err error) {
- return sd.GetAsOf(domain, key, ts)
+// RangeAsOf returns domain values over [fromKey, toKey) as of txNum ts, merging
+// the in-memory (not-yet-committed) history with committed DB+files. In-memory
+// values take precedence per key; keys deleted or not yet created as of ts are
+// omitted. Only used by the RPC test harness to read the in-flight tip.
+func (sd *TemporalMemBatch) RangeAsOf(ctx context.Context, domain kv.Domain, fromKey, toKey []byte, ts uint64, asc order.By, limit int, roTx kv.Tx) (stream.KV, error) {
+ committed, err := AggTx(roTx).RangeAsOf(ctx, roTx, domain, fromKey, toKey, ts, asc, limit)
+ if err != nil {
+ return nil, err
+ }
+ if !sd.inMemHistoryReads {
+ return committed, nil
+ }
+ mem := sd.memRangeAsOf(domain, fromKey, toKey, ts, asc)
+ if len(mem) == 0 {
+ return committed, nil
+ }
+ merged := stream.UnionKV(&memKVIter{pairs: mem}, committed, -1)
+ return &liveLimitKV{inner: merged, limit: limit}, nil
+}
+
+// memRangeAsOf collects the in-memory latest history for domain over
+// [fromKey, toKey), resolved as of ts. Tombstones (empty value) are kept so
+// they mask committed rows; keys with no in-memory entry at or before ts are
+// omitted so committed state supplies them.
+func (sd *TemporalMemBatch) memRangeAsOf(domain kv.Domain, fromKey, toKey []byte, ts uint64, asc order.By) []memKVPair {
+ sd.latestStateLock.RLock()
+ defer sd.latestStateLock.RUnlock()
+ inRange := func(k string) bool {
+ if fromKey != nil && k < string(fromKey) {
+ return false
+ }
+ if toKey != nil && k >= string(toKey) {
+ return false
+ }
+ return true
+ }
+ var pairs []memKVPair
+ collect := func(k string, entries []dataWithTxNum) {
+ if !inRange(k) {
+ return
+ }
+ if v, ok := asOfEntry(entries, ts); ok {
+ pairs = append(pairs, memKVPair{k: []byte(k), v: v})
+ }
+ }
+ if domain == kv.StorageDomain {
+ sd.storage.Scan(func(k string, entries []dataWithTxNum) bool {
+ collect(k, entries)
+ return true
+ })
+ } else {
+ for k, entries := range sd.domains[domain] {
+ collect(k, entries)
+ }
+ }
+ sort.Slice(pairs, func(i, j int) bool {
+ if asc == order.Asc {
+ return bytes.Compare(pairs[i].k, pairs[j].k) < 0
+ }
+ return bytes.Compare(pairs[i].k, pairs[j].k) > 0
+ })
+ return pairs
+}
+
+// HistorySeek returns the in-memory value in effect at ts: the value of the last
+// recorded change with txNum < ts. Returns (nil, false) when there is no such
+// change — either the key has no in-memory history, or ts precedes its first
+// in-memory write — so the caller falls back to committed history, which holds
+// the value for a key that existed before the overlay (and correctly reports
+// absent for one created in it).
+func (sd *TemporalMemBatch) HistorySeek(domain kv.Domain, key []byte, ts uint64) ([]byte, bool, error) {
+ if !sd.inMemHistoryReads {
+ return nil, false, nil
+ }
+ sd.latestStateLock.RLock()
+ defer sd.latestStateLock.RUnlock()
+ ks := common.ToStringZeroCopy(key)
+ var entries []dataWithTxNum
+ if domain == kv.StorageDomain {
+ entries, _ = sd.storage.Get(ks)
+ } else {
+ entries = sd.domains[domain][ks]
+ }
+ for _, e := range slices.Backward(entries) {
+ if e.txNum < ts {
+ return e.data, true, nil
+ }
+ }
+ return nil, false, nil
+}
+
+// asOfEntry returns the value in effect at ts from a txNum-ascending history
+// (the entry with the greatest txNum < ts). ok is false when ts precedes the
+// first entry, i.e. the key did not exist yet.
+func asOfEntry(entries []dataWithTxNum, ts uint64) ([]byte, bool) {
+ for i := range entries {
+ if ts > entries[i].txNum && (i == len(entries)-1 || ts <= entries[i+1].txNum) {
+ return entries[i].data, true
+ }
+ }
+ return nil, false
+}
+
+// HistoryRange returns one entry per key changed in [fromTs, toTs), each with
+// its value just before the range, merging in-memory history with committed.
+// In-memory keys take precedence. Only used by the RPC test harness.
+func (sd *TemporalMemBatch) HistoryRange(ctx context.Context, domain kv.Domain, fromTs, toTs int, asc order.By, limit int, roTx kv.Tx) (stream.KV, error) {
+ committed, err := AggTx(roTx).HistoryRange(domain, fromTs, toTs, asc, limit, roTx)
+ if err != nil {
+ return nil, err
+ }
+ if !sd.inMemHistoryReads {
+ return committed, nil
+ }
+ mem := sd.memHistoryRange(domain, fromTs, toTs, asc, roTx)
+ if len(mem) == 0 {
+ return committed, nil
+ }
+ return stream.UnionKV(&memKVIter{pairs: mem}, committed, limit), nil
+}
+
+// memHistoryRange collects keys changed in [fromTs, toTs) from the in-memory
+// history, each paired with its pre-range value (in-memory value just before
+// fromTs, falling back to committed state when the key predates its in-memory
+// history). Empty pre-values are kept — they mean "did not exist before".
+func (sd *TemporalMemBatch) memHistoryRange(domain kv.Domain, fromTs, toTs int, asc order.By, roTx kv.Tx) []memKVPair {
+ sd.latestStateLock.RLock()
+ defer sd.latestStateLock.RUnlock()
+ from, to := uint64(fromTs), uint64(toTs)
+ var pairs []memKVPair
+ collect := func(k string, entries []dataWithTxNum) {
+ changed := false
+ for i := range entries {
+ if entries[i].txNum >= from && entries[i].txNum < to {
+ changed = true
+ break
+ }
+ }
+ if !changed {
+ return
+ }
+ pre, ok := asOfEntry(entries, from)
+ if !ok {
+ pre, _, _ = AggTx(roTx).GetAsOf(domain, common.ToBytesZeroCopy(k), from, roTx)
+ }
+ pairs = append(pairs, memKVPair{k: []byte(k), v: pre})
+ }
+ if domain == kv.StorageDomain {
+ sd.storage.Scan(func(k string, entries []dataWithTxNum) bool {
+ collect(k, entries)
+ return true
+ })
+ } else {
+ for k, entries := range sd.domains[domain] {
+ collect(k, entries)
+ }
+ }
+ sort.Slice(pairs, func(i, j int) bool {
+ if asc == order.Asc {
+ return bytes.Compare(pairs[i].k, pairs[j].k) < 0
+ }
+ return bytes.Compare(pairs[i].k, pairs[j].k) > 0
+ })
+ return pairs
+}
+
+// IndexRange returns the txNums at which key k appears in [fromTs, toTs),
+// merging the in-flight in-memory index with the committed inverted index.
+// Domain-backed indices derive their in-memory txNums from the domain history;
+// standalone indices (logs, traces) read them from the local ii collection.
+func (sd *TemporalMemBatch) IndexRange(name kv.InvertedIdx, k []byte, fromTs, toTs int, asc order.By, limit int, roTx kv.Tx) (stream.U64, error) {
+ committed, err := AggTx(roTx).IndexRange(name, k, fromTs, toTs, asc, limit, roTx)
+ if err != nil {
+ return nil, err
+ }
+ if !sd.inMemHistoryReads {
+ return committed, nil
+ }
+ at := AggTx(roTx)
+ var memTs []uint64
+ found := false
+ for i := range at.d {
+ if at.d[i].d.HistoryIdx == name {
+ memTs = sd.memIndexTxNums(kv.Domain(i), k, fromTs, toTs, asc)
+ found = true
+ break
+ }
+ }
+ if !found {
+ memTs = sd.memIndexTxNumsII(name, k, fromTs, toTs, asc)
+ }
+ if len(memTs) == 0 {
+ return committed, nil
+ }
+ return stream.Union[uint64](stream.Array(memTs), committed, asc, limit), nil
+}
+
+// memIndexTxNumsII reads the local standalone inverted-index collection for the
+// txNums of key k in [fromTs, toTs), deduplicated and ordered per asc.
+func (sd *TemporalMemBatch) memIndexTxNumsII(name kv.InvertedIdx, k []byte, fromTs, toTs int, asc order.By) []uint64 {
+ sd.latestStateLock.RLock()
+ defer sd.latestStateLock.RUnlock()
+ txNums := sd.iiMem[name][common.ToStringZeroCopy(k)]
+ out := make([]uint64, 0, len(txNums))
+ for _, tn := range txNums {
+ if !idxTxNumInRange(tn, fromTs, toTs, asc) {
+ continue
+ }
+ if len(out) > 0 && out[len(out)-1] == tn {
+ continue // ascending append order makes duplicates consecutive
+ }
+ out = append(out, tn)
+ }
+ if asc == order.Desc {
+ for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
+ out[i], out[j] = out[j], out[i]
+ }
+ }
+ return out
+}
+
+func (sd *TemporalMemBatch) memIndexTxNums(domain kv.Domain, k []byte, fromTs, toTs int, asc order.By) []uint64 {
+ sd.latestStateLock.RLock()
+ defer sd.latestStateLock.RUnlock()
+ ks := common.ToStringZeroCopy(k)
+ var entries []dataWithTxNum
+ if domain == kv.StorageDomain {
+ entries, _ = sd.storage.Get(ks)
+ } else {
+ entries = sd.domains[domain][ks]
+ }
+ out := make([]uint64, 0, len(entries))
+ for i := range entries {
+ if tn := entries[i].txNum; idxTxNumInRange(tn, fromTs, toTs, asc) {
+ out = append(out, tn)
+ }
+ }
+ if asc == order.Desc {
+ for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
+ out[i], out[j] = out[j], out[i]
+ }
+ }
+ return out
+}
+
+// idxTxNumInRange applies the inverted-index range bounds: asc = [fromTs, toTs),
+// desc = (toTs, fromTs]. A negative bound is unbounded.
+func idxTxNumInRange(tn uint64, fromTs, toTs int, asc order.By) bool {
+ if asc == order.Asc {
+ return (fromTs < 0 || tn >= uint64(fromTs)) && (toTs < 0 || tn < uint64(toTs))
+ }
+ return (fromTs < 0 || tn <= uint64(fromTs)) && (toTs < 0 || tn > uint64(toTs))
+}
+
+type memKVPair struct{ k, v []byte }
+
+type memKVIter struct {
+ pairs []memKVPair
+ i int
+}
+
+func (m *memKVIter) HasNext() bool { return m.i < len(m.pairs) }
+func (m *memKVIter) Close() {}
+func (m *memKVIter) Next() ([]byte, []byte, error) {
+ p := m.pairs[m.i]
+ m.i++
+ return p.k, p.v, nil
+}
+
+// liveLimitKV drops tombstone (empty-value) rows from the merged stream and
+// stops after limit live rows (limit < 0 means unlimited).
+type liveLimitKV struct {
+ inner stream.KV
+ limit int
+ seen int
+ k, v []byte
+ ready bool
+ err error
+}
+
+func (m *liveLimitKV) advance() {
+ if m.ready || m.err != nil {
+ return
+ }
+ if m.limit >= 0 && m.seen >= m.limit {
+ return
+ }
+ for m.inner.HasNext() {
+ k, v, err := m.inner.Next()
+ if err != nil {
+ m.err = err
+ return
+ }
+ if len(v) == 0 {
+ continue
+ }
+ m.k, m.v, m.ready = k, v, true
+ return
+ }
+}
+
+func (m *liveLimitKV) HasNext() bool { m.advance(); return m.ready || m.err != nil }
+func (m *liveLimitKV) Close() { m.inner.Close() }
+func (m *liveLimitKV) Next() ([]byte, []byte, error) {
+ m.advance()
+ if m.err != nil {
+ return nil, nil, m.err
+ }
+ m.ready = false
+ m.seen++
+ return m.k, m.v, nil
}
func (sd *TemporalMemBatch) SizeEstimate() uint64 {
@@ -370,6 +690,7 @@ func (sd *TemporalMemBatch) ClearRam() {
}
sd.storage = btree2.NewMap[string, []dataWithTxNum](128)
+ sd.iiMem = map[kv.InvertedIdx]map[string][]uint64{}
sd.unwindToTxNum = 0
sd.unwindChangeset = nil
sd.unwindChangesetRaw = nil
@@ -598,6 +919,24 @@ func (sd *TemporalMemBatch) Unwind(unwindToTxNum uint64, changeset *[kv.DomainLe
sd.storage.Set(e.key, e.kept)
}
}
+ for table, byKey := range sd.iiMem {
+ for k, txNums := range byKey {
+ kept := txNums[:0]
+ for _, tn := range txNums {
+ if tn < unwindToTxNum {
+ kept = append(kept, tn)
+ }
+ }
+ if len(kept) == 0 {
+ delete(byKey, k)
+ } else {
+ byKey[k] = kept
+ }
+ }
+ if len(byKey) == 0 {
+ delete(sd.iiMem, table)
+ }
+ }
var unwindChangeset *[kv.DomainLen]map[string]kv.DomainEntryDiff
var unwindChangesetRaw *[kv.DomainLen][]kv.DomainEntryDiff
@@ -628,12 +967,29 @@ func (sd *TemporalMemBatch) Unwind(unwindToTxNum uint64, changeset *[kv.DomainLe
func (sd *TemporalMemBatch) IndexAdd(table kv.InvertedIdx, key []byte, txNum uint64) (err error) {
for _, writer := range sd.iiWriters {
if writer.name == table {
+ if sd.inMemHistoryReads {
+ sd.iiMemAdd(table, key, txNum)
+ }
return writer.Add(key, txNum)
}
}
panic(fmt.Errorf("unknown index %s", table))
}
+func (sd *TemporalMemBatch) iiMemAdd(table kv.InvertedIdx, key []byte, txNum uint64) {
+ sd.latestStateLock.Lock()
+ defer sd.latestStateLock.Unlock()
+ byKey := sd.iiMem[table]
+ if byKey == nil {
+ byKey = map[string][]uint64{}
+ sd.iiMem[table] = byKey
+ }
+ ks := string(key)
+ // IndexAdd is called in ascending txNum order during execution, so append
+ // keeps each key's txNum slice sorted.
+ byKey[ks] = append(byKey[ks], txNum)
+}
+
func (sd *TemporalMemBatch) Close() {
for _, d := range sd.domainWriters {
if d != nil {
diff --git a/execution/abi/bind/backends/simulated.go b/execution/abi/bind/backends/simulated.go
index 9c894aa0cf5..459c881c837 100644
--- a/execution/abi/bind/backends/simulated.go
+++ b/execution/abi/bind/backends/simulated.go
@@ -109,7 +109,7 @@ func NewSimulatedBackendWithConfig(t *testing.T, alloc types.GenesisAlloc, confi
m: m,
prependBlock: m.Genesis,
getHeader: func(hash common.Hash, number uint64) (h *types.Header, err error) {
- err = m.DB.View(context.Background(), func(tx kv.Tx) error {
+ err = m.OverlayDB().View(context.Background(), func(tx kv.Tx) error {
h, err = m.BlockReader.Header(context.Background(), tx, hash, number)
return nil
})
@@ -161,6 +161,10 @@ func (b *SimulatedBackend) Commit() {
}); err != nil {
panic(err)
}
+ // Make the just-committed block durable before generating the next pending
+ // block on it: the generation below reads prependBlock's committed state
+ // with a fresh SharedDomains, so it must not race the background commit.
+ b.m.ExecModule.WaitCommitsDrained()
//nolint:prealloc
var allLogs []*types.Log
for _, r := range b.pendingReceipts {
@@ -192,7 +196,7 @@ func (b *SimulatedBackend) emptyPendingBlock() {
if b.pendingState != nil {
b.pendingState.Close()
}
- tx, err := b.m.DB.BeginTemporalRo(context.Background()) //nolint:gocritic
+ tx, err := b.m.OverlayDB().BeginTemporalRo(context.Background()) //nolint:gocritic
if err != nil {
panic(err)
}
@@ -213,7 +217,7 @@ func (b *SimulatedBackend) stateByBlockNumber(db kv.TemporalTx, blockNumber *uin
func (b *SimulatedBackend) CodeAt(ctx context.Context, contract common.Address, blockNumber *uint256.Int) ([]byte, error) {
b.mu.Lock()
defer b.mu.Unlock()
- tx, err := b.m.DB.BeginTemporalRo(context.Background())
+ tx, err := b.m.OverlayDB().BeginTemporalRo(context.Background())
if err != nil {
return nil, err
}
@@ -226,7 +230,7 @@ func (b *SimulatedBackend) CodeAt(ctx context.Context, contract common.Address,
func (b *SimulatedBackend) BalanceAt(ctx context.Context, contract common.Address, blockNumber *uint256.Int) (*uint256.Int, error) {
b.mu.Lock()
defer b.mu.Unlock()
- tx, err := b.m.DB.BeginTemporalRo(context.Background())
+ tx, err := b.m.OverlayDB().BeginTemporalRo(context.Background())
if err != nil {
return nil, err
}
@@ -240,7 +244,7 @@ func (b *SimulatedBackend) BalanceAt(ctx context.Context, contract common.Addres
func (b *SimulatedBackend) NonceAt(ctx context.Context, contract common.Address, blockNumber *uint256.Int) (uint64, error) {
b.mu.Lock()
defer b.mu.Unlock()
- tx, err := b.m.DB.BeginTemporalRo(context.Background())
+ tx, err := b.m.OverlayDB().BeginTemporalRo(context.Background())
if err != nil {
return 0, err
}
@@ -254,7 +258,7 @@ func (b *SimulatedBackend) NonceAt(ctx context.Context, contract common.Address,
func (b *SimulatedBackend) StorageAt(ctx context.Context, contract common.Address, key common.Hash, blockNumber *uint256.Int) ([]byte, error) {
b.mu.Lock()
defer b.mu.Unlock()
- tx, err := b.m.DB.BeginTemporalRo(context.Background())
+ tx, err := b.m.OverlayDB().BeginTemporalRo(context.Background())
if err != nil {
return nil, err
}
@@ -273,7 +277,7 @@ func (b *SimulatedBackend) TransactionReceipt(ctx context.Context, txHash common
b.mu.Lock()
defer b.mu.Unlock()
- tx, err := b.m.DB.BeginTemporalRo(context.Background())
+ tx, err := b.m.OverlayDB().BeginTemporalRo(context.Background())
if err != nil {
return nil, err
}
@@ -317,7 +321,7 @@ func (b *SimulatedBackend) TransactionByHash(ctx context.Context, txHash common.
b.mu.Lock()
defer b.mu.Unlock()
- tx, err := b.m.DB.BeginRo(ctx)
+ tx, err := b.m.OverlayDB().BeginRo(ctx)
if err != nil {
return nil, false, err
}
@@ -364,7 +368,7 @@ func (b *SimulatedBackend) BlockByHash(ctx context.Context, hash common.Hash) (*
if hash == b.pendingBlock.Hash() {
return b.pendingBlock, nil
}
- tx, err := b.m.DB.BeginRo(ctx)
+ tx, err := b.m.OverlayDB().BeginRo(ctx)
if err != nil {
return nil, err
}
@@ -398,7 +402,7 @@ func (b *SimulatedBackend) blockByNumberNoLock(ctx context.Context, number *uint
return b.prependBlock, nil
}
- tx, err := b.m.DB.BeginRo(context.Background())
+ tx, err := b.m.OverlayDB().BeginRo(context.Background())
if err != nil {
return nil, err
}
@@ -423,7 +427,7 @@ func (b *SimulatedBackend) HeaderByHash(ctx context.Context, hash common.Hash) (
if hash == b.pendingBlock.Hash() {
return b.pendingBlock.Header(), nil
}
- tx, err := b.m.DB.BeginRo(context.Background())
+ tx, err := b.m.OverlayDB().BeginRo(context.Background())
if err != nil {
return nil, err
}
@@ -449,7 +453,7 @@ func (b *SimulatedBackend) HeaderByHash(ctx context.Context, hash common.Hash) (
func (b *SimulatedBackend) HeaderByNumber(ctx context.Context, number *uint256.Int) (*types.Header, error) {
b.mu.Lock()
defer b.mu.Unlock()
- tx, err := b.m.DB.BeginRo(context.Background())
+ tx, err := b.m.OverlayDB().BeginRo(context.Background())
if err != nil {
return nil, err
}
@@ -474,7 +478,7 @@ func (b *SimulatedBackend) TransactionCount(ctx context.Context, blockHash commo
if blockHash == b.pendingBlock.Hash() {
return uint(b.pendingBlock.Transactions().Len()), nil
}
- tx, err := b.m.DB.BeginRo(context.Background())
+ tx, err := b.m.OverlayDB().BeginRo(context.Background())
if err != nil {
return 0, err
}
@@ -508,7 +512,7 @@ func (b *SimulatedBackend) TransactionInBlock(ctx context.Context, blockHash com
return transactions[index], nil
}
- tx, err := b.m.DB.BeginRo(context.Background())
+ tx, err := b.m.OverlayDB().BeginRo(context.Background())
if err != nil {
return nil, err
}
@@ -582,7 +586,7 @@ func (b *SimulatedBackend) CallContract(ctx context.Context, call bind.CallMsg,
return nil, errBlockNumberUnsupported
}
var res *evmtypes.ExecutionResult
- if err := b.m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) (err error) {
+ if err := b.m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) (err error) {
s := state.New(b.m.NewStateReader(tx))
res, err = b.callContract(ctx, call, b.pendingBlock, s)
if err != nil {
diff --git a/execution/builder/block_builder.go b/execution/builder/block_builder.go
index 431246a628d..5715bbe3bb8 100644
--- a/execution/builder/block_builder.go
+++ b/execution/builder/block_builder.go
@@ -37,12 +37,26 @@ type BlockBuilder struct {
done chan struct{}
result *types.BlockWithReceipts
err error
+ view ScopedReadView // pinned by-block read snapshot; released when the build goroutine exits
}
+// Cancel signals the build to stop; the goroutine then exits and releases its
+// scoped read view. Non-blocking (used on eviction).
+func (b *BlockBuilder) Cancel() { b.interrupt.Store(true) }
+
func NewBlockBuilder(build BlockBuilderFunc, param *Parameters, maxBuildTimeSecs uint64) *BlockBuilder {
builder := &BlockBuilder{done: make(chan struct{})}
+ builder.view = param.ScopedView
go func() {
+ // Release the pinned read snapshot (its roTx) when the build goroutine
+ // exits — on success, error, timeout-driven Stop, or panic. Registered
+ // before the result defer, so it runs after done is closed. Idempotent.
+ defer func() {
+ if builder.view != nil {
+ builder.view.Release()
+ }
+ }()
var result *types.BlockWithReceipts
var err error
diff --git a/execution/builder/builder.go b/execution/builder/builder.go
index 5150348b674..a640ff3ce41 100644
--- a/execution/builder/builder.go
+++ b/execution/builder/builder.go
@@ -37,10 +37,6 @@ import (
"github.com/erigontech/erigon/txnprovider"
)
-// SDProvider returns the latest published SharedDomains from FCU, or nil if none.
-// Used by the builder to read uncommitted state during background commits.
-type SDProvider func() *execctx.SharedDomains
-
// Builder runs the three block-building steps (createBlock, execBlock, finishBlock) directly
// without staged-sync machinery. Its Build method satisfies BlockBuilderFunc and can
// be passed directly to ExecModule.
@@ -59,7 +55,6 @@ type Builder struct {
txnProvider txnprovider.TxnProvider
sealCancel chan struct{}
latestBlockBuiltStore *LatestBlockBuiltStore
- sdProvider SDProvider
logger log.Logger
}
@@ -77,7 +72,6 @@ func NewBuilder(
txnProvider txnprovider.TxnProvider,
sealCancel chan struct{},
latestBlockBuiltStore *LatestBlockBuiltStore,
- sdProvider SDProvider,
logger log.Logger,
) *Builder {
return &Builder{
@@ -95,7 +89,6 @@ func NewBuilder(
txnProvider: txnProvider,
sealCancel: sealCancel,
latestBlockBuiltStore: latestBlockBuiltStore,
- sdProvider: sdProvider,
logger: logger,
}
}
@@ -124,24 +117,29 @@ func (b *Builder) Build(param *Parameters, interrupt *atomic.Bool) (result *type
BuiltBlock: &exec.AssembledBlock{},
}
- tx, err := b.db.BeginTemporalRo(b.ctx)
- if err != nil {
- return nil, err
+ // Read only through the scoped, by-block read view captured by AssembleBlock
+ // (pinned to param.ParentHash): its roTx is the consistent committed-state
+ // snapshot and its head SharedDomains carries the in-flight tip state. The
+ // builder never opens its own db.BeginTemporalRo nor reads the mutable global
+ // published SD. The view's roTx is released by the BlockBuilder goroutine.
+ view := param.ScopedView
+ if view == nil {
+ return nil, fmt.Errorf("builder: nil scoped read view")
}
- defer tx.Rollback()
+ tx := view.Tx()
- // When a published SD is available (background commit in progress), create
- // a child SD that reads domain state from the parent's mem batch and table
- // data from the parent's overlay. The child's own writes are local and
+ // Create a child SD that reads domain state from the head's mem batch and
+ // table data from its overlay. The child's own writes are local and
// discarded after block construction.
var compositeTx kv.TemporalTx = tx
- var parentSD *execctx.SharedDomains
- if b.sdProvider != nil {
- parentSD = b.sdProvider()
- }
+ parentSD := view.HeadSD()
if parentSD != nil {
- if overlay := parentSD.BlockOverlay(); overlay != nil {
- compositeTx = overlay.NewReadView(tx)
+ // Read block metadata (and stage progress) through the full parent
+ // generation chain, not just parentSD's local overlay — otherwise block
+ // data held in an ancestor generation is missed and executionAt reads
+ // stale, which desyncs the builder from the txpool's head.
+ if v := parentSD.OverlayTemporalTx(tx); v != nil {
+ compositeTx = v
}
}
@@ -175,7 +173,7 @@ func (b *Builder) Build(param *Parameters, interrupt *atomic.Bool) (result *type
if err := createBlock(b.ctx, sd, compositeTx, executionAt, createCfg, b.logger); err != nil {
return nil, err
}
- if err := execBlock(b.ctx, sd, compositeTx, executionAt, execCfg, b.executeBlockCfg, b.logger); err != nil {
+ if err := execBlock(b.ctx, sd, parentSD, compositeTx, executionAt, execCfg, b.executeBlockCfg, b.logger); err != nil {
return nil, err
}
if err := finishBlock(compositeTx, finishCfg, b.logger); err != nil {
diff --git a/execution/builder/builder_test.go b/execution/builder/builder_test.go
index f7c758522fe..8c7560e1856 100644
--- a/execution/builder/builder_test.go
+++ b/execution/builder/builder_test.go
@@ -18,45 +18,32 @@ package builder
import (
"context"
- "errors"
"sync/atomic"
"testing"
"github.com/stretchr/testify/require"
"github.com/erigontech/erigon/common/log/v3"
- "github.com/erigontech/erigon/db/kv"
"github.com/erigontech/erigon/execution/builder/buildercfg"
"github.com/erigontech/erigon/execution/types"
)
-// errDB is a minimal kv.TemporalRoDB stub whose BeginTemporalRo always fails.
-// Other methods are never reached because Build returns at the first error.
-type errDB struct {
- kv.TemporalRoDB // nil embed satisfies the interface; other methods must not be called
- err error
-}
-
-func (e *errDB) BeginTemporalRo(_ context.Context) (kv.TemporalTx, error) {
- return nil, e.err
-}
-
-// TestBuilder_Build_DBError verifies that Build propagates a BeginTemporalRo error
-// immediately, without panicking or hanging on a channel read.
-func TestBuilder_Build_DBError(t *testing.T) {
+// TestBuilder_Build_NilScopedView verifies that Build fails fast (no panic, no
+// hang) when AssembleBlock attached no scoped read view — the builder reads only
+// through that view and must not fall back to opening its own DB snapshot.
+func TestBuilder_Build_NilScopedView(t *testing.T) {
t.Parallel()
- want := errors.New("db open failed")
b := &Builder{
ctx: context.Background(),
- db: &errDB{err: want},
builderCfg: &buildercfg.BuilderConfig{},
pendingBlockCh: make(chan *types.Block, 1),
logger: log.New(),
}
_, err := b.Build(&Parameters{}, &atomic.Bool{})
- require.ErrorIs(t, err, want)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "scoped read view")
}
// TestBuilder_PendingBlockCh verifies that NewBuilder initialises a non-nil buffered
diff --git a/execution/builder/exec.go b/execution/builder/exec.go
index fd8112e9be6..b186f83b577 100644
--- a/execution/builder/exec.go
+++ b/execution/builder/exec.go
@@ -97,7 +97,7 @@ func StageBuilderExecCfg(
//
// TODO:
// - resubmitAdjustCh - variable is not implemented
-func execBlock(ctx context0.Context, sd *execctx.SharedDomains, tx kv.TemporalTx, executionAt uint64, cfg BuilderExecCfg, execCfg stagedsync.ExecuteBlockCfg, logger log.Logger) (err error) {
+func execBlock(ctx context0.Context, sd *execctx.SharedDomains, parentSD *execctx.SharedDomains, tx kv.TemporalTx, executionAt uint64, cfg BuilderExecCfg, execCfg stagedsync.ExecuteBlockCfg, logger log.Logger) (err error) {
const logPrefix = "BuilderExec"
// Copy vmConfig to avoid mutating the shared struct across concurrent Build calls.
@@ -133,6 +133,16 @@ func execBlock(ctx context0.Context, sd *execctx.SharedDomains, tx kv.TemporalTx
return err
}
defer filterSd.Close()
+ // Chain filterSd to the parent generation (gate item 2): like the main
+ // sd, the filter must read the latest executed block's not-yet-committed
+ // domain state. Without this, filterBadTransactions resolves the sender
+ // nonce from committed files only — stale during a background commit —
+ // and drops valid spill-over txns as nonce-gapped (block built empty).
+ // filterSd's own speculative writes stay in filterSd.mem; the parent is
+ // read-only here, so sd's commitment is unaffected.
+ if parentSD != nil {
+ filterSd.SetParent(parentSD)
+ }
filterWriter := state.NewWriter(filterSd.AsPutDel(filterMb), nil, txNum)
filterReader := state.NewReaderV3(filterSd.AsGetter(filterMb))
diff --git a/execution/builder/parameters.go b/execution/builder/parameters.go
index 10d063fd37d..8059e100692 100644
--- a/execution/builder/parameters.go
+++ b/execution/builder/parameters.go
@@ -18,13 +18,31 @@ package builder
import (
"github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/db/state/execctx"
"github.com/erigontech/erigon/execution/types"
"github.com/erigontech/erigon/txnprovider"
)
+// ScopedReadView is a consistent, by-block read snapshot handed to the builder
+// so it reads the exact block it builds on (never the raw DB directly nor a
+// mutable global). Implemented by execmodule.ScopedReadView. The build owns it
+// and releases it when it finishes.
+type ScopedReadView interface {
+ Tx() kv.TemporalTx
+ HeadSD() *execctx.SharedDomains
+ BlockHash() common.Hash
+ BlockNum() uint64
+ Release()
+}
+
// Parameters for PoS block building
// See also https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#payloadattributesv4
type Parameters struct {
+ // ScopedView is the consistent read snapshot pinned to ParentHash. Set by
+ // AssembleBlock; the build reads only through it and releases it on finish.
+ ScopedView ScopedReadView
+
PayloadId uint64
ParentHash common.Hash
Timestamp uint64
diff --git a/execution/engineapi/engine_server_test.go b/execution/engineapi/engine_server_test.go
index a2d89da18f2..92884637964 100644
--- a/execution/engineapi/engine_server_test.go
+++ b/execution/engineapi/engine_server_test.go
@@ -36,7 +36,6 @@ import (
"github.com/erigontech/erigon/common/log/v3"
"github.com/erigontech/erigon/db/kv"
"github.com/erigontech/erigon/db/kv/kvcache"
- "github.com/erigontech/erigon/db/rawdb"
"github.com/erigontech/erigon/execution/chain"
"github.com/erigontech/erigon/execution/execmodule/execmoduletester"
"github.com/erigontech/erigon/execution/tests/blockgen"
@@ -50,13 +49,14 @@ import (
)
// Do 1 step to start txPool
-func oneBlockSteps(m *execmoduletester.ExecModuleTester, require *require.Assertions, blocks int) {
+func oneBlockSteps(m *execmoduletester.ExecModuleTester, require *require.Assertions, blocks int) *blockgen.ChainPack {
chain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, blocks, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{1})
})
require.NoError(err)
err = m.InsertChain(chain)
require.NoError(err)
+ return chain
}
// Do 1 step to start txPool
@@ -290,65 +290,35 @@ func TestGetBlobsV3(t *testing.T) {
}
}
-func canonicalHashAt(t *testing.T, db kv.TemporalRoDB, blockNum uint64) common.Hash {
- t.Helper()
- var hash common.Hash
- err := db.View(context.Background(), func(tx kv.Tx) error {
- var err error
- hash, err = rawdb.ReadCanonicalHash(tx, blockNum)
- return err
- })
- require.NoError(t, err)
- return hash
-}
-
-func writeBlockAccessListBytes(t *testing.T, db kv.TemporalRwDB, blockHash common.Hash, blockNum uint64, balBytes []byte) {
- t.Helper()
- err := db.Update(context.Background(), func(tx kv.RwTx) error {
- return rawdb.WriteBlockAccessListBytes(tx, blockHash, blockNum, balBytes)
- })
- require.NoError(t, err)
-}
-
func TestGetPayloadBodiesByHashV2(t *testing.T) {
mockSentry := execmoduletester.New(t, execmoduletester.WithTxPool(), execmoduletester.WithChainConfig(chain.AllProtocolChanges))
req := require.New(t)
- oneBlockStep(mockSentry, req)
+ // Insert a block carrying its BAL through the unified InsertChain path (BAL
+ // stored in the overlay, flushed at commit); serving must reflect that BAL.
+ chain := oneBlockSteps(mockSentry, req, 1)
executionRpc := mockSentry.ExecModule
maxReorgDepth := ethconfig.Defaults.MaxReorgDepth
engineServer := NewEngineServer(mockSentry.Log, mockSentry.ChainConfig, executionRpc, nil, false, false, false, true, nil, nil, ethconfig.Defaults.FcuTimeout, maxReorgDepth)
- const blockNum = 1
- blockHash := canonicalHashAt(t, mockSentry.DB, blockNum)
+ blockHash := chain.Blocks[0].Hash()
req.NotEqual(common.Hash{}, blockHash)
+ req.NotEmpty(chain.BlockAccessLists[0], "Amsterdam block must carry a BAL from GenerateChain")
ctx := context.Background()
- // Amsterdam-enabled chains always have a BAL written by GenerateChain
bodies, err := engineServer.GetPayloadBodiesByHashV2(ctx, []common.Hash{blockHash})
req.NoError(err)
req.Len(bodies, 1)
req.NotNil(bodies[0])
req.NotNil(bodies[0].BlockAccessList)
- req.NotEmpty(*bodies[0].BlockAccessList)
-
- // Overwrite with a non-empty BAL and verify it's returned
- balBytes := []byte{0x01, 0x02, 0x03}
- writeBlockAccessListBytes(t, mockSentry.DB, blockHash, blockNum, balBytes)
-
- bodies, err = engineServer.GetPayloadBodiesByHashV2(ctx, []common.Hash{blockHash})
- req.NoError(err)
- req.Len(bodies, 1)
- req.NotNil(bodies[0])
- req.NotNil(bodies[0].BlockAccessList)
- req.Equal(hexutil.Bytes(balBytes), *bodies[0].BlockAccessList)
+ req.Equal(hexutil.Bytes(chain.BlockAccessLists[0]), *bodies[0].BlockAccessList)
}
func TestGetPayloadBodiesByRangeV2(t *testing.T) {
mockSentry := execmoduletester.New(t, execmoduletester.WithTxPool(), execmoduletester.WithChainConfig(chain.AllProtocolChanges))
req := require.New(t)
- oneBlockSteps(mockSentry, req, 2)
+ chain := oneBlockSteps(mockSentry, req, 2)
executionRpc := mockSentry.ExecModule
maxReorgDepth := ethconfig.Defaults.MaxReorgDepth
@@ -358,14 +328,12 @@ func TestGetPayloadBodiesByRangeV2(t *testing.T) {
start = 1
count = 2
)
- blockHash1 := canonicalHashAt(t, mockSentry.DB, start)
- blockHash2 := canonicalHashAt(t, mockSentry.DB, start+1)
- req.NotEqual(common.Hash{}, blockHash1)
- req.NotEqual(common.Hash{}, blockHash2)
+ req.NotEmpty(chain.BlockAccessLists[0], "Amsterdam block must carry a BAL from GenerateChain")
+ req.NotEmpty(chain.BlockAccessLists[1], "Amsterdam block must carry a BAL from GenerateChain")
ctx := context.Background()
- // Amsterdam-enabled chains always have a BAL written by GenerateChain
+ // Serving must reflect the BALs inserted with the blocks (unified path).
bodies, err := engineServer.GetPayloadBodiesByRangeV2(ctx, start, count)
req.NoError(err)
req.Len(bodies, 2)
@@ -373,22 +341,6 @@ func TestGetPayloadBodiesByRangeV2(t *testing.T) {
req.NotNil(bodies[1])
req.NotNil(bodies[0].BlockAccessList)
req.NotNil(bodies[1].BlockAccessList)
- req.NotEmpty(*bodies[0].BlockAccessList)
- req.NotEmpty(*bodies[1].BlockAccessList)
-
- // Overwrite with non-empty BALs and verify they're returned
- balBytes1 := []byte{0x01, 0x02, 0x03}
- balBytes2 := []byte{0x04, 0x05, 0x06}
- writeBlockAccessListBytes(t, mockSentry.DB, blockHash1, start, balBytes1)
- writeBlockAccessListBytes(t, mockSentry.DB, blockHash2, start+1, balBytes2)
-
- bodies, err = engineServer.GetPayloadBodiesByRangeV2(ctx, start, count)
- req.NoError(err)
- req.Len(bodies, 2)
- req.NotNil(bodies[0])
- req.NotNil(bodies[1])
- req.NotNil(bodies[0].BlockAccessList)
- req.NotNil(bodies[1].BlockAccessList)
- req.Equal(hexutil.Bytes(balBytes1), *bodies[0].BlockAccessList)
- req.Equal(hexutil.Bytes(balBytes2), *bodies[1].BlockAccessList)
+ req.Equal(hexutil.Bytes(chain.BlockAccessLists[0]), *bodies[0].BlockAccessList)
+ req.Equal(hexutil.Bytes(chain.BlockAccessLists[1]), *bodies[1].BlockAccessList)
}
diff --git a/execution/exec/blocks_read_ahead.go b/execution/exec/blocks_read_ahead.go
index 87c44d3664b..6d3388f5abf 100644
--- a/execution/exec/blocks_read_ahead.go
+++ b/execution/exec/blocks_read_ahead.go
@@ -17,7 +17,8 @@ import (
"github.com/erigontech/erigon/db/dbservices"
"github.com/erigontech/erigon/db/kv"
"github.com/erigontech/erigon/db/kv/dbutils"
- "github.com/erigontech/erigon/execution/cache"
+ "github.com/erigontech/erigon/db/state/execctx"
+ "github.com/erigontech/erigon/db/state/kvmetrics"
"github.com/erigontech/erigon/execution/protocol/rules"
"github.com/erigontech/erigon/execution/state"
"github.com/erigontech/erigon/execution/types"
@@ -34,13 +35,12 @@ type BlockReadAheader struct {
warming atomic.Bool // only one warmBody can run at a time
warmWg sync.WaitGroup
- // stateCache is the process-global state cache that SharedDomains.GetLatest
- // consults on the EVM hot path. When set, warmBody routes its prefetches
- // through a cache-populating getter so the same hashmap the EVM probes is
- // pre-warmed. Without it, prefetches only warm OS page cache + RoTx
- // cursors — disconnected from the cache layer the EVM actually reads.
- // Mirrors reth's CachedReads / ExecutionCache "same hashmap" property.
- stateCache *cache.StateCache
+ // publishedSD returns the latest published SharedDomains (the stable tip leaf).
+ // When set, warmBody prefetches through it so reads see in-flight tip state and
+ // the SD's own read-fill warms the process-global cache the EVM probes — keeping
+ // cache population an SD concern. A nil provider (or nil return) falls back to a
+ // raw read that does not touch the cache.
+ publishedSD func() *execctx.SharedDomains
}
func NewBlockReadAheader() *BlockReadAheader {
@@ -63,43 +63,30 @@ func NewBlockReadAheader() *BlockReadAheader {
}
}
-// SetStateCache wires the process-global state cache so warmBody's
-// prefetches land in the same hashmap that SharedDomains.GetLatest probes
-// on the EVM hot path. Without this, prefetches warm OS page cache only —
-// the EVM still pays the file accessor stack on its first per-address read.
-// Idempotent; safe to call before the first AddHeaderAndBody.
-func (bra *BlockReadAheader) SetStateCache(sc *cache.StateCache) {
- bra.stateCache = sc
+// SetPublishedSD wires the published-SharedDomains provider so warmBody
+// prefetches read through the in-flight tip state and the SD's read-fill warms
+// the process-global cache the EVM probes. Idempotent; safe to call before the
+// first AddHeaderAndBody.
+func (bra *BlockReadAheader) SetPublishedSD(provider func() *execctx.SharedDomains) {
+ bra.publishedSD = provider
}
-// cachePopulatingGetter wraps a kv.TemporalGetter and fills a StateCache
-// ReadView as a side effect. Used by warmBody to make read-ahead prefetches
-// populate the same in-process cache layer that SharedDomains.GetLatest
-// consults — eliminating the file-accessor stack cost on the EVM's first
-// touch of any prefetched address.
-//
-// Code reads also populate the content-addressed and size-cache layers.
-type cachePopulatingGetter struct {
- kv.TemporalGetter
- view cache.ReadView
- stepSize uint64 // for the read txNum upper bound (last txNum of the read's step)
-}
-
-func readAheadGetter(ttx kv.TemporalTx, sc *cache.StateCache) kv.TemporalGetter {
- if sc == nil {
- return ttx
+// warmView opens a per-goroutine read view for prefetching. A prefetch is a
+// fire-and-forget warmer, so it takes a plain (uncoordinated) RO snapshot — no
+// foreground-mutex traffic on the tip path. With a published SD it reads through
+// it (in-flight tip state, and the SD's own read-fill warms the process-global
+// cache); without one it reads the raw tx and populates nothing. A throwaway
+// per-worker metrics accumulator keeps concurrent workers off the SD's shared
+// request metrics.
+func warmView(ctx context.Context, tdb kv.TemporalRoDB, sd *execctx.SharedDomains) (kv.TemporalTx, kv.TemporalGetter, error) {
+ ttx, err := tdb.BeginTemporalRo(ctx) //nolint:gocritic // tx is returned to the caller, which defers Rollback
+ if err != nil {
+ return nil, nil, err
}
- debug := ttx.Debug()
- return &cachePopulatingGetter{TemporalGetter: ttx, view: sc.View(debug), stepSize: debug.StepSize()}
-}
-
-func (cpg *cachePopulatingGetter) GetLatest(name kv.Domain, k []byte) ([]byte, kv.Step, error) {
- v, step, err := cpg.TemporalGetter.GetLatest(name, k)
- if err == nil {
- readTxNum := (uint64(step)+1)*cpg.stepSize - 1
- cpg.view.Fill(name, k, v, readTxNum)
+ if sd != nil {
+ return ttx, sd.AsGetterMetered(ttx, kvmetrics.NewDomainMetrics()), nil
}
- return v, step, err
+ return ttx, ttx, nil
}
func (bra *BlockReadAheader) AddHeaderAndBody(ctx context.Context, db kv.RoDB, header *types.Header, body *types.Body) {
@@ -154,16 +141,36 @@ func (bra *BlockReadAheader) warmBody(ctx context.Context, db kv.RoDB, header *t
workers = 1
}
+ tdb, ok := db.(kv.TemporalRoDB)
+ if !ok {
+ return
+ }
+ // Capture the published tip leaf once; all workers share it (concurrent reads
+ // are safe on the stable published SD) and each opens its own read tx. SetHead
+ // drains in-flight warmup before closing generations, so the captured leaf
+ // can't be closed out from under a worker.
+ var sd *execctx.SharedDomains
+ if bra.publishedSD != nil {
+ sd = bra.publishedSD()
+ }
+
var wg errgroup.Group
- // If BAL exists in DB, use BAL warming (more complete)
+ // If a BAL exists, use it (more complete). Read it through the SD's block
+ // overlay so an in-flight tip BAL is visible before its commit lands.
var bal types.BlockAccessList
- if header != nil && db != nil {
- tx, err := db.BeginRo(ctx)
+ if header != nil {
+ btx, err := tdb.BeginTemporalRo(ctx)
if err != nil {
log.Warn("[warmBody] failed to open tx for BAL", "blockNum", header.Number.Uint64(), "blockHash", header.Hash(), "err", err)
} else {
- data, err := tx.GetOne(kv.BlockAccessList, dbutils.BlockBodyKey(header.Number.Uint64(), header.Hash()))
+ var balSrc kv.TemporalTx = btx
+ if sd != nil {
+ if ov := sd.OverlayTemporalTx(btx); ov != nil {
+ balSrc = ov
+ }
+ }
+ data, err := balSrc.GetOne(kv.BlockAccessList, dbutils.BlockBodyKey(header.Number.Uint64(), header.Hash()))
if err != nil {
log.Warn("[warmBody] failed to read BAL", "blockNum", header.Number.Uint64(), "blockHash", header.Hash(), "err", err)
} else if len(data) > 0 {
@@ -172,7 +179,7 @@ func (bra *BlockReadAheader) warmBody(ctx context.Context, db kv.RoDB, header *t
log.Warn("[warmBody] failed to decode BAL", "blockNum", header.Number.Uint64(), "blockHash", header.Hash(), "err", err)
}
}
- tx.Rollback()
+ btx.Rollback()
}
}
@@ -194,17 +201,12 @@ func (bra *BlockReadAheader) warmBody(ctx context.Context, db kv.RoDB, header *t
workerStart, workerEnd, workerID := start, end, w
wg.Go(func() error {
startTime := time.Now()
- tx, err := db.BeginRo(ctx)
+ ttx, getter, err := warmView(ctx, tdb, sd)
if err != nil {
return err
}
- defer tx.Rollback()
-
- ttx, ok := tx.(kv.TemporalTx)
- if !ok {
- return nil
- }
- stateReader := state.NewReaderV3(readAheadGetter(ttx, bra.stateCache))
+ defer ttx.Rollback()
+ stateReader := state.NewReaderV3(getter)
for idx := workerStart; idx < workerEnd; idx++ {
select {
@@ -260,17 +262,12 @@ func (bra *BlockReadAheader) warmBody(ctx context.Context, db kv.RoDB, header *t
workerStart, workerEnd, workerID := start, end, w
wg.Go(func() error {
startTime := time.Now()
- tx, err := db.BeginRo(ctx)
+ ttx, getter, err := warmView(ctx, tdb, sd)
if err != nil {
return err
}
- defer tx.Rollback()
-
- ttx, ok := tx.(kv.TemporalTx)
- if !ok {
- return nil
- }
- stateReader := state.NewReaderV3(readAheadGetter(ttx, bra.stateCache))
+ defer ttx.Rollback()
+ stateReader := state.NewReaderV3(getter)
for txIdx := workerStart; txIdx < workerEnd; txIdx++ {
select {
diff --git a/execution/exec/blocks_read_ahead_test.go b/execution/exec/blocks_read_ahead_test.go
deleted file mode 100644
index b5d153feef3..00000000000
--- a/execution/exec/blocks_read_ahead_test.go
+++ /dev/null
@@ -1,187 +0,0 @@
-// 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 exec
-
-import (
- "testing"
-
- "github.com/c2h5oh/datasize"
- "github.com/stretchr/testify/require"
-
- "github.com/erigontech/erigon/common/crypto"
- "github.com/erigontech/erigon/db/kv"
- "github.com/erigontech/erigon/execution/cache"
-)
-
-// stubTemporalGetter stands in for the committed-state read view a warmup
-// goroutine reads: every GetLatest returns the same fixed value.
-type stubTemporalGetter struct {
- v []byte
- step kv.Step
-}
-
-func (s stubTemporalGetter) GetLatest(kv.Domain, []byte) ([]byte, kv.Step, error) {
- return s.v, s.step, nil
-}
-
-func (s stubTemporalGetter) HasPrefix(kv.Domain, []byte) ([]byte, []byte, bool, error) {
- return nil, nil, false, nil
-}
-
-func (s stubTemporalGetter) StepsInFiles(...kv.Domain) kv.Step { return 0 }
-
-func newTestStateCache() *cache.StateCache {
- b := 1 * datasize.MB
- return cache.NewStateCache(b, b, b, b)
-}
-
-// seedFill places an entry with an exact txNum stamp through the public fill
-// API without moving the applied frontier.
-func seedFill(sc *cache.StateCache, domain kv.Domain, k, v []byte, txNum uint64) {
- sc.View(cache.FrontierFunc(func(kv.Domain) (uint64, bool) { return txNum + 1, true })).Fill(domain, k, v, txNum)
-}
-
-// A warmup read-through must never replace a fresher entry an authoritative
-// writer (the FCU flush cache-apply) has already put: the warmup reads a
-// pre-flush read view, so a laggard Put landing after the flush would pin
-// stale state in the cache and corrupt the next block's execution.
-func TestCachePopulatingGetterKeepsFresherEntry(t *testing.T) {
- key := []byte("\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44")
- fresh := []byte("account-record-nonce-5")
- stale := []byte("account-record-nonce-4")
- for _, domain := range []kv.Domain{kv.AccountsDomain, kv.StorageDomain} {
- sc := newTestStateCache()
- seedFill(sc, domain, key, fresh, 54)
- cpg := &cachePopulatingGetter{TemporalGetter: stubTemporalGetter{v: stale}, view: sc.View(cache.FrontierFunc(emptyVisibleEnd)), stepSize: 1_562_500}
-
- v, _, err := cpg.GetLatest(domain, key)
- require.NoError(t, err)
- require.Equal(t, stale, v, "read-through must still return the view's value")
-
- got, ok := sc.View(nil).Get(domain, key)
- require.True(t, ok, "domain %s", domain)
- require.Equal(t, fresh, got, "domain %s: warmup must not clobber the fresher entry", domain)
- }
-}
-
-// Same invariant for the code addr→code binding, which is rebound when an
-// account's code changes and is therefore just as clobber-able as accounts.
-func TestCachePopulatingGetterKeepsFresherCodeBinding(t *testing.T) {
- addr := []byte("\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44")
- freshCode := []byte{0xaa, 0x01, 0x02, 0x03}
- staleCode := []byte{0xbb, 0x04, 0x05, 0x06}
- sc := newTestStateCache()
- seedFill(sc, kv.CodeDomain, addr, freshCode, 54)
- cpg := &cachePopulatingGetter{TemporalGetter: stubTemporalGetter{v: staleCode}, view: sc.View(cache.FrontierFunc(emptyVisibleEnd)), stepSize: 1_562_500}
-
- _, _, err := cpg.GetLatest(kv.CodeDomain, addr)
- require.NoError(t, err)
-
- got, ok := sc.View(nil).Get(kv.CodeDomain, addr)
- require.True(t, ok)
- require.Equal(t, freshCode, got, "warmup must not rebind addr to older code")
-}
-
-// Cold keys must still be warmed — that is the prefetcher's purpose.
-func TestCachePopulatingGetterWarmsColdKeys(t *testing.T) {
- key := []byte("\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44")
- val := []byte("account-record")
- code := []byte{0xaa, 0x01, 0x02, 0x03}
-
- for _, domain := range []kv.Domain{kv.AccountsDomain, kv.StorageDomain} {
- sc := newTestStateCache()
- cpg := &cachePopulatingGetter{TemporalGetter: stubTemporalGetter{v: val}, view: sc.View(cache.FrontierFunc(emptyVisibleEnd)), stepSize: 1_562_500}
- _, _, err := cpg.GetLatest(domain, key)
- require.NoError(t, err)
- got, ok := sc.View(nil).Get(domain, key)
- require.True(t, ok, "domain %s", domain)
- require.Equal(t, val, got, "domain %s", domain)
- }
-
- sc := newTestStateCache()
- cpg := &cachePopulatingGetter{TemporalGetter: stubTemporalGetter{v: code}, view: sc.View(cache.FrontierFunc(emptyVisibleEnd)), stepSize: 1_562_500}
- _, _, err := cpg.GetLatest(kv.CodeDomain, key)
- require.NoError(t, err)
- got, ok := sc.View(nil).Get(kv.CodeDomain, key)
- require.True(t, ok)
- require.Equal(t, code, got)
- got, ok = sc.View(nil).GetCodeByHash(crypto.Keccak256(code))
- require.True(t, ok)
- require.Equal(t, code, got)
-
- // Negative results (missing account, empty slot) are cached as nil hits.
- sc = newTestStateCache()
- cpg = &cachePopulatingGetter{TemporalGetter: stubTemporalGetter{v: nil}, view: sc.View(cache.FrontierFunc(emptyVisibleEnd)), stepSize: 1_562_500}
- _, _, err = cpg.GetLatest(kv.AccountsDomain, key)
- require.NoError(t, err)
- got, ok = sc.View(nil).Get(kv.AccountsDomain, key)
- require.True(t, ok)
- require.Empty(t, got)
-}
-
-func TestCachePopulatingGetterNegativeUsesLastVisibleTxNum(t *testing.T) {
- const visibleEnd = uint64(10_000_001)
- key := []byte("\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44")
- sc := newTestStateCache()
- cpg := &cachePopulatingGetter{
- TemporalGetter: stubTemporalGetter{v: nil}, stepSize: 1_562_500,
- view: sc.View(cache.FrontierFunc(func(kv.Domain) (uint64, bool) { return visibleEnd, true })),
- }
- _, _, err := cpg.GetLatest(kv.AccountsDomain, key)
- require.NoError(t, err)
- _, ok := sc.View(nil).Get(kv.AccountsDomain, key)
- require.True(t, ok)
-
- sc.Applier().Unwind(visibleEnd)
- _, ok = sc.View(nil).Get(kv.AccountsDomain, key)
- require.True(t, ok, "a negative observed before the unwind floor must remain cached")
-
- sc.Applier().Unwind(visibleEnd - 1)
- _, ok = sc.View(nil).Get(kv.AccountsDomain, key)
- require.False(t, ok, "a negative observed at the unwind floor must be invalidated")
-}
-
-func TestCachePopulatingGetterUnavailableVisibleEndNeverFills(t *testing.T) {
- key := []byte("\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44")
- sc := newTestStateCache()
- cpg := &cachePopulatingGetter{
- TemporalGetter: stubTemporalGetter{v: nil}, stepSize: 1_562_500,
- view: sc.View(cache.FrontierFunc(func(kv.Domain) (uint64, bool) { return 0, false })),
- }
- _, _, err := cpg.GetLatest(kv.AccountsDomain, key)
- require.NoError(t, err)
- _, ok := sc.View(nil).Get(kv.AccountsDomain, key)
- require.False(t, ok, "no exact frontier — nothing may be cached")
-}
-
-func TestCachePopulatingGetterStaleViewDoesNotFill(t *testing.T) {
- key := []byte("\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44")
- sc := newTestStateCache()
- sc.Applier().Apply(kv.AccountsDomain, key, nil, 20)
- cpg := &cachePopulatingGetter{
- TemporalGetter: stubTemporalGetter{v: []byte("pre-delete-record")},
- stepSize: 1_562_500,
- view: sc.View(cache.FrontierFunc(func(kv.Domain) (uint64, bool) { return 11, true })),
- }
-
- _, _, err := cpg.GetLatest(kv.AccountsDomain, key)
- require.NoError(t, err)
- _, ok := sc.View(nil).Get(kv.AccountsDomain, key)
- require.False(t, ok)
-}
-
-func emptyVisibleEnd(kv.Domain) (uint64, bool) { return 0, true }
diff --git a/execution/execmodule/bg_commit.go b/execution/execmodule/bg_commit.go
new file mode 100644
index 00000000000..d062a051e96
--- /dev/null
+++ b/execution/execmodule/bg_commit.go
@@ -0,0 +1,345 @@
+// Copyright 2024 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 execmodule
+
+import (
+ "context"
+ "errors"
+
+ "github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/db/state/execctx"
+)
+
+// Background commit coordination: the post-FCU flush+commit+prune runs on a
+// single background goroutine in a foreground-free window, so the commit RwTx
+// never overlaps a foreground roTx (which would pin MDBX pages and grow the
+// freelist). An in-flight commit is never aborted; a foreground op arriving
+// mid-commit makes the next commit wait.
+
+// commitGen is one in-flight background-commit unit: the SharedDomains of a
+// completed FCU whose domain state must be flushed + committed off the
+// foreground path.
+type commitGen struct {
+ sd *execctx.SharedDomains
+ roTx kv.TemporalTx
+ blockHash common.Hash // the FCU head this generation's state is for
+ blockNum uint64
+ finishProgressBefore uint64
+ isSynced bool
+ initialCycle bool
+ committed bool // guarded by ExecModule.fgMu
+ epoch uint64 // genEpoch at enqueue; a stale epoch means superseded+closed
+}
+
+// fgTryAcquire acquires the foreground semaphore non-blocking and, on
+// success, registers the caller as an active foreground op. Pair with
+// fgRelease. Replaces direct e.semaphore.TryAcquire so the foreground
+// counter and the semaphore can never drift.
+func (e *ExecModule) fgTryAcquire() bool {
+ if !e.semaphore.TryAcquire(1) {
+ return false
+ }
+ e.enterForeground()
+ return true
+}
+
+// fgAcquire is the blocking counterpart of fgTryAcquire.
+func (e *ExecModule) fgAcquire(ctx context.Context) error {
+ if err := e.semaphore.Acquire(ctx, 1); err != nil {
+ return err
+ }
+ e.enterForeground()
+ return nil
+}
+
+// fgRelease releases the foreground semaphore and deregisters the foreground
+// op, waking the commit worker if this was the last one.
+func (e *ExecModule) fgRelease() {
+ e.leaveForeground()
+ e.semaphore.Release(1)
+}
+
+// enterForeground marks a foreground operation as active. The background
+// commit worker will not start a commit while any foreground op is active.
+func (e *ExecModule) enterForeground() {
+ e.fgMu.Lock()
+ e.fgCount++
+ e.fgMu.Unlock()
+}
+
+// leaveForeground marks a foreground operation as finished, waking the
+// commit worker when the last one leaves.
+func (e *ExecModule) leaveForeground() {
+ e.fgMu.Lock()
+ e.fgCount--
+ if e.fgCount == 0 {
+ e.fgIdle.Broadcast()
+ }
+ e.fgMu.Unlock()
+}
+
+// enqueueCommit hands a completed generation to the background commit worker.
+// Non-blocking so the foreground FCU never blocks handing off its commit (the
+// channel is buffered and the maxInFlightCommits backpressure keeps it from
+// filling). Once the commit stream is poisoned (failed commit or shutdown) the
+// worker no longer drains, so roll the generation's roTx back rather than park
+// it forever — a leaked open reader would hang chainDB.Close.
+func (e *ExecModule) enqueueCommit(gen *commitGen) {
+ if e.commitIsPoisoned() {
+ gen.roTx.Rollback()
+ return
+ }
+ e.commitCh <- gen
+}
+
+// commitWorker is the single FIFO background-commit goroutine. It pulls
+// completed generations and commits each one's domain delta — but only once
+// foreground execution is idle, so the commit never overlaps a foreground
+// roTx. An in-flight commit is never aborted; a foreground op that arrives
+// mid-commit makes the next iteration wait.
+//
+// On commitWorkerStop it drains any still-queued generations (so a shutdown
+// never drops a not-yet-landed commit) and exits; commitWg lets WaitIdle
+// block until the worker — and its in-flight commit tx — are done before
+// DB-close.
+func (e *ExecModule) commitWorker() {
+ defer e.commitWg.Done()
+ for {
+ select {
+ case gen := <-e.commitCh:
+ e.runCommit(gen)
+ case <-e.commitWorkerStop:
+ for {
+ select {
+ case gen := <-e.commitCh:
+ e.runCommit(gen)
+ default:
+ return
+ }
+ }
+ }
+ }
+}
+
+// runCommit flushes + commits one generation's delta in a foreground-free
+// window. Called only by commitWorker.
+func (e *ExecModule) runCommit(gen *commitGen) {
+ // Hold the foreground semaphore across flush+commit+prune so it is
+ // coordinated with foreground tx opening — a foreground unwind then always
+ // opens against a consistent db/file view. This full exclusion is the current
+ // coordination mechanism; making the background pausable so foreground has
+ // priority (rather than blocking it) is tracked follow-up work.
+ if err := e.fgAcquire(e.bacgroundCtx); err != nil {
+ // Could not acquire (shutdown-cancelled ctx): the commit did not run, so
+ // do NOT mark it committed — pretending it landed would drop the delta.
+ // The FCU's state is re-derivable on restart.
+ return
+ }
+ defer e.fgRelease()
+
+ // A superseded generation had its set discarded (SetHead unwind) while it sat
+ // queued — its SharedDomains is closed, so skip it rather than commit a closed SD.
+ if e.genSuperseded(gen) {
+ return
+ }
+
+ // A prior generation's durable commit failed: do not advance the DB past the
+ // hole it left. Skip every remaining generation while shutdown propagates.
+ if e.commitIsPoisoned() {
+ return
+ }
+
+ err := e.runPostForkchoice(gen.sd, gen.roTx, gen.finishProgressBefore, gen.isSynced, gen.initialCycle)
+ // roTx is rolled back inside runForkchoiceFlushCommit between Flush and
+ // Commit; this is a safety net (Rollback is idempotent).
+ gen.roTx.Rollback()
+ if err != nil {
+ if errors.Is(err, context.Canceled) {
+ // Shutdown cancelled the commit; leave the gen un-retired (re-derived
+ // on restart) rather than mark a commit that did not land.
+ return
+ }
+ // A durable commit failed for a non-shutdown reason: the block was already
+ // reported VALID to the CL, so silently dropping its state would diverge
+ // from consensus. Poison the queue first (synchronously) so no already-
+ // enqueued descendant commits its delta over this hole before shutdown
+ // propagates. Crit does not terminate this logger, and returning would
+ // leave the generation un-retired (FCU wedges Busy, drain/shutdown hangs).
+ // Trigger a clean node shutdown (cancels the root context) so the process
+ // restarts and re-derives the state. In a goroutine — stopNode drains this
+ // same worker — and once, so repeated drain failures don't respawn it.
+ e.poisonCommits()
+ e.logger.Crit("background commit failed; shutting down node", "block", gen.blockNum, "hash", gen.blockHash, "err", err)
+ e.commitFatalOnce.Do(func() {
+ go func() {
+ if stopErr := e.stopNode(); stopErr != nil {
+ e.logger.Error("Could not stop node after background commit failure", "err", stopErr)
+ }
+ }()
+ })
+ return
+ }
+ e.markGenCommitted(gen)
+ // The worker must not touch the published SD (Events.LatestSD): what the
+ // latest block is stays a foreground concern (updateForkChoice), and a commit
+ // landing does not change it.
+}
+
+// markGenCommitted records that a generation's commit has landed. The
+// generation's SharedDomains is NOT closed here — a foreground op may still
+// hold it via the parent chain. It is closed as a unit by drainCommittedGens
+// once the whole chain is committed.
+func (e *ExecModule) markGenCommitted(gen *commitGen) {
+ e.fgMu.Lock()
+ gen.committed = true
+ e.uncommittedGens--
+ if e.uncommittedGens == 0 {
+ e.fgIdle.Broadcast()
+ }
+ e.fgMu.Unlock()
+}
+
+// WaitCommitsDrained blocks until every enqueued background commit has landed;
+// unlike WaitIdle it leaves the commit worker running, so it is repeatable.
+func (e *ExecModule) WaitCommitsDrained() {
+ e.fgMu.Lock()
+ for e.uncommittedGens > 0 {
+ e.fgIdle.Wait()
+ }
+ e.fgMu.Unlock()
+}
+
+// maxInFlightCommits bounds the not-yet-committed generation chain. Each
+// in-flight generation adds a level to the block-overlay parent chain that
+// foreground reads walk, so an unbounded chain degrades every read; the bound
+// also forces a foreground-idle window (via commitBacklogFull → Busy) so the
+// commit worker can never be starved by back-to-back FCUs.
+const maxInFlightCommits = 4
+
+// commitBacklogFull reports whether the not-yet-committed generation chain has
+// reached maxInFlightCommits.
+func (e *ExecModule) commitBacklogFull() bool {
+ e.fgMu.Lock()
+ defer e.fgMu.Unlock()
+ return e.uncommittedGens >= maxInFlightCommits
+}
+
+// latestGen returns the newest not-yet-committed generation to chain a new
+// SharedDomains onto — a committed generation's state is already in the raw DB,
+// so chaining to it adds nothing and masks direct-DB reads. The single FIFO
+// commit worker commits in enqueue order, so a committed newest generation
+// means all are committed: read straight from the DB.
+func (e *ExecModule) latestGen() *execctx.SharedDomains {
+ e.fgMu.Lock()
+ defer e.fgMu.Unlock()
+ if len(e.gens) == 0 {
+ return nil
+ }
+ last := e.gens[len(e.gens)-1]
+ if last.committed {
+ return nil
+ }
+ return last.sd
+}
+
+// overlayBaseFor wraps roTx with the newest in-flight generation's block overlay
+// when one exists, so block-data reads (headers, bodies, TDs) see a not-yet-committed
+// FCU's writes instead of a stale DB. Returns roTx unchanged when there is none.
+func (e *ExecModule) overlayBaseFor(roTx kv.TemporalTx) kv.TemporalTx {
+ if parent := e.latestGen(); parent != nil {
+ if v := parent.OverlayTemporalTx(roTx); v != nil {
+ return v
+ }
+ }
+ return roTx
+}
+
+// beginCoordinatedRo opens a base RO tx under fgMu so its committed snapshot
+// reflects every generation markGenCommitted has recorded — markGenCommitted runs
+// under fgMu strictly after the DB commit, so a datum dropped from the parent
+// chain as committed is guaranteed visible in this tx. Handed to SharedDomains as
+// their read coordinator; exec/validation readers open through it instead of an
+// ad-hoc BeginTemporalRo that could straddle a background commit.
+func (e *ExecModule) beginCoordinatedRo(ctx context.Context) (kv.TemporalTx, error) {
+ e.fgMu.Lock()
+ defer e.fgMu.Unlock()
+ return e.db.BeginTemporalRo(ctx)
+}
+
+// genSuperseded reports whether gen's set was discarded (via closeAllGens bumping
+// genEpoch) since it was enqueued. The epoch bump and this check both hold fgMu.
+func (e *ExecModule) genSuperseded(gen *commitGen) bool {
+ e.fgMu.Lock()
+ defer e.fgMu.Unlock()
+ return gen.epoch != e.genEpoch
+}
+
+// poisonCommits marks the commit stream dead after a failed durable commit, so
+// no queued or future generation commits its delta over the resulting hole.
+func (e *ExecModule) poisonCommits() {
+ e.fgMu.Lock()
+ e.commitPoisoned = true
+ e.fgMu.Unlock()
+}
+
+// commitIsPoisoned reports whether a durable commit has failed.
+func (e *ExecModule) commitIsPoisoned() bool {
+ e.fgMu.Lock()
+ defer e.fgMu.Unlock()
+ return e.commitPoisoned
+}
+
+// addGen appends a new in-flight generation to the chain.
+func (e *ExecModule) addGen(gen *commitGen) {
+ e.fgMu.Lock()
+ gen.epoch = e.genEpoch
+ e.gens = append(e.gens, gen)
+ e.uncommittedGens++
+ e.fgMu.Unlock()
+}
+
+// drainCommittedGens is a no-op: committed generations' mem is retained (they
+// may still be a parent in a reader's chain) and freed as a unit at shutdown by
+// closeAllGens. Precise per-generation freeing needs a refcount keyed on a
+// publication id — tracked in erigontech/erigon#22494.
+func (e *ExecModule) drainCommittedGens() {}
+
+// closeAllGens detaches + closes every remaining generation. Called only at
+// shutdown (after the commit worker has stopped) to release the generations
+// that drainCommittedGens deliberately keeps alive for the lifetime of the run.
+func (e *ExecModule) closeAllGens() {
+ e.fgMu.Lock()
+ defer e.fgMu.Unlock()
+ // Invalidate any generation still queued for commit: the worker compares its
+ // gen's epoch against this and skips a superseded (now-closed) one.
+ e.genEpoch++
+ for _, g := range e.gens {
+ g.sd.SetParent(nil)
+ g.sd.Close()
+ // Roll back the generation's read tx: committed gens already had it
+ // rolled back in runCommit (Rollback is idempotent), but a never-committed
+ // gen would otherwise leak an open MDBX reader and wedge DB.Close.
+ if g.roTx != nil {
+ g.roTx.Rollback()
+ }
+ }
+ e.gens = nil
+ e.uncommittedGens = 0
+ // Wake any WaitCommitsDrained/WaitIdle waiter blocked on the generation count.
+ e.fgIdle.Broadcast()
+}
diff --git a/execution/execmodule/bg_commit_gens_test.go b/execution/execmodule/bg_commit_gens_test.go
new file mode 100644
index 00000000000..226e80d32de
--- /dev/null
+++ b/execution/execmodule/bg_commit_gens_test.go
@@ -0,0 +1,71 @@
+// Copyright 2024 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 execmodule
+
+import (
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/erigontech/erigon/db/state/execctx"
+)
+
+func newGenTestModule() *ExecModule {
+ em := &ExecModule{}
+ em.fgIdle = sync.NewCond(&em.fgMu)
+ return em
+}
+
+func TestGenStackCountersAndLatestGen(t *testing.T) {
+ em := newGenTestModule()
+ a := &commitGen{sd: new(execctx.SharedDomains)}
+ b := &commitGen{sd: new(execctx.SharedDomains)}
+
+ em.addGen(a)
+ em.addGen(b)
+ require.Equal(t, 2, em.uncommittedGens)
+ require.True(t, em.latestGen() == b.sd, "latestGen is the newest uncommitted generation")
+
+ em.markGenCommitted(b)
+ require.Equal(t, 1, em.uncommittedGens)
+ require.Nil(t, em.latestGen(), "a committed newest generation means read straight from the DB")
+
+ em.markGenCommitted(a)
+ require.Equal(t, 0, em.uncommittedGens)
+ em.WaitCommitsDrained() // returns immediately once the backlog is empty
+}
+
+func TestGenEpochGuardSkipsSupersededGen(t *testing.T) {
+ em := newGenTestModule()
+ a := &commitGen{sd: new(execctx.SharedDomains)}
+ em.addGen(a)
+ require.Equal(t, uint64(0), a.epoch)
+ require.False(t, em.genSuperseded(a), "a freshly enqueued generation is current")
+
+ // closeAllGens bumps genEpoch to discard the queued set (the SetHead-unwind
+ // path); it also closes the real SharedDomains, so exercise just the bump here.
+ em.fgMu.Lock()
+ em.genEpoch++
+ em.fgMu.Unlock()
+ require.True(t, em.genSuperseded(a), "a generation whose set was discarded is superseded")
+
+ c := &commitGen{sd: new(execctx.SharedDomains)}
+ em.addGen(c)
+ require.Equal(t, uint64(1), c.epoch)
+ require.False(t, em.genSuperseded(c), "a generation enqueued after the bump is current")
+}
diff --git a/execution/execmodule/block_building.go b/execution/execmodule/block_building.go
index 6d8952a6f6a..542b35589b8 100644
--- a/execution/execmodule/block_building.go
+++ b/execution/execmodule/block_building.go
@@ -18,6 +18,7 @@ package execmodule
import (
"context"
+ "errors"
"reflect"
"github.com/holiman/uint256"
@@ -44,15 +45,20 @@ func (e *ExecModule) evictOldBuilders() {
// remove old builders so that at most MaxBuilders - 1 remain
for i := 0; i <= len(e.builders)-engine_helpers.MaxBuilders; i++ {
+ if bldr := e.builders[ids[i]]; bldr != nil {
+ // Cancel so the build goroutine exits and releases its scoped read
+ // view (pinned roTx) rather than leaking it past eviction.
+ bldr.Cancel()
+ }
delete(e.builders, ids[i])
}
}
func (e *ExecModule) AssembleBlock(ctx context.Context, params *builder.Parameters) (AssembleBlockResult, error) {
- if !e.semaphore.TryAcquire(1) {
+ if !e.fgTryAcquire() {
return AssembleBlockResult{Busy: true}, nil
}
- defer e.semaphore.Release(1)
+ defer e.fgRelease()
if err := e.checkWithdrawalsPresence(params.Timestamp, params.Withdrawals); err != nil {
return AssembleBlockResult{}, err
@@ -67,6 +73,19 @@ func (e *ExecModule) AssembleBlock(ctx context.Context, params *builder.Paramete
}
}
+ // Pin a consistent, by-block read snapshot for the requested parent while we
+ // hold the foreground semaphore (settled state, no commit mid-flight). If the
+ // head is not yet at ParentHash, signal Busy so the CL retries rather than
+ // building on the wrong block. The build reads only through this view — never
+ // the raw DB directly nor the mutable global published SD.
+ view, err := e.captureScopedReadView(ctx, params.ParentHash)
+ if err != nil {
+ if errors.Is(err, errHeadMismatch) {
+ return AssembleBlockResult{Busy: true}, nil
+ }
+ return AssembleBlockResult{}, err
+ }
+
// Initiate payload building
e.evictOldBuilders()
@@ -74,7 +93,11 @@ func (e *ExecModule) AssembleBlock(ctx context.Context, params *builder.Paramete
params.PayloadId = e.nextPayloadId
e.lastParameters = params
- e.builders[e.nextPayloadId] = builder.NewBlockBuilder(e.builderFunc, params, e.config.SecondsPerSlot()/4)
+ // Carry the view on a per-build copy so it never enters e.lastParameters
+ // (which the duplicate-request check DeepEquals).
+ buildParams := *params
+ buildParams.ScopedView = view
+ e.builders[e.nextPayloadId] = builder.NewBlockBuilder(e.builderFunc, &buildParams, e.config.SecondsPerSlot()/4)
e.logger.Info("[ForkChoiceUpdated] BlockBuilder added", "payload", e.nextPayloadId)
return AssembleBlockResult{PayloadID: e.nextPayloadId}, nil
@@ -97,10 +120,10 @@ func blockValue(br *types.BlockWithReceipts, baseFee *uint256.Int) *uint256.Int
}
func (e *ExecModule) GetAssembledBlock(ctx context.Context, payloadID uint64) (AssembledBlockResult, error) {
- if !e.semaphore.TryAcquire(1) {
+ if !e.fgTryAcquire() {
return AssembledBlockResult{Busy: true}, nil
}
- defer e.semaphore.Release(1)
+ defer e.fgRelease()
bldr, ok := e.builders[payloadID]
if !ok {
diff --git a/execution/execmodule/exec_module.go b/execution/execmodule/exec_module.go
index 9a5b3204633..99d8b4f40cb 100644
--- a/execution/execmodule/exec_module.go
+++ b/execution/execmodule/exec_module.go
@@ -117,15 +117,16 @@ var _ kvcache.Cache = (*Cache)(nil) // compile-time interface check
var _ kvcache.CacheView = (*CacheView)(nil) // compile-time interface check
func (c *Cache) View(_ context.Context, tx kv.TemporalTx) (kvcache.CacheView, error) {
+ // Read the latest *published* SharedDomains — the most recently executed
+ // block's stable leaf snapshot. Deliberately NOT e.currentContext: while
+ // an FCU is in progress currentContext is that block's half-written SD,
+ // actively mutated by the executing pipeline (the coinbase nonce is
+ // already bumped mid-block). A consumer that grabbed currentContext would
+ // see a not-yet-final block — e.g. GetTransactionCount and txpool
+ // validateTx in the same SubmitTransfer disagreeing by one nonce. The
+ // published leaf only advances at block completion, so it is stable.
var context *execctx.SharedDomains
- if c.execModule != nil {
- c.execModule.lock.RLock()
- context = c.execModule.currentContext
- c.execModule.lock.RUnlock()
- }
- // Fall back to the published SD from Events during background commits
- // (currentContext is nil but the SD is still valid in memory).
- if context == nil && c.publishedSD != nil {
+ if c.publishedSD != nil {
context = c.publishedSD()
}
@@ -222,6 +223,33 @@ type ExecModule struct {
currentContext *execctx.SharedDomains
publishedSD func() *execctx.SharedDomains // fallback for background commit
+ // fgMu guards fgCount + gens; fgIdle is signalled when fgCount reaches zero
+ // so the commit worker can run in a foreground-free window (see bg_commit.go).
+ fgMu sync.Mutex
+ fgCount int
+ fgIdle *sync.Cond
+ gens []*commitGen
+ uncommittedGens int
+ // genEpoch is bumped (under fgMu) whenever the generation set is discarded
+ // wholesale (closeAllGens, e.g. a SetHead unwind). A commit worker holding a
+ // generation from an earlier epoch skips it rather than committing an
+ // already-closed SharedDomains.
+ genEpoch uint64
+ commitCh chan *commitGen
+ // commitWorker lifecycle: commitWorkerStop signals the worker to drain
+ // and exit; commitWg tracks it so shutdown (WaitIdle) waits for the
+ // worker — and its in-flight commit txs — to finish before DB-close.
+ commitWorkerStop chan struct{}
+ commitStopOnce sync.Once
+ commitWg sync.WaitGroup
+ // commitFatalOnce ensures a failed durable commit triggers node shutdown at
+ // most once, even as the worker drains further generations that also fail.
+ commitFatalOnce sync.Once
+ // commitPoisoned (guarded by fgMu) is set when a durable commit fails: each
+ // generation flushes only its own delta, so a queued descendant must not
+ // commit over the hole left by a failed parent while shutdown propagates.
+ commitPoisoned bool
+
// stateCache is a cache for state data (accounts, storage, code)
stateCache *cache.StateCache
// codeStore is the persistent codehash-keyed code cache (in-mem + MDBX backing).
@@ -287,26 +315,64 @@ func NewExecModule(
stopNode: stopNode,
}
- // Wire the process-global state cache into the read-ahead so its
- // prefetches populate the same hashmap that SharedDomains.GetLatest
- // probes on the EVM hot path. Reth's "same hashmap" pattern.
+ // Route the read-ahead's prefetches through the published SharedDomains so
+ // reads see in-flight tip state and the SD's own read-fill warms the
+ // process-global cache the EVM probes — keeping cache population an SD concern.
if readAheader != nil {
- readAheader.SetStateCache(domainCache)
+ readAheader.SetPublishedSD(func() *execctx.SharedDomains {
+ if em.publishedSD != nil {
+ return em.publishedSD()
+ }
+ return nil
+ })
}
if stateCache != nil {
stateCache.execModule = em
}
+
+ // Start the background-commit worker. It pulls completed
+ // generations off commitCh and commits each in a foreground-free
+ // window. The buffered channel keeps the foreground FCU's hand-off
+ // non-blocking. WaitIdle stops the worker before DB-close.
+ em.fgIdle = sync.NewCond(&em.fgMu)
+ em.commitCh = make(chan *commitGen, 1024)
+ em.commitWorkerStop = make(chan struct{})
+ em.commitWg.Add(1)
+ go em.commitWorker()
+
return em
}
// WaitIdle blocks until any in-flight updateForkChoice goroutine finishes.
// Call before closing the database to avoid waitTxsAllDoneOnClose hangs.
func (e *ExecModule) WaitIdle(ctx context.Context) {
- if err := e.semaphore.Acquire(ctx, 1); err != nil {
- return // context cancelled — best effort
+ if e.fgAcquire(ctx) == nil {
+ // Foreground is idle. Drain + commit any queued generations (the worker's
+ // stop path runs them), then release the generation chain now that no
+ // reader can be in flight — drainCommittedGens keeps the newest alive as
+ // Events.LatestSD until here.
+ e.fgRelease()
+ e.stopCommitWorker()
+ e.closeAllGens()
+ return
}
- e.semaphore.Release(1)
+ // Timed out with a foreground op still holding the semaphore: closing the
+ // generation chain now would wipe SDs that op is still reading through, and
+ // parking its later commit in the stopped worker's channel would leak an open
+ // roTx and hang chainDB.Close. Poison so that late enqueueCommit rolls back
+ // instead of parking, stop the worker, leave the gens for the owning op (the
+ // process is exiting anyway), and log loudly.
+ e.poisonCommits()
+ e.stopCommitWorker()
+ e.logger.Warn("WaitIdle: foreground op still active at shutdown; not closing generations")
+}
+
+// stopCommitWorker signals the background-commit worker to drain and exit,
+// then waits for it. Idempotent.
+func (e *ExecModule) stopCommitWorker() {
+ e.commitStopOnce.Do(func() { close(e.commitWorkerStop) })
+ e.commitWg.Wait()
}
// newDomainStateCache is the module's one construction site of the domain
@@ -466,16 +532,20 @@ const nextForkBanner = `
func (e *ExecModule) ValidateChain(ctx context.Context, blockHash common.Hash, blockNumber uint64) (ValidationResult, error) {
defer validateChainDuration.ObserveDuration(time.Now())
- if !e.semaphore.TryAcquire(1) {
+ if !e.fgTryAcquire() {
e.logger.Trace("ethereumExecutionModule.ValidateChain: ExecutionStatus_Busy")
return ValidationResult{
ValidationStatus: ExecutionStatusBusy,
}, nil
}
- defer e.semaphore.Release(1)
+ defer e.fgRelease()
e.hook.LastNewBlockSeen(blockNumber) // used by eth_syncing
- e.currentContext.ResetPendingUpdates()
+ // currentContext is nil while a background commit holds the previous
+ // FCU's SD — guard the access.
+ if e.currentContext != nil {
+ e.currentContext.ResetPendingUpdates()
+ }
e.forkValidator.ClearWithUnwind()
e.logger.Debug("[execmodule] validating chain", "number", blockNumber, "hash", blockHash)
var (
@@ -506,22 +576,26 @@ func (e *ExecModule) ValidateChain(ctx context.Context, blockHash common.Hash, b
e.readAheader.AddHeaderAndBody(ctx, e.db, header, body)
currentBlockNumber = rawdb.ReadCurrentBlockNumber(overlay)
} else {
- if err := e.db.View(ctx, func(tx kv.Tx) error {
- header, err = e.blockReader.Header(ctx, tx, blockHash, blockNumber)
- if err != nil {
- return err
- }
-
- body, err = e.blockReader.BodyWithTransactions(ctx, tx, blockHash, blockNumber)
- if err != nil {
- return err
- }
- e.readAheader.AddHeaderAndBody(ctx, e.db, header, body)
- currentBlockNumber = rawdb.ReadCurrentBlockNumber(tx)
- return nil
- }); err != nil {
+ // currentContext is nil — read block data through the newest
+ // in-flight commit generation's overlay when one exists (gate item
+ // 2), so the previous FCU's not-yet-committed headers/bodies/TDs
+ // are visible; otherwise a plain DB read.
+ roTx, err := e.db.BeginTemporalRo(ctx)
+ if err != nil {
+ return ValidationResult{}, err
+ }
+ defer roTx.Rollback()
+ src := e.overlayBaseFor(roTx)
+ header, err = e.blockReader.Header(ctx, src, blockHash, blockNumber)
+ if err != nil {
return ValidationResult{}, err
}
+ body, err = e.blockReader.BodyWithTransactions(ctx, src, blockHash, blockNumber)
+ if err != nil {
+ return ValidationResult{}, err
+ }
+ e.readAheader.AddHeaderAndBody(ctx, e.db, header, body)
+ currentBlockNumber = rawdb.ReadCurrentBlockNumber(src)
}
if header == nil || body == nil {
return ValidationResult{
@@ -558,24 +632,59 @@ func (e *ExecModule) ValidateChain(ctx context.Context, blockHash common.Hash, b
// Do not defer doms.Close(): on the success path ownership transfers to
// forkValidator.sharedDom inside ValidatePayload and later phases close it,
// so we Close explicitly only on the early-return error paths below.
+ doms.SetReadCoordinator(e.beginCoordinatedRo)
doms.SetInMemHistoryReads(inMemHistoryReads)
- if err := doms.InitBlockOverlay(roTx, roTx.Debug().Dirs().Tmp); err != nil {
+ // Chain the validation SD to the latest in-memory canonical generation:
+ // e.currentContext when present, otherwise the newest in-flight commit
+ // generation (the prior FCU cleared currentContext and
+ // handed its SD to the background commit). This parent link is the single
+ // lookup path: the overlay base below is derived from it so block-data reads
+ // and domain-state reads traverse the identical generation chain.
+ //
+ // The parent link serves two roles:
+ //
+ // 1. Head-extending payloads read the canonical generation's
+ // not-yet-committed domain state instead of stale MDBX.
+ //
+ // 2. Fork payloads: unwindToCommonCanonical below must build an unwind
+ // set, and the diffsets of the canonical blocks it unwinds live in
+ // the canonical generation's pastChangesAccumulator — reachable only
+ // through this parent link (GetDiffset chains to the parent). Without
+ // it the unwind silently runs with no unwind set, leaving the
+ // BranchCache unmasked and corrupting the computed root.
+ //
+ // For a fork payload the parent does NOT shadow the unwound base: once
+ // unwindToCommonCanonical has run, doms.mem.unwindChangeset holds every
+ // key the unwound canonical blocks touched, and TemporalMemBatch.getLatest
+ // resolves those from the unwind set before ever consulting the parent.
+ if e.currentContext != nil {
+ // Refresh the in-progress top's parent to the current in-flight tip so
+ // its chain reaches generations pushed since currentContext was created;
+ // otherwise its stale/nil parent leaves in-flight block data unreachable.
+ if parent := e.latestGen(); parent != nil {
+ e.currentContext.SetParent(parent)
+ }
+ doms.SetParent(e.currentContext)
+ } else if parent := e.latestGen(); parent != nil {
+ doms.SetParent(parent)
+ }
+
+ // Back the validation overlay by the SD's OWN parent chain so block-data
+ // reads cascade through the same generations that domain-state reads do —
+ // never a separate, divergent capture. doms has no overlay of its own yet
+ // (InitBlockOverlay is next), so OverlayTemporalTx here yields exactly the
+ // parent chain.
+ valOverlayBase := kv.TemporalTx(roTx)
+ if v := doms.OverlayTemporalTx(roTx); v != nil {
+ valOverlayBase = v
+ }
+ if err := doms.InitBlockOverlay(valOverlayBase, roTx.Debug().Dirs().Tmp); err != nil {
doms.Close()
return ValidationResult{}, fmt.Errorf("ValidateChain: init block overlay: %w", err)
}
var tx kv.TemporalRwTx = doms.BlockOverlay()
- // Chain the validation SD to the canonical generation (e.currentContext) for
- // any payload with a parent, not just head-extending ones: head-extending
- // payloads read its not-yet-committed domain state instead of stale MDBX, and
- // fork payloads reach the canonical generation's pastChangesAccumulator (via
- // GetDiffset's parent chain) to build the unwind set — without the link the
- // unwind runs empty, leaving the BranchCache unmasked and corrupting the root.
- if e.currentContext != nil {
- doms.SetParent(e.currentContext)
- }
-
// Flush block overlay data (headers, bodies, TDs from InsertBlocks) into
// the validation overlay so unwindToCommonCanonical and ValidatePayload —
// and the parallel exec goroutine via NewReadView — see this block data.
@@ -694,13 +803,13 @@ func (e *ExecModule) purgeBadChain(ctx context.Context, tx kv.RwTx, latestValidH
}
func (e *ExecModule) Start(ctx context.Context, hook *stageloop.Hook) {
- if err := e.semaphore.Acquire(ctx, 1); err != nil {
+ if err := e.fgAcquire(ctx); err != nil {
if !errors.Is(err, context.Canceled) {
e.logger.Error("Could not start execution service", "err", err)
}
return
}
- defer e.semaphore.Release(1)
+ defer e.fgRelease()
if err := e.pipelineExecutor.ProcessFrozenBlocks(ctx, hook, e.onlySnapDownloadOnStart); err != nil {
if !errors.Is(err, context.Canceled) {
@@ -746,11 +855,11 @@ func (e *ExecModule) Ready(ctx context.Context) (bool, error) {
return false, err
}
- if !e.semaphore.TryAcquire(1) {
+ if !e.fgTryAcquire() {
e.logger.Trace("ethereumExecutionModule.Ready: ExecutionStatus_Busy")
return false, nil
}
- defer e.semaphore.Release(1)
+ defer e.fgRelease()
return true, nil
}
diff --git a/execution/execmodule/exec_module_devp2p_test.go b/execution/execmodule/exec_module_devp2p_test.go
index a4241c57339..14f31d8b1ba 100644
--- a/execution/execmodule/exec_module_devp2p_test.go
+++ b/execution/execmodule/exec_module_devp2p_test.go
@@ -81,7 +81,7 @@ func TestGetBlockReceiptsFrozenBlocks(t *testing.T) {
tx, err := types.SignTx(types.NewTransaction(block.TxNonce(devp2pTestAddr), common.Address{1}, uint256.NewInt(1), params.TxGas, uint256.NewInt(m.Genesis.BaseFee().Uint64()), nil), *signer, devp2pTestKey)
require.NoError(t, err)
block.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(generated))
// Freeze the first segment's blocks into a snapshot file and prune them from
diff --git a/execution/execmodule/exec_module_test.go b/execution/execmodule/exec_module_test.go
index d1ce2ad3d9b..386028f3cda 100644
--- a/execution/execmodule/exec_module_test.go
+++ b/execution/execmodule/exec_module_test.go
@@ -178,7 +178,7 @@ func TestValidateChainWithLastTxNumOfBlockAtStepBoundary(t *testing.T) {
)
require.NoError(t, err)
b.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.Len(t, chainPack.Blocks, 1)
exec := m.ExecModule
@@ -244,7 +244,7 @@ func TestValidateChainAndUpdateForkChoiceWithSideForksThatGoBackAndForwardInHeig
)
require.NoError(t, err)
b.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
//goland:noinspection DuplicatedCode
shorterFork, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, func(i int, b *blockgen.BlockGen) {
@@ -317,7 +317,7 @@ func TestValidateForkPayloadOffNonTipCanonicalBlockWithCache(t *testing.T) {
// entries. After this the head is block 2.
prefix, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 2, func(i int, b *blockgen.BlockGen) {
b.AddTx(mkTx(uint64(i), 1_000))
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, insertValidateAndUfc1By1(t.Context(), m.ExecModule, prefix.Blocks))
block2 := prefix.Blocks[1]
@@ -375,7 +375,7 @@ func TestUpdateForkChoiceRecoversWhenStateAheadOfTxNums(t *testing.T) {
)
require.NoError(t, err)
b.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.Len(t, chainPack.Blocks, 10)
@@ -460,7 +460,7 @@ func TestUpdateForkChoiceForwardExecutesAfterStateAheadRecovery(t *testing.T) {
)
require.NoError(t, err)
b.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.Len(t, chainPack.Blocks, 15)
@@ -509,6 +509,7 @@ func TestUpdateForkChoiceForwardExecutesAfterStateAheadRecovery(t *testing.T) {
res2, err := updateForkChoice(ctx, m.ExecModule, tip)
require.NoError(t, err)
require.Equal(t, execmodule.ExecutionStatusSuccess, res2.Status, "second FCU should execute forward to the tip")
+ m.ExecModule.WaitCommitsDrained()
require.NoError(t, m.DB.ViewTemporal(ctx, func(tx kv.TemporalTx) error {
execProg, err := stages.GetStageProgress(tx, stages.Execution)
require.NoError(t, err)
@@ -544,7 +545,6 @@ func TestReorgBackAndForwardIntoCanonicalChain(t *testing.T) {
t.Run(mode.name, func(t *testing.T) {
ctx := t.Context()
m := execmoduletester.New(t, opts...)
- m.ExecModule.WaitIdle(ctx)
const chainLen = 9
const reorgBackTo = 5
@@ -586,7 +586,7 @@ func TestReorgBackAndForwardIntoCanonicalChain(t *testing.T) {
// Let the last FCU's commit and prune (foreground or background)
// settle before reading the committed head.
- m.ExecModule.WaitIdle(ctx)
+ m.ExecModule.WaitCommitsDrained()
require.NoError(t, m.DB.ViewTemporal(ctx, func(tx kv.TemporalTx) error {
require.Equal(t, headerAt(chainLen).Hash(), rawdb.ReadHeadBlockHash(tx), "head must be at canonical tip")
return nil
@@ -632,7 +632,7 @@ func TestAssembleBlock(t *testing.T) {
tx, err := types.SignTx(types.NewTransaction(gen.TxNonce(m.Address), common.Address{1}, uint256.NewInt(10_000), params.TxGas, uint256.NewInt(m.Genesis.BaseFee().Uint64()), nil), *types.LatestSignerForChainID(m.ChainConfig.ChainID), m.Key)
require.NoError(t, err)
gen.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -815,7 +815,7 @@ func TestAssembleBlockWithFreshlyAddedTxns(t *testing.T) {
tx, err := types.SignTx(types.NewTransaction(gen.TxNonce(m.Address), common.Address{1}, uint256.NewInt(10_000), params.TxGas, uint256.NewInt(m.Genesis.BaseFee().Uint64()), nil), *types.LatestSignerForChainID(m.ChainConfig.ChainID), m.Key)
require.NoError(t, err)
gen.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -925,15 +925,15 @@ func insertValidateAndUfc1By1(ctx context.Context, exec *execmodule.ExecModule,
return fmt.Errorf("unexpected updateForkChoice status: %s", ur.Status)
}
}
- // UpdateForkChoice returns before the background flush+commit finishes
- // (per #21444). The next semaphore-acquiring op blocks until the prior
- // FCU's commit defers complete — so do one more idempotent FCU for the
- // last block to ensure commitBlock has settled before the caller reads it.
+ // UpdateForkChoice returns before the background flush+commit finishes, so do
+ // one more idempotent FCU for the last block and then wait for the commit
+ // backlog to drain — committed state is settled before the caller reads it.
if len(blocks) > 0 {
if _, err := updateForkChoice(ctx, exec, blocks[len(blocks)-1].Header()); err != nil {
return err
}
}
+ exec.WaitCommitsDrained()
return nil
}
@@ -998,7 +998,7 @@ func TestAssembleEmptyBlock(t *testing.T) {
tx, txErr := types.SignTx(types.NewTransaction(gen.TxNonce(m.Address), common.Address{1}, uint256.NewInt(10_000), params.TxGas, uint256.NewInt(m.Genesis.BaseFee().Uint64()), nil), *types.LatestSignerForChainID(m.ChainConfig.ChainID), m.Key)
require.NoError(t, txErr)
gen.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -1036,7 +1036,7 @@ func TestAssembleBlockWithStateVerification(t *testing.T) {
tx, txErr := types.SignTx(types.NewTransaction(gen.TxNonce(m.Address), common.Address{1}, uint256.NewInt(10_000), params.TxGas, uint256.NewInt(m.Genesis.BaseFee().Uint64()), nil), *types.LatestSignerForChainID(m.ChainConfig.ChainID), m.Key)
require.NoError(t, txErr)
gen.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -1086,6 +1086,89 @@ func TestAssembleBlockWithStateVerification(t *testing.T) {
require.NoError(t, err)
}
+// TestAssembleBlockRejectsWrongParent verifies AssembleBlock pins to the
+// requested ParentHash: a request whose parent is not the current head returns
+// Busy (the CL retries) rather than building on the wrong block, while a request
+// on the real head builds a block extending exactly that parent.
+func TestAssembleBlockRejectsWrongParent(t *testing.T) {
+ t.Parallel()
+ ctx := t.Context()
+ m := execmoduletester.New(t, execmoduletester.WithTxPool(), execmoduletester.WithChainConfig(chain.AllProtocolChanges))
+ exec := m.ExecModule
+
+ chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, func(i int, gen *blockgen.BlockGen) {
+ gen.SetCoinbase(common.Address{1})
+ }, m.PublishedSD())
+ require.NoError(t, err)
+ require.NoError(t, m.InsertChain(chainPack))
+ head := chainPack.TopBlock
+
+ mkParams := func(parent common.Hash) *builder.Parameters {
+ return &builder.Parameters{
+ ParentHash: parent,
+ Timestamp: head.Time() + 1,
+ PrevRandao: head.Header().MixDigest,
+ SuggestedFeeRecipient: common.Address{1},
+ Withdrawals: make([]*types.Withdrawal, 0),
+ ParentBeaconBlockRoot: func() *common.Hash { h := randomHash(); return &h }(),
+ }
+ }
+
+ // Wrong parent → Busy, no block built (call directly, not via the retryBusy helper).
+ res, err := exec.AssembleBlock(ctx, mkParams(randomHash()))
+ require.NoError(t, err)
+ require.True(t, res.Busy, "assemble on a non-head parent must return Busy")
+
+ // Correct parent → builds a block extending exactly that parent.
+ payloadId, err := assembleBlock(ctx, exec, mkParams(head.Hash()))
+ require.NoError(t, err)
+ block, err := getAssembledBlock(ctx, exec, payloadId)
+ require.NoError(t, err)
+ require.NotNil(t, block)
+ require.Equal(t, head.Hash(), block.ParentHash(), "assembled block must extend the requested parent")
+ require.Equal(t, head.NumberU64()+1, block.NumberU64())
+}
+
+// TestAssembleBlockRejectsOldCanonicalParent verifies AssembleBlock rejects a
+// parent that is canonical at its own height but is not the current head: it
+// must return Busy rather than build on a stale ancestor (which the async
+// builder would then fail its parent check against).
+func TestAssembleBlockRejectsOldCanonicalParent(t *testing.T) {
+ t.Parallel()
+ ctx := t.Context()
+ m := execmoduletester.New(t, execmoduletester.WithTxPool(), execmoduletester.WithChainConfig(chain.AllProtocolChanges))
+ exec := m.ExecModule
+
+ chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 2, func(i int, gen *blockgen.BlockGen) {
+ gen.SetCoinbase(common.Address{1})
+ }, m.PublishedSD())
+ require.NoError(t, err)
+ require.NoError(t, insertValidateAndUfc1By1(ctx, exec, chainPack.Blocks))
+ head := chainPack.TopBlock // block 2 — the head
+ oldParent := chainPack.Blocks[0] // block 1 — canonical, but not the head
+
+ mkParams := func(parent common.Hash) *builder.Parameters {
+ return &builder.Parameters{
+ ParentHash: parent,
+ Timestamp: head.Time() + 1,
+ PrevRandao: head.Header().MixDigest,
+ SuggestedFeeRecipient: common.Address{1},
+ Withdrawals: make([]*types.Withdrawal, 0),
+ ParentBeaconBlockRoot: func() *common.Hash { h := randomHash(); return &h }(),
+ }
+ }
+
+ res, err := exec.AssembleBlock(ctx, mkParams(oldParent.Hash()))
+ require.NoError(t, err)
+ require.True(t, res.Busy, "assemble on an old canonical ancestor must return Busy")
+
+ payloadId, err := assembleBlock(ctx, exec, mkParams(head.Hash()))
+ require.NoError(t, err)
+ block, err := getAssembledBlock(ctx, exec, payloadId)
+ require.NoError(t, err)
+ require.Equal(t, head.Hash(), block.ParentHash(), "assembled block must extend the head")
+}
+
func TestAssembleBlockWithContractCreation(t *testing.T) {
t.Parallel()
ctx := t.Context()
@@ -1098,7 +1181,7 @@ func TestAssembleBlockWithContractCreation(t *testing.T) {
tx, txErr := types.SignTx(types.NewTransaction(gen.TxNonce(m.Address), common.Address{1}, uint256.NewInt(10_000), params.TxGas, uint256.NewInt(m.Genesis.BaseFee().Uint64()), nil), *types.LatestSignerForChainID(m.ChainConfig.ChainID), m.Key)
require.NoError(t, txErr)
gen.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -1168,7 +1251,7 @@ func TestAssembleBlockGasOverflow(t *testing.T) {
// Generate 1 empty block as initial chain state.
chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1,
- func(i int, gen *blockgen.BlockGen) {})
+ func(i int, gen *blockgen.BlockGen) {}, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -1246,7 +1329,7 @@ func TestAssembleBlockMixedTxTypes(t *testing.T) {
tx, txErr := types.SignTx(types.NewTransaction(gen.TxNonce(m.Address), common.Address{1}, uint256.NewInt(10_000), params.TxGas, uint256.NewInt(m.Genesis.BaseFee().Uint64()), nil), *types.LatestSignerForChainID(m.ChainConfig.ChainID), m.Key)
require.NoError(t, txErr)
gen.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -1339,7 +1422,7 @@ func TestAssembleBlockWithWithdrawalRequest(t *testing.T) {
)
require.NoError(t, err)
gen.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -1641,14 +1724,16 @@ func TestGetPayloadBodiesRegenerateBlockAccessLists(t *testing.T) {
func TestGetPayloadBodiesEmptyBlockAccessList(t *testing.T) {
t.Parallel()
- m, chainPack := newPayloadBodiesBALTestChain(t, chain.AllProtocolChanges)
- block := chainPack.Blocks[0]
- require.NotNil(t, block.Header().BlockAccessListHash)
- err := m.DB.Update(t.Context(), func(tx kv.RwTx) error {
- return rawdb.WriteBlockAccessListBytes(tx, block.Hash(), block.NumberU64(), []byte{0xc0})
- })
+ m := execmoduletester.New(t, execmoduletester.WithChainConfig(chain.AllProtocolChanges))
+ chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, nil)
require.NoError(t, err)
- requirePayloadBodiesBlockAccessList(t, m, block, []byte{0xc0})
+ block := chainPack.Blocks[0]
+ // A present-but-empty BAL (empty RLP list) is served verbatim, not treated
+ // as absent and regenerated. Write the block and its empty BAL to the DB and
+ // read them back through a synchronous getter that reads the DB directly.
+ writeCanonicalBlockFixture(t, m.DB, block, []byte{0xc0})
+ em := execmodule.NewGetterMockForTest(m.DB, m.BlockReader, m.Engine, m.ChainConfig, m.Log)
+ requireMockGetterBAL(t, em, block, []byte{0xc0})
}
func TestGetPayloadBodiesPreAmsterdamBlockAccessList(t *testing.T) {
@@ -1667,8 +1752,12 @@ func TestGetPayloadBodiesPrunedHistoryBlockAccessList(t *testing.T) {
m, chainPack := newPayloadBodiesBALTestChain(t, chain.AllProtocolChanges)
block := chainPack.Blocks[0]
require.NotNil(t, block.Header().BlockAccessListHash)
+ // Remove the stored BAL and the state history it could be regenerated from,
+ // then read through a synchronous DB-backed getter: with nothing to serve
+ // and no history to re-execute against, it must degrade to nil.
prunePayloadBodiesBALHistory(t, m, block)
- requirePayloadBodiesBlockAccessList(t, m, block, nil)
+ em := execmodule.NewGetterMockForTest(m.DB, m.BlockReader, m.Engine, m.ChainConfig, m.Log)
+ requireMockGetterBAL(t, em, block, nil)
}
func newPayloadBodiesBALTestChain(t *testing.T, config *chain.Config) (*execmoduletester.ExecModuleTester, *blockgen.ChainPack) {
@@ -1694,6 +1783,44 @@ func requirePayloadBodiesBlockAccessList(t *testing.T, m *execmoduletester.ExecM
require.Equal(t, want, byRange[0].BlockAccessList)
}
+// writeCanonicalBlockFixture stores a block (header, canonical marker, body) and
+// optionally its BAL directly in the DB, without going through the execution
+// module — for getter tests that assert DB-sourced serving deterministically.
+func writeCanonicalBlockFixture(t *testing.T, db kv.TemporalRwDB, block *types.Block, bal []byte) {
+ t.Helper()
+ require.NoError(t, db.Update(t.Context(), func(tx kv.RwTx) error {
+ if err := rawdb.WriteHeader(tx, block.Header()); err != nil {
+ return err
+ }
+ if err := rawdb.WriteCanonicalHash(tx, block.Hash(), block.NumberU64()); err != nil {
+ return err
+ }
+ if _, err := rawdb.WriteRawBodyIfNotExists(tx, block.Hash(), block.NumberU64(), block.RawBody()); err != nil {
+ return err
+ }
+ if bal != nil {
+ return rawdb.WriteBlockAccessListBytes(tx, block.Hash(), block.NumberU64(), bal)
+ }
+ return nil
+ }))
+}
+
+// requireMockGetterBAL asserts the payload-bodies getters resolve the block's
+// BAL to want, reading through a synchronous DB-backed ExecModule.
+func requireMockGetterBAL(t *testing.T, em *execmodule.ExecModule, block *types.Block, want []byte) {
+ t.Helper()
+ byHash, err := em.GetPayloadBodiesByHash(t.Context(), []common.Hash{block.Hash()})
+ require.NoError(t, err)
+ require.Len(t, byHash, 1)
+ require.NotNil(t, byHash[0])
+ require.Equal(t, want, byHash[0].BlockAccessList)
+ byRange, err := em.GetPayloadBodiesByRange(t.Context(), block.NumberU64(), 1)
+ require.NoError(t, err)
+ require.Len(t, byRange, 1)
+ require.NotNil(t, byRange[0])
+ require.Equal(t, want, byRange[0].BlockAccessList)
+}
+
func prunePayloadBodiesBALHistory(t *testing.T, m *execmoduletester.ExecModuleTester, block *types.Block) {
t.Helper()
err := m.DB.Update(t.Context(), func(tx kv.RwTx) error {
@@ -1791,7 +1918,7 @@ func TestNotificationDispatchForegroundCommit(t *testing.T) {
headerCh, unsub := m.Notifications.Events.AddHeaderSubscription()
defer unsub()
- chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 3, nil)
+ chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 3, nil, m.PublishedSD())
require.NoError(t, err)
err = insertValidateAndUfc1By1(ctx, exec, chainPack.Blocks)
@@ -1822,18 +1949,12 @@ func TestNotificationDispatchForegroundCommit(t *testing.T) {
// test only processes the genesis → block 1 transition to verify that
// notification dispatch works correctly in the background commit path.
func TestNotificationDispatchBackgroundCommit(t *testing.T) {
- // Background commit creates a race: FCU N returns before commit finishes,
- // so FCU N+1 reads stale state from DB. This is the known limitation that
- // the API-layer "latest head pointer" coordination is designed to solve.
- // Once that's implemented, remove this skip and verify the full flow.
- t.Skip("background commit requires API-layer coordination (latest head pointer) to work correctly")
-
m := execmoduletester.New(t, execmoduletester.WithFcuBackgroundCommit())
headerCh, unsub := m.Notifications.Events.AddHeaderSubscription()
defer unsub()
- chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, nil)
+ chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, nil, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
@@ -1855,7 +1976,7 @@ func TestNotificationDispatchBackgroundPrune(t *testing.T) {
headerCh, unsub := m.Notifications.Events.AddHeaderSubscription()
defer unsub()
- chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 3, nil)
+ chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 3, nil, m.PublishedSD())
require.NoError(t, err)
err = insertValidateAndUfc1By1(ctx, exec, chainPack.Blocks)
@@ -1927,7 +2048,7 @@ func TestAssembleBlockStateGasLimit(t *testing.T) {
// Generate 1 empty block as initial chain state.
chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1,
- func(i int, gen *blockgen.BlockGen) {})
+ func(i int, gen *blockgen.BlockGen) {}, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -2032,7 +2153,7 @@ func TestAssembleBlockStateGasLimitSSTORE(t *testing.T) {
// Generate block 1 with the deployment.
chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1,
- func(i int, gen *blockgen.BlockGen) { gen.AddTx(deployTx) })
+ func(i int, gen *blockgen.BlockGen) { gen.AddTx(deployTx) }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chainPack)
require.NoError(t, err)
@@ -2158,7 +2279,7 @@ func TestAssembleBlockGasPoolSnapshotRestoreBug(t *testing.T) {
txpool := m.TxPoolGrpcServer
chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1,
- func(i int, gen *blockgen.BlockGen) {})
+ func(i int, gen *blockgen.BlockGen) {}, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(chainPack))
@@ -2259,7 +2380,7 @@ func TestAssembleBlockGasPoolMultiBatchInitBug(t *testing.T) {
txpool := m.TxPoolGrpcServer
chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1,
- func(i int, gen *blockgen.BlockGen) {})
+ func(i int, gen *blockgen.BlockGen) {}, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(chainPack))
@@ -2385,7 +2506,7 @@ func TestEIP8246NoBurnLogWhenCoinbaseSelfDestructs(t *testing.T) {
)
require.NoError(t, txErr)
gen.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.Len(t, chainPack.Receipts[0], 1)
@@ -2448,7 +2569,7 @@ func TestInsertBlocksWithBatchedFCU(t *testing.T) {
)
require.NoError(t, err)
b.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.Len(t, chainPack.Blocks, totalBlocks)
@@ -2469,6 +2590,7 @@ func TestInsertBlocksWithBatchedFCU(t *testing.T) {
// After each batch's FCU, TxNums + execution must have advanced to
// the batch tip. The next batch's first block reads its parent's TD
// from this committed state.
+ m.ExecModule.WaitCommitsDrained()
require.NoError(t, m.DB.ViewTemporal(ctx, func(tx kv.TemporalTx) error {
lastTxNumBlock, _, err := rawdbv3.TxNums.Last(tx)
require.NoError(t, err)
@@ -2677,7 +2799,7 @@ func TestLargeBatchExecGeneratesChangesetsForReorgWindow(t *testing.T) {
chainLen := int(maxReorgDepth) + 14
chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, chainLen,
- transferGen(t, privKey, senderAddr, 1_000))
+ transferGen(t, privKey, senderAddr, 1_000), m.PublishedSD())
require.NoError(t, err)
insRes, err := insertBlocks(ctx, m.ExecModule, chainPack.Blocks)
@@ -2715,7 +2837,7 @@ func TestUpdateForkChoiceShallowReorgAfterLargeBatchExec(t *testing.T) {
divergeFrom := chainLen - reorgDepth
canonical, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, chainLen,
- transferGen(t, privKey, senderAddr, 1_000))
+ transferGen(t, privKey, senderAddr, 1_000), m.PublishedSD())
require.NoError(t, err)
fork, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, chainLen,
func(i int, b *blockgen.BlockGen) {
@@ -2745,6 +2867,7 @@ func TestUpdateForkChoiceShallowReorgAfterLargeBatchExec(t *testing.T) {
"shallow reorg of %d blocks after a %d-block batch must succeed; status=%s validationError=%q",
reorgDepth, chainLen, fcuRes.Status, fcuRes.ValidationError)
+ m.ExecModule.WaitCommitsDrained()
require.NoError(t, m.DB.ViewTemporal(ctx, func(tx kv.TemporalTx) error {
execProg, err := stages.GetStageProgress(tx, stages.Execution)
require.NoError(t, err)
@@ -2858,7 +2981,7 @@ func runBALComputeAheadChangeset(t *testing.T, computeAhead, shadow bool) balCom
)
require.NoError(t, txErr)
b.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
insRes, err := insertBlocksWithBAL(ctx, m.ExecModule, canonical.Blocks, canonical.BlockAccessLists)
@@ -2869,7 +2992,7 @@ func runBALComputeAheadChangeset(t *testing.T, computeAhead, shadow bool) balCom
require.Equal(t, execmodule.ExecutionStatusSuccess, fcuRes.Status,
"batch with compute-ahead=%v must execute cleanly; validationError=%q", computeAhead, fcuRes.ValidationError)
- m.ExecModule.WaitIdle(ctx)
+ m.ExecModule.WaitCommitsDrained()
res := balComputeAheadResult{
windowCommitmentKeys: map[uint64][]string{},
@@ -2913,7 +3036,7 @@ func runBALComputeAheadChangeset(t *testing.T, computeAhead, shadow bool) balCom
back, err := updateForkChoice(ctx, m.ExecModule, canonical.Blocks[reorgBackTo-1].Header())
require.NoError(t, err)
require.Equal(t, execmodule.ExecutionStatusSuccess, back.Status, "reorg back must succeed")
- m.ExecModule.WaitIdle(ctx)
+ m.ExecModule.WaitCommitsDrained()
require.NoError(t, m.DB.ViewTemporal(ctx, func(tx kv.TemporalTx) error {
execProg, err := stages.GetStageProgress(tx, stages.Execution)
require.NoError(t, err)
@@ -2926,7 +3049,7 @@ func runBALComputeAheadChangeset(t *testing.T, computeAhead, shadow bool) balCom
require.Equal(t, execmodule.ExecutionStatusSuccess, fwd.Status,
"forward re-exec after unwind must reach the correct root (compute-ahead=%v); validationError=%q",
computeAhead, fwd.ValidationError)
- m.ExecModule.WaitIdle(ctx)
+ m.ExecModule.WaitCommitsDrained()
require.NoError(t, m.DB.ViewTemporal(ctx, func(tx kv.TemporalTx) error {
execProg, err := stages.GetStageProgress(tx, stages.Execution)
require.NoError(t, err)
@@ -2937,6 +3060,135 @@ func runBALComputeAheadChangeset(t *testing.T, computeAhead, shadow bool) balCom
return res
}
+// TestBALFoldAheadFiresOnTipValidateChain pins the goal of the changeset-ownership
+// refactor: a single chain-tip newPayload (ValidateChain) must fold its commitment
+// from the BAL. It fails today because ownsChangeset forces the tip onto the
+// incremental path; it passes once the changeset is produced result-locally by the
+// commitment calculator and the ownsChangeset fold gate is removed.
+func TestBALFoldAheadFiresOnTipValidateChain(t *testing.T) {
+ defer func(prev bool) { dbg.BALDrivenCommitment = prev }(dbg.BALDrivenCommitment)
+ defer func(prev bool) { dbg.IgnoreBAL = prev }(dbg.IgnoreBAL)
+ dbg.BALDrivenCommitment = true
+ dbg.IgnoreBAL = false
+
+ const chainLen = 6
+ ctx := t.Context()
+ privKey, err := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ require.NoError(t, err)
+ senderAddr := crypto.PubkeyToAddress(privKey.PublicKey)
+ m := execmoduletester.New(t,
+ execmoduletester.WithKey(privKey),
+ execmoduletester.WithGenesisSpec(&types.Genesis{
+ Config: chain.AllProtocolChanges,
+ Alloc: types.GenesisAlloc{senderAddr: {Balance: big.NewInt(1 * common.Ether)}},
+ }),
+ execmoduletester.WithExperimentalBAL(),
+ execmoduletester.WithAlwaysGenerateChangesets(false),
+ )
+ canonical, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, chainLen,
+ func(i int, b *blockgen.BlockGen) {
+ tx, txErr := types.SignTx(
+ types.NewTransaction(uint64(i), senderAddr, uint256.NewInt(1_000), 50000, uint256.NewInt(10_000_000_000), nil),
+ *types.LatestSignerForChainID(nil), privKey,
+ )
+ require.NoError(t, txErr)
+ b.AddTx(tx)
+ }, m.PublishedSD())
+ require.NoError(t, err)
+
+ // Commit all but the tip so the tip is a single-block newPayload whose parent is
+ // already committed — the fold baseline comes from the prior cycle.
+ for i := range chainLen - 1 {
+ _, err := insertBlocksWithBAL(ctx, m.ExecModule, canonical.Blocks[i:i+1], canonical.BlockAccessLists[i:i+1])
+ require.NoError(t, err)
+ _, err = validateChain(ctx, m.ExecModule, canonical.Blocks[i].Header())
+ require.NoError(t, err)
+ _, err = updateForkChoice(ctx, m.ExecModule, canonical.Blocks[i].Header())
+ require.NoError(t, err)
+ }
+ m.ExecModule.WaitCommitsDrained()
+
+ tip := canonical.TopBlock
+ tipBAL := canonical.BlockAccessLists[chainLen-1]
+ // The tip payload must carry a BAL — that is the precondition that lets its
+ // commitment be folded in parallel with execution instead of computed
+ // incrementally from the per-tx result stream.
+ require.NotEmpty(t, tipBAL, "tip newPayload must carry a BAL")
+ _, err = insertBlocksWithBAL(ctx, m.ExecModule, []*types.Block{tip}, [][]byte{tipBAL})
+ require.NoError(t, err)
+
+ stagedsync.ResetComputedAheadForTest()
+ vr, err := validateChain(ctx, m.ExecModule, tip.Header())
+ require.NoError(t, err)
+ require.Equal(t, execmodule.ExecutionStatusSuccess, vr.ValidationStatus)
+
+ // With a BAL present, the tip's commitment is folded from it (in parallel with
+ // exec) rather than computed on the incremental path.
+ require.Positive(t, stagedsync.ComputedAheadCountForTest(),
+ "tip newPayload (ValidateChain) must fold its commitment from the BAL")
+}
+
+// TestBALFoldTipStorageClearValidateChain folds a single Amsterdam block whose tx
+// CLEARS a genesis-preset storage slot (SSTORE existing non-zero slot → 0) on an
+// account that also receives value (balance change). This is the storage-delete /
+// subtree-collapse case that a non-zero write does not exercise, and it is what
+// exposed a wrong folded root: the fold runs ahead of exec, so its as-of reader
+// returned pre-block state (slot still present) during the collapse. Mirrors the
+// EEST blockchain_test_from_state_test shape (one block on genesis).
+func TestBALFoldTipStorageClearValidateChain(t *testing.T) {
+ defer func(prev bool) { dbg.BALDrivenCommitment = prev }(dbg.BALDrivenCommitment)
+ defer func(prev bool) { dbg.IgnoreBAL = prev }(dbg.IgnoreBAL)
+ dbg.BALDrivenCommitment = true
+ dbg.IgnoreBAL = false
+
+ privKey, err := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ require.NoError(t, err)
+ senderAddr := crypto.PubkeyToAddress(privKey.PublicKey)
+ contractAddr := common.HexToAddress("0x00000000000000000000000000000000c0de5501")
+ otherAddr := common.HexToAddress("0x00000000000000000000000000000000c0de5502")
+ m := execmoduletester.New(t,
+ execmoduletester.WithKey(privKey),
+ execmoduletester.WithGenesisSpec(&types.Genesis{
+ Config: chain.AllProtocolChanges,
+ Alloc: types.GenesisAlloc{
+ senderAddr: {Balance: big.NewInt(1 * common.Ether)},
+ // Pre-set slot 0, then have the tx SSTORE(0,0) to CLEAR it (and send
+ // value, so the account leaf recomputes against the collapsed storage).
+ contractAddr: {
+ Balance: big.NewInt(0),
+ Code: []byte{0x60, 0x00, 0x60, 0x00, 0x55, 0x00}, // SSTORE(0,0); STOP
+ Storage: map[common.Hash]common.Hash{{}: common.BigToHash(big.NewInt(3))},
+ },
+ // A second storage-bearing account so the account trie has siblings.
+ otherAddr: {
+ Balance: big.NewInt(1),
+ Storage: map[common.Hash]common.Hash{{}: common.BigToHash(big.NewInt(7))},
+ },
+ },
+ }),
+ execmoduletester.WithExperimentalBAL(),
+ execmoduletester.WithAlwaysGenerateChangesets(false),
+ )
+ canonical, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1,
+ func(i int, b *blockgen.BlockGen) {
+ tx, txErr := types.SignTx(
+ types.NewTransaction(0, contractAddr, uint256.NewInt(1_000), 100000, uint256.NewInt(10_000_000_000), nil),
+ *types.LatestSignerForChainID(nil), privKey,
+ )
+ require.NoError(t, txErr)
+ b.AddTx(tx)
+ }, m.PublishedSD())
+ require.NoError(t, err)
+
+ require.NotEmpty(t, canonical.BlockAccessLists[0], "block must carry a BAL")
+ stagedsync.ResetComputedAheadForTest()
+ // Insert + FCU in one step (no intervening ValidateChain), matching the EEST
+ // blockchain-test path: the fold runs during the FCU's execution and a wrong
+ // folded root surfaces as a BadBlock here.
+ require.NoError(t, m.InsertChain(canonical))
+ require.Positive(t, stagedsync.ComputedAheadCountForTest(), "block must fold from its BAL")
+}
+
// A forkchoice head at height 0 that is not the genesis (e.g. a block carrying a
// corrupted number) must be rejected, not treated as a genesis reset.
func TestUpdateForkChoiceToNonGenesisBlockAtHeightZero(t *testing.T) {
diff --git a/execution/execmodule/exec_module_unwind_gap_test.go b/execution/execmodule/exec_module_unwind_gap_test.go
index 6d1acff3a93..638e1260cd4 100644
--- a/execution/execmodule/exec_module_unwind_gap_test.go
+++ b/execution/execmodule/exec_module_unwind_gap_test.go
@@ -76,7 +76,7 @@ func TestUpdateForkChoiceBadBlockMidBatchThenRecovery(t *testing.T) {
)
require.NoError(t, err)
b.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.Len(t, chainPack.Blocks, chainLen)
@@ -153,6 +153,7 @@ func TestUpdateForkChoiceBadBlockMidBatchThenRecovery(t *testing.T) {
_, err = updateForkChoice(ctx, m.ExecModule, block13.Header())
require.NoError(t, err)
+ m.ExecModule.WaitCommitsDrained()
var acc accounts.Account
require.NoError(t, m.DB.ViewTemporal(ctx, func(tx kv.TemporalTx) error {
v, _, err := tx.GetLatest(kv.AccountsDomain, senderAddr[:])
@@ -196,7 +197,7 @@ func TestUpdateForkChoiceBadBlockAtLongBatchTailThenRecovery(t *testing.T) {
}
chainPack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, chainLen+1, func(i int, b *blockgen.BlockGen) {
b.AddTx(mkTx(i, 1_000))
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, insertValidateAndUfc1By1(ctx, m.ExecModule, chainPack.Blocks[:committedTo]))
@@ -243,6 +244,7 @@ func TestUpdateForkChoiceBadBlockAtLongBatchTailThenRecovery(t *testing.T) {
_, err = updateForkChoice(ctx, m.ExecModule, forkTip.Header())
require.NoError(t, err)
+ m.ExecModule.WaitCommitsDrained()
var acc accounts.Account
require.NoError(t, m.DB.ViewTemporal(ctx, func(tx kv.TemporalTx) error {
v, _, err := tx.GetLatest(kv.AccountsDomain, senderAddr[:])
diff --git a/execution/execmodule/execmoduletester/exec_module_tester.go b/execution/execmodule/execmoduletester/exec_module_tester.go
index b01020e34b6..f719e5366a9 100644
--- a/execution/execmodule/execmoduletester/exec_module_tester.go
+++ b/execution/execmodule/execmoduletester/exec_module_tester.go
@@ -27,7 +27,6 @@ import (
"time"
"github.com/c2h5oh/datasize"
- "github.com/holiman/uint256"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"golang.org/x/sync/errgroup"
@@ -54,7 +53,9 @@ import (
"github.com/erigontech/erigon/db/snapshotsync/freezeblocks"
"github.com/erigontech/erigon/db/snaptype"
dbstate "github.com/erigontech/erigon/db/state"
+ "github.com/erigontech/erigon/db/state/execctx"
"github.com/erigontech/erigon/execution/builder"
+ "github.com/erigontech/erigon/execution/cache"
"github.com/erigontech/erigon/execution/chain"
"github.com/erigontech/erigon/execution/exec"
"github.com/erigontech/erigon/execution/execmodule"
@@ -125,6 +126,7 @@ type ExecModuleTester struct {
ForkValidator *execmodule.ForkValidator
ExecModule *execmodule.ExecModule
StateCache *execmodule.Cache
+ domainCache *cache.StateCache
retirementStart chan bool
retirementDone chan struct{}
retirementWg sync.WaitGroup
@@ -148,6 +150,13 @@ type ExecModuleTester struct {
}
func (emt *ExecModuleTester) Close() {
+ // Drain and stop the background-commit worker (and its in-flight commit tx)
+ // before closing the DB, mirroring production shutdown. Otherwise DB.Close
+ // can race a commit still writing, which deadlocks. Must run before cancel()
+ // so WaitIdle can still acquire the foreground semaphore.
+ if emt.ExecModule != nil {
+ emt.ExecModule.WaitIdle(emt.Ctx)
+ }
emt.cancel()
if err := emt.bgComponentsEg.Wait(); err != nil && emt.tb != nil {
require.Equal(emt.tb, context.Canceled, err) // upon waiting for clean exit we should get ctx cancelled
@@ -167,8 +176,8 @@ func (emt *ExecModuleTester) Close() {
if emt.DB != nil {
emt.DB.Close()
}
- if emt.ExecModule != nil {
- emt.ExecModule.Close()
+ if emt.domainCache != nil {
+ emt.domainCache.Close()
}
if emt.tb == nil && emt.Dirs.DataDir != "" {
dir.RemoveAll(emt.Dirs.DataDir)
@@ -353,6 +362,12 @@ func WithFcuBackgroundCommit() Option {
}
}
+func WithFcuForegroundCommit() Option {
+ return func(opts *options) {
+ opts.fcuBackgroundCommit = false
+ }
+}
+
// WithAlwaysGenerateChangesets pins --experimental.always-generate-changesets
// regardless of the tester default: true for tests that reorg deeper than
// MaxReorgDepth, false for tests that rely on the windowed-changesets
@@ -406,11 +421,12 @@ func applyOptions(opts []Option) options {
defaultKey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
defaultPruneMode := prune.MockMode
opt := options{
- key: defaultKey,
- pruneMode: &defaultPruneMode,
- chainConfig: chain.TestChainBerlinConfig,
- experimentalBAL: false,
- sentryProtocol: direct.ETH68,
+ key: defaultKey,
+ pruneMode: &defaultPruneMode,
+ chainConfig: chain.TestChainBerlinConfig,
+ experimentalBAL: false,
+ sentryProtocol: direct.ETH68,
+ fcuBackgroundCommit: false,
}
for _, o := range opts {
o(&opt)
@@ -705,7 +721,6 @@ func New(tb testing.TB, opts ...Option) *ExecModuleTester {
mock.TxPool,
sealCancel,
latestBlockBuiltStore,
- nil, /*sdProvider*/
logger,
)
@@ -774,6 +789,10 @@ func New(tb testing.TB, opts ...Option) *ExecModuleTester {
Accumulator: mock.Notifications.Accumulator,
RecentReceipts: mock.Notifications.RecentReceipts,
}
+ // Per-instance domain cache, held on the tester so Close releases its
+ // envelope reservation. Uses the production default — the caches jump-grow on
+ // demand, so a small-working-set fixture stays small.
+ mock.domainCache = cache.NewDefaultStateCache()
mock.ExecModule = execmodule.NewExecModule(
ctx,
mock.BlockReader,
@@ -785,7 +804,7 @@ func New(tb testing.TB, opts ...Option) *ExecModuleTester {
hook,
accum,
mock.StateCache,
- 0, // stateCacheBudget: production default; the caches jump-grow on demand
+ 0, // stateCacheBudget: production default; caches jump-grow on demand
logger,
engine,
cfg.Sync,
@@ -797,6 +816,13 @@ func New(tb testing.TB, opts ...Option) *ExecModuleTester {
)
mock.ForkValidator = mock.ExecModule.ForkValidator()
+ // Mirror production (node/eth/backend.go): readers fall back to the
+ // published in-flight SharedDomains when no foreground context is active,
+ // so consensus-table reads see not-yet-committed state under background
+ // commit instead of a stale raw DB.
+ mock.ExecModule.SetPublishedSD(mock.Notifications.Events.LatestSD)
+ mock.StateCache.SetPublishedSD(mock.Notifications.Events.LatestSD)
+
mock.StreamWg.Add(1)
mock.bgComponentsEg.Go(func() error {
mock.sentriesClient.RecvMessageLoop(mock.Ctx, mock.SentryClient, &mock.ReceiveWg)
@@ -846,7 +872,15 @@ func (emt *ExecModuleTester) EnableLogs() {
func (emt *ExecModuleTester) Cfg() ethconfig.Config { return emt.cfg }
-func (emt *ExecModuleTester) insertChain(chain *blockgen.ChainPack) error {
+// PublishedSD returns the latest published SharedDomains (the tip after the
+// last FCU). Pass it to blockgen.GenerateChain when building on the tip so the
+// generator reads the tip's in-flight state under background commit instead of
+// a lagging raw DB.
+func (emt *ExecModuleTester) PublishedSD() *execctx.SharedDomains {
+ return emt.Notifications.Events.LatestSD()
+}
+
+func (emt *ExecModuleTester) insertPoSBlocks(chain *blockgen.ChainPack) error {
wr := chainreader.NewChainReaderEth1(emt.ChainConfig, emt.ExecModule, time.Hour)
streamCtx, cancel := context.WithCancel(emt.Ctx)
@@ -871,9 +905,23 @@ func (emt *ExecModuleTester) insertChain(chain *blockgen.ChainPack) error {
tipHash := chain.TopBlock.Hash()
- status, verr, _, err := wr.UpdateForkChoice(emt.Ctx, tipHash, tipHash, tipHash)
- if err != nil {
- return err
+ // Busy means the background-commit backlog is full; the real CL retries the
+ // FCU, giving the commit worker a foreground-idle window to drain. Mirror
+ // that here instead of failing the insert.
+ var status execmodule.ExecutionStatus
+ var verr *string
+ for {
+ status, verr, _, err = wr.UpdateForkChoice(emt.Ctx, tipHash, tipHash, tipHash)
+ if err != nil {
+ return err
+ }
+ if status != execmodule.ExecutionStatusBusy {
+ break
+ }
+ if emt.Ctx.Err() != nil {
+ return emt.Ctx.Err()
+ }
+ time.Sleep(time.Millisecond)
}
if status != execmodule.ExecutionStatusSuccess {
@@ -892,7 +940,7 @@ func (emt *ExecModuleTester) insertChain(chain *blockgen.ChainPack) error {
req, err := stream.Recv()
if err != nil {
if emt.Ctx.Err() != nil {
- break
+ return nil
}
if streamCtx.Err() != nil {
return fmt.Errorf("block insert recv timed out: %d remaining", len(insertedBlocks))
@@ -911,11 +959,27 @@ func (emt *ExecModuleTester) insertChain(chain *blockgen.ChainPack) error {
}
}
- roTx, err := emt.DB.BeginRo(emt.Ctx)
+ return nil
+}
+
+func (emt *ExecModuleTester) InsertChain(chain *blockgen.ChainPack) error {
+ if err := emt.insertPoSBlocks(chain); err != nil {
+ return err
+ }
+ baseTx, err := emt.DB.BeginTemporalRo(emt.Ctx)
if err != nil {
return err
}
- defer roTx.Rollback()
+ defer baseTx.Rollback()
+ // Under background commit the just-inserted block metadata (header, stage
+ // progress, head hash) lives in the published SharedDomains overlay before
+ // it lands in the raw DB, so read through the overlay when one is published.
+ var roTx kv.Tx = baseTx
+ if sd := emt.PublishedSD(); sd != nil {
+ if v := sd.OverlayTemporalTx(baseTx); v != nil {
+ roTx = v
+ }
+ }
// Check if the latest header was imported or rolled back
if rawdb.ReadHeader(roTx, chain.TopBlock.Hash(), chain.TopBlock.NumberU64()) == nil {
return fmt.Errorf("did not import block %d %x", chain.TopBlock.NumberU64(), chain.TopBlock.Hash())
@@ -930,68 +994,13 @@ func (emt *ExecModuleTester) insertChain(chain *blockgen.ChainPack) error {
if rawdb.ReadHeadBlockHash(roTx) != chain.TopBlock.Hash() {
return fmt.Errorf("did not import block %d %x", chain.TopBlock.NumberU64(), chain.TopBlock.Hash())
}
+ // Under background commit InsertChain returns before its commit lands. Drain
+ // it before returning so a following operation (a reorg's unwind, a raw-DB
+ // read) cannot race the still-in-flight commit of the chain just inserted.
+ emt.ExecModule.WaitCommitsDrained()
return nil
}
-func (emt *ExecModuleTester) insertChainPoW(chain *blockgen.ChainPack) error {
- tip := chain.TopBlock
- currentHeader, err := emt.ExecModule.CurrentHeader(emt.Ctx)
- if err != nil {
- return err
- }
- currentHash := currentHeader.Hash()
- currentNumber := currentHeader.Number.Uint64()
- currentTd, err := emt.ExecModule.GetTD(emt.Ctx, ¤tHash, ¤tNumber)
- if err != nil {
- return err
- }
- if currentTd == nil {
- return fmt.Errorf("total difficulty not found for current head %d %x", currentNumber, currentHash)
- }
- firstBlock := chain.Blocks[0]
- firstNumber := firstBlock.NumberU64()
- if firstNumber == 0 {
- return emt.insertChain(chain)
- }
- parentHash := firstBlock.ParentHash()
- parentNumber := firstNumber - 1
- parentTd, err := emt.ExecModule.GetTD(emt.Ctx, &parentHash, &parentNumber)
- if err != nil {
- return err
- }
- if parentTd == nil {
- return fmt.Errorf("total difficulty not found for parent %d %x", parentNumber, parentHash)
- }
- candidateTd := new(uint256.Int).Set(parentTd)
- for _, block := range chain.Blocks {
- difficulty := block.Difficulty()
- if _, overflow := candidateTd.AddOverflow(candidateTd, &difficulty); overflow {
- return fmt.Errorf("total difficulty overflows for block %d %x", block.NumberU64(), block.Hash())
- }
- }
- if !ethash.ShouldReorg(currentTd, currentNumber, currentHash, candidateTd, tip.NumberU64(), tip.Hash()) {
- wr := chainreader.NewChainReaderEth1(emt.ChainConfig, emt.ExecModule, time.Hour)
- for _, block := range chain.Blocks {
- if err := block.HashCheck(false); err != nil {
- return err
- }
- }
- return wr.InsertBlocks(emt.Ctx, chain.Blocks, chain.BlockAccessLists)
- }
- return emt.insertChain(chain)
-}
-
-func (emt *ExecModuleTester) InsertChain(chain *blockgen.ChainPack) error {
- if chain.Length() == 0 {
- return nil
- }
- tipDifficulty := chain.TopBlock.Difficulty()
- if !tipDifficulty.IsZero() {
- return emt.insertChainPoW(chain)
- }
- return emt.insertChain(chain)
-}
-
func (emt *ExecModuleTester) NewHistoryStateReader(blockNum uint64, tx kv.TemporalTx) state.StateReader {
r, err := rpchelper.CreateHistoryStateReader(context.Background(), tx, blockNum, 0, emt.BlockReader.TxnumReader())
if err != nil {
diff --git a/execution/execmodule/execmoduletester/exec_module_tester_test.go b/execution/execmodule/execmoduletester/exec_module_tester_test.go
index 901cc1d25a3..f417d0a4a28 100644
--- a/execution/execmodule/execmoduletester/exec_module_tester_test.go
+++ b/execution/execmodule/execmoduletester/exec_module_tester_test.go
@@ -40,7 +40,7 @@ func TestInsertChain(t *testing.T) {
m := execmoduletester.New(t)
chain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 100, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chain)
require.NoError(t, err)
@@ -51,7 +51,7 @@ func TestReorgsWithInsertChain(t *testing.T) {
m := execmoduletester.New(t)
chain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 10, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
// insert initial chain
err = m.InsertChain(chain)
@@ -59,16 +59,16 @@ func TestReorgsWithInsertChain(t *testing.T) {
// Now generate three competing branches, one short and two longer ones
short, err := blockgen.GenerateChain(m.ChainConfig, chain.TopBlock, m.Engine, m.DB, 2, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
long1, err := blockgen.GenerateChain(m.ChainConfig, chain.TopBlock, m.Engine, m.DB, 10, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{2}) // Need to make headers different from short branch
- })
+ }, m.PublishedSD())
require.NoError(t, err)
// Second long chain needs to be slightly shorter than the first long chain
long2, err := blockgen.GenerateChain(m.ChainConfig, chain.TopBlock, m.Engine, m.DB, 9, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{3}) // Need to make headers different from short branch and another long branch
- })
+ }, m.PublishedSD())
require.NoError(t, err)
// insert short chain
err = m.InsertChain(short)
@@ -79,7 +79,7 @@ func TestReorgsWithInsertChain(t *testing.T) {
// another short chain
short2, err := blockgen.GenerateChain(m.ChainConfig, long1.TopBlock, m.Engine, m.DB, 2, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
// insert long2 chain
err = m.InsertChain(long2)
diff --git a/execution/execmodule/execmoduletester/overlay_read.go b/execution/execmodule/execmoduletester/overlay_read.go
new file mode 100644
index 00000000000..f470b98e022
--- /dev/null
+++ b/execution/execmodule/execmoduletester/overlay_read.go
@@ -0,0 +1,136 @@
+// Copyright 2024 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 execmoduletester
+
+import (
+ "context"
+
+ "github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/db/kv/order"
+ "github.com/erigontech/erigon/db/kv/stream"
+ "github.com/erigontech/erigon/db/state/execctx"
+)
+
+// OverlayDB returns a read-only DB whose transactions route reads through the
+// latest published SharedDomains: consensus-table reads see the block overlay
+// and domain-state reads see the in-flight (not-yet-committed) values. Pass it
+// to RPC-daemon APIs so their reads observe the tip under background commit
+// instead of a lagging raw DB.
+func (emt *ExecModuleTester) OverlayDB() kv.TemporalRoDB {
+ return &sdRoDB{TemporalRoDB: emt.DB, publishedSD: emt.PublishedSD}
+}
+
+type sdRoDB struct {
+ kv.TemporalRoDB
+ publishedSD func() *execctx.SharedDomains
+}
+
+func (d *sdRoDB) BeginTemporalRo(ctx context.Context) (kv.TemporalTx, error) {
+ base, err := d.TemporalRoDB.BeginTemporalRo(ctx) //nolint:gocritic // base is wrapped and returned; caller owns Rollback
+ if err != nil {
+ return nil, err
+ }
+ return wrapSDTx(base, d.publishedSD()), nil
+}
+
+func (d *sdRoDB) BeginRo(ctx context.Context) (kv.Tx, error) {
+ return d.BeginTemporalRo(ctx)
+}
+
+func (d *sdRoDB) ViewTemporal(ctx context.Context, f func(tx kv.TemporalTx) error) error {
+ tx, err := d.BeginTemporalRo(ctx)
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+ return f(tx)
+}
+
+func (d *sdRoDB) View(ctx context.Context, f func(tx kv.Tx) error) error {
+ tx, err := d.BeginTemporalRo(ctx)
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+ return f(tx)
+}
+
+// wrapSDTx layers domain- and table-level read-through onto base for the given
+// published SD. With no published SD it returns base unchanged.
+func wrapSDTx(base kv.TemporalTx, sd *execctx.SharedDomains) kv.TemporalTx {
+ if sd == nil {
+ return base
+ }
+ read := base
+ if v := sd.OverlayTemporalTx(base); v != nil {
+ read = v
+ }
+ return &sdRoTx{TemporalTx: read, base: base, sd: sd}
+}
+
+// sdRoTx serves consensus-table reads from the block overlay (the embedded
+// TemporalTx) and latest domain-state reads from the published SD's in-flight
+// chain. Historical (GetAsOf) reads try the in-flight mem first, then fall back
+// to committed state on the overlay tx.
+type sdRoTx struct {
+ kv.TemporalTx
+ base kv.TemporalTx
+ sd *execctx.SharedDomains
+}
+
+// PublishedSharedDomains exposes the published SharedDomains this tx reads
+// through, so a consumer building a fresh SharedDomains (e.g. eth_getProof) can
+// chain it as a parent to see the in-flight tip under background commit.
+func (t *sdRoTx) PublishedSharedDomains() *execctx.SharedDomains { return t.sd }
+
+func (t *sdRoTx) GetLatest(name kv.Domain, k []byte) ([]byte, kv.Step, error) {
+ return t.sd.GetLatest(name, t.base, k)
+}
+
+func (t *sdRoTx) HasPrefix(name kv.Domain, prefix []byte) ([]byte, []byte, bool, error) {
+ return t.sd.HasPrefix(name, prefix, t.base)
+}
+
+func (t *sdRoTx) GetAsOf(name kv.Domain, k []byte, ts uint64) ([]byte, bool, error) {
+ if v, ok, err := t.sd.GetAsOf(name, k, ts); err != nil || ok {
+ return v, ok, err
+ }
+ return t.TemporalTx.GetAsOf(name, k, ts)
+}
+
+func (t *sdRoTx) RangeAsOf(name kv.Domain, fromKey, toKey []byte, ts uint64, asc order.By, limit int) (stream.KV, error) {
+ return t.sd.RangeAsOf(context.Background(), name, fromKey, toKey, ts, asc, limit, t.base)
+}
+
+func (t *sdRoTx) HistoryRange(name kv.Domain, fromTs, toTs int, asc order.By, limit int) (stream.KV, error) {
+ return t.sd.HistoryRange(context.Background(), name, fromTs, toTs, asc, limit, t.base)
+}
+
+func (t *sdRoTx) IndexRange(name kv.InvertedIdx, k []byte, fromTs, toTs int, asc order.By, limit int) (stream.U64, error) {
+ return t.sd.IndexRange(name, k, fromTs, toTs, asc, limit, t.base)
+}
+
+func (t *sdRoTx) HistorySeek(name kv.Domain, k []byte, ts uint64) ([]byte, bool, error) {
+ if v, ok, err := t.sd.HistorySeek(name, k, ts); err != nil || ok {
+ return v, ok, err
+ }
+ return t.TemporalTx.HistorySeek(name, k, ts)
+}
+
+func (t *sdRoTx) Rollback() {
+ t.base.Rollback()
+}
diff --git a/execution/execmodule/forkchoice.go b/execution/execmodule/forkchoice.go
index 93b61132df9..fdee572c628 100644
--- a/execution/execmodule/forkchoice.go
+++ b/execution/execmodule/forkchoice.go
@@ -336,7 +336,19 @@ func (e *ExecModule) unwindIfNeeded(
}
func (e *ExecModule) updateForkChoice(ctx context.Context, originalBlockHash, safeHash, finalizedHash common.Hash, outcomeCh chan forkchoiceOutcome) (err error) {
- if !e.semaphore.TryAcquire(1) {
+ if e.fcuBackgroundCommit && e.commitBacklogFull() {
+ // Report Busy without acquiring the foreground semaphore so the caller
+ // retries, leaving a foreground-idle window for the background commit
+ // worker to drain the backlog. Otherwise back-to-back FCUs starve the
+ // worker (it commits only when no foreground op is active) and the
+ // generation overlay chain grows unbounded.
+ sendForkchoiceResultWithoutWaiting(outcomeCh, ForkChoiceResult{
+ LatestValidHash: common.Hash{},
+ Status: ExecutionStatusBusy,
+ }, false)
+ return fmt.Errorf("commit backlog full")
+ }
+ if !e.fgTryAcquire() {
e.logger.Trace("ethereumExecutionModule.updateForkChoice: ExecutionStatus_Busy")
sendForkchoiceResultWithoutWaiting(outcomeCh, ForkChoiceResult{
LatestValidHash: common.Hash{},
@@ -344,18 +356,35 @@ func (e *ExecModule) updateForkChoice(ctx context.Context, originalBlockHash, sa
}, false)
return fmt.Errorf("semaphore timeout")
}
- shouldReleaseSema := true
+ // The foreground slot is released when updateForkChoice returns, unless a
+ // background prune goroutine takes it over (pruneHandedOff): RunPrune shares
+ // the pipeline Sync with the next FCU's RunLoop, so the next FCU must stay
+ // Busy until the prune finishes. In that case the prune goroutine releases.
+ pruneHandedOff := false
defer func() {
- if shouldReleaseSema {
- e.semaphore.Release(1)
+ if !pruneHandedOff {
+ e.fgRelease()
}
}()
+ // Retire the in-flight commit-generation chain if every generation has
+ // committed. Safe here: the foreground semaphore is held,
+ // so no concurrent op is reading a generation's SharedDomains.
+ e.drainCommittedGens()
+
defer UpdateForkChoiceDuration(time.Now())
// The next semaphore acquirer must observe settled state, so the bg-commit/
// bg-prune paths run this eagerly before handing the semaphore to their goroutine.
cleanupBeforeSemaRelease := sync.OnceFunc(func() {
- e.currentContext.ResetPendingUpdates()
+ // e.currentContext is written by InsertBlocks under e.lock; read it under
+ // the same lock so this cleanup (which may run on the FCU goroutine while
+ // a later InsertBlocks proceeds) doesn't race that write.
+ e.lock.RLock()
+ cc := e.currentContext
+ e.lock.RUnlock()
+ if cc != nil {
+ cc.ResetPendingUpdates()
+ }
e.forkValidator.ClearWithUnwind()
})
defer cleanupBeforeSemaRelease()
@@ -400,12 +429,21 @@ func (e *ExecModule) updateForkChoice(ctx context.Context, originalBlockHash, sa
}
}
if currentContext != nil {
+ currentContext.SetReadCoordinator(e.beginCoordinatedRo)
currentContext.SetInMemHistoryReads(inMemHistoryReads)
// Wire the state cache so canonical execution reads benefit from
// the per-execution Account/Storage/Code cache. Previously only
// ValidateChain (fork validation, exec_module.go) set this, leaving
// the canonical execution path running uncached against the aggTx.
currentContext.SetStateCache(e.stateCache)
+ // Chain to the newest in-flight commit generation:
+ // if a previous FCU's commit has not yet landed, its domain state
+ // lives only in that generation's sd.mem — SetParent lets this FCU
+ // read through to it instead of a stale DB. nil when the chain is
+ // empty (all prior commits landed → DB is current).
+ if parent := e.latestGen(); parent != nil {
+ currentContext.SetParent(parent)
+ }
currentContext.SetCodeStore(e.codeStore)
}
@@ -428,7 +466,7 @@ func (e *ExecModule) updateForkChoice(ctx context.Context, originalBlockHash, sa
// hashes, stage progress, forkchoice markers, TxNums, etc.). All
// pipeline reads cascade through the overlay to the RO tx; writes
// stay in memory until commit.
- if err := currentContext.InitBlockOverlay(roTx, roTx.Debug().Dirs().Tmp); err != nil {
+ if err := currentContext.InitBlockOverlay(e.overlayBaseFor(roTx), roTx.Debug().Dirs().Tmp); err != nil {
return sendForkchoiceErrorWithoutWaiting(e.logger, outcomeCh, fmt.Errorf("updateForkChoice: init block overlay: %w", err), false)
}
var tx kv.TemporalRwTx = currentContext.BlockOverlay()
@@ -690,71 +728,83 @@ func (e *ExecModule) updateForkChoice(ctx context.Context, originalBlockHash, sa
e.closeModuleContext()
}
+ // Register the generation in the in-flight chain before publishing the
+ // overlay, so a reader that observes the newly published SD can also reach
+ // its uncommitted state as latestGen() when chaining parents.
+ var bgGen *commitGen
+ if e.fcuBackgroundCommit {
+ bgGen = &commitGen{
+ sd: currentContext,
+ roTx: roTx,
+ blockHash: blockHash,
+ blockNum: fcuHeader.Number.Uint64(),
+ finishProgressBefore: finishProgressBefore,
+ isSynced: isSynced,
+ initialCycle: initialCycle,
+ }
+ e.addGen(bgGen)
+ }
+
// Dispatch notifications from the SD overlay (before flush/commit).
// After this, all consumers have the data — the semaphore can be
// released and flush/commit/prune can proceed without blocking the
// next FCU.
e.logger.Debug("[updateForkChoice] dispatching notifications", "head", blockHash, "bgCommit", e.fcuBackgroundCommit)
if err := e.dispatchNotificationsFromOverlay(currentContext, finishProgressBefore); err != nil {
+ // The generation is already registered; enqueue it so its commit
+ // still lands and the chain can drain — otherwise an uncommitted
+ // generation would block drainCommittedGens forever.
+ if bgGen != nil {
+ roTx = nil
+ currentContext = nil
+ e.enqueueCommit(bgGen)
+ }
return sendForkchoiceErrorWithoutWaiting(e.logger, outcomeCh, fmt.Errorf("fcu: dispatch notifications: %w", err), stateFlushingInParallel)
}
- // Hand the semaphore to a background goroutine: FCU cleanup runs first,
- // so the next acquirer observes settled state whenever it gets in.
- handOffSemaphore := func(work func() error) {
- shouldReleaseSema = false
- cleanupBeforeSemaRelease()
- go func() {
- defer e.semaphore.Release(1)
- if err := work(); err != nil && !errors.Is(err, context.Canceled) {
- e.logger.Error("Error running background post forkchoice", "err", err)
- }
- }()
- }
-
// Flush + commit: foreground by default, background only if
- // fcuBackgroundCommit is explicitly enabled.
+ // fcuBackgroundCommit is explicitly enabled. No semaphore handoff —
+ // fgRelease (the outer defer) drops the foreground lock the moment
+ // updateForkChoice returns, so the next FCU never blocks on this
+ // commit.
var commitTimings []any
if e.fcuBackgroundCommit {
- // Transfer roTx + SD ownership to the goroutine so the outer
- // defers become no-ops.
- bgRoTx, bgSD := roTx, currentContext
- roTx, currentContext = nil, nil
- dispatcher := e.pipelineExecutor.Dispatcher()
- handOffSemaphore(func() error {
- defer bgSD.Close()
- // bgRoTx is rolled back inside runForkchoiceFlushCommit between
- // Flush and Commit so the commit sees openTxs=1 in MDBX; this
- // defer is redundant (Rollback is idempotent).
- defer bgRoTx.Rollback()
- err := e.runPostForkchoice(bgSD, bgRoTx, finishProgressBefore, isSynced, initialCycle)
- // Signal that the DB commit is done — RPC consumers can
- // drop their SD reference and read from committed DB.
- if dispatcher != nil {
- dispatcher.PublishOverlay(nil)
- }
- return err
- })
+ // Hand the commit to the background worker: it acquires the foreground
+ // semaphore before committing so the commit RwTx never overlaps a
+ // foreground roTx, and the generation is on the chain so the next FCU's
+ // SD reads its not-yet-committed domain state via SetParent instead of
+ // waiting.
+ roTx = nil
+ currentContext = nil
+ e.enqueueCommit(bgGen)
} else {
// Foreground commit: pass the outer roTx so it gets released
- // between Flush and Commit (same openTxs=2→1 optimization as
- // the bg-commit path).
+ // between Flush and Commit (openTxs=2→1 optimization).
ct, err := e.runForkchoiceFlushCommit(currentContext, roTx, finishProgressBefore, isSynced)
if err != nil {
return sendForkchoiceErrorWithoutWaiting(e.logger, outcomeCh, err, stateFlushingInParallel)
}
commitTimings = ct
- // Prune: background by default (fcuBackgroundPrune=true). RunPrune
- // shares the pipeline Sync with the next FCU's RunLoop, so the
- // goroutine keeps the semaphore until done.
+ // Prune: background by default (fcuBackgroundPrune=true). The prune
+ // goroutine takes over the foreground slot (semaphore + fg count) and
+ // releases it when done, so the next FCU stays Busy — RunPrune shares
+ // the pipeline Sync with the next FCU's RunLoop and opens its own RwTx,
+ // which must not overlap either.
if e.fcuBackgroundPrune {
- // Prune doesn't use the overlay/SD — tear down eagerly to free
- // the RAM now and keep the outer defer out of the next FCU's way.
- teardownOverlay()
- handOffSemaphore(func() error {
- return e.runPostForkchoice(nil, nil, finishProgressBefore, isSynced, initialCycle)
- })
+ // Settle pending updates + unwind state before handing the
+ // semaphore to the prune goroutine: a fast prune could otherwise
+ // fgRelease before the deferred cleanup runs, letting the next
+ // acquirer observe unsettled state. OnceFunc, so the outer defer
+ // is then a no-op.
+ cleanupBeforeSemaRelease()
+ pruneHandedOff = true
+ go func() {
+ defer e.fgRelease()
+ if _, err := e.runForkchoicePrune(initialCycle); err != nil && !errors.Is(err, context.Canceled) {
+ e.logger.Error("Error running background prune", "err", err)
+ }
+ }()
} else {
pruneTimings, err := e.runForkchoicePrune(initialCycle)
if err != nil {
diff --git a/execution/execmodule/getter_mock_test.go b/execution/execmodule/getter_mock_test.go
new file mode 100644
index 00000000000..24705edd573
--- /dev/null
+++ b/execution/execmodule/getter_mock_test.go
@@ -0,0 +1,42 @@
+// 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 execmodule
+
+import (
+ "github.com/erigontech/erigon/common/log/v3"
+ "github.com/erigontech/erigon/db/dbservices"
+ "github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/execution/bal"
+ "github.com/erigontech/erigon/execution/chain"
+ "github.com/erigontech/erigon/execution/protocol/rules"
+)
+
+// NewGetterMockForTest builds a synchronous ExecModule that serves the
+// payload-bodies getters straight from the raw DB. It publishes no
+// SharedDomains, so beginOverlayOrRo reads the committed database directly, and
+// starts no background-commit worker — tests set DB state themselves and read
+// it back deterministically. Exported from a _test.go file: usable by the
+// execmodule_test binary, never compiled into a production build.
+func NewGetterMockForTest(db kv.TemporalRwDB, blockReader dbservices.FullBlockReader, engine rules.Engine, config *chain.Config, logger log.Logger) *ExecModule {
+ return &ExecModule{
+ db: db,
+ blockReader: blockReader,
+ balRegenerator: bal.NewRegenerator(blockReader, engine, logger),
+ config: config,
+ logger: logger,
+ }
+}
diff --git a/execution/execmodule/getters.go b/execution/execmodule/getters.go
index 17d84cca46a..f696c0fb9a0 100644
--- a/execution/execmodule/getters.go
+++ b/execution/execmodule/getters.go
@@ -66,17 +66,32 @@ func (e *ExecModule) beginOverlayOrRo(ctx context.Context) (kv.TemporalTx, func(
}
if sd != nil {
if overlay := sd.BlockOverlay(); overlay != nil {
- // Open a fresh RO tx while still holding the read lock so that
- // the overlay cannot be closed between our check and the
- // NewReadView call (TOCTOU avoidance).
+ // Open a fresh RO tx while still holding the read lock so the
+ // generation chain cannot change between the check and building the
+ // view (TOCTOU avoidance). Route through OverlayTemporalTx so
+ // the view chains every ancestor generation's block overlay, not just
+ // the leaf — otherwise reads miss uncommitted block data from earlier
+ // FCUs.
roTx, err := e.db.BeginTemporalRo(ctx) //nolint:gocritic
if err != nil {
e.lock.RUnlock()
return nil, nil, err
}
- view := overlay.NewReadView(roTx)
+ view := sd.OverlayTemporalTx(roTx)
e.lock.RUnlock()
- return view, func() { roTx.Rollback() }, nil
+ if view != nil {
+ return view, func() { roTx.Rollback() }, nil
+ }
+ // The overlay was flushed to the DB (CloseBlockOverlay, e.g. a bulk
+ // InsertBlocks) between the check and the re-load, leaving no overlay
+ // in the chain. The flushed blocks are now committed, so a fresh roTx
+ // sees them; the one we opened predates the flush, so drop it.
+ roTx.Rollback()
+ tx, err := e.db.BeginTemporalRo(ctx) //nolint:gocritic
+ if err != nil {
+ return nil, nil, err
+ }
+ return tx, func() { tx.Rollback() }, nil
}
}
e.lock.RUnlock()
diff --git a/execution/execmodule/inserters.go b/execution/execmodule/inserters.go
index e2555cac3b4..5b67892208b 100644
--- a/execution/execmodule/inserters.go
+++ b/execution/execmodule/inserters.go
@@ -54,15 +54,14 @@ func (e *ExecModule) flushBlockOverlayToDB(ctx context.Context, sd *execctx.Shar
}
func (e *ExecModule) InsertBlocks(ctx context.Context, blocks []*types.RawBlock) (ExecutionStatus, error) {
- // Serialize behind any in-flight exec-module op, blocking rather than failing fast with Busy.
- start := time.Now()
- // Timed across the whole call so the metric includes semaphore wait.
- defer insertBlocksDuration.ObserveDuration(start)
- if err := e.semaphore.Acquire(ctx, 1); err != nil {
- return 0, fmt.Errorf("ethereumExecutionModule.InsertBlocks: semaphore acquire: %w", err)
+ defer insertBlocksDuration.ObserveDuration(time.Now())
+ // Serialize behind any in-flight exec-module op, blocking rather than
+ // failing fast with Busy (callers and tests rely on the block), while
+ // staying in the foreground-worker model (enterForeground for the worker).
+ if err := e.fgAcquire(ctx); err != nil {
+ return 0, fmt.Errorf("ethereumExecutionModule.InsertBlocks: fg acquire: %w", err)
}
- defer e.semaphore.Release(1)
- e.logger.Debug("ethereumExecutionModule.InsertBlocks: semaphore acquired", "wait", time.Since(start))
+ defer e.fgRelease()
e.forkValidator.ClearWithUnwind()
frozenBlocks := e.blockReader.FrozenBlocks()
@@ -85,12 +84,21 @@ func (e *ExecModule) InsertBlocks(ctx context.Context, blocks []*types.RawBlock)
}
e.logger.Info("ethereumExecutionModule.InsertBlocks: state ahead of blocks, proceeding with catch-up", "err", err)
}
+ // Chain to the newest in-flight commit generation so
+ // the parent-TD read below sees a previous FCU's not-yet-committed
+ // block data instead of a stale DB.
+ if sd != nil {
+ sd.SetReadCoordinator(e.beginCoordinatedRo)
+ if parent := e.latestGen(); parent != nil {
+ sd.SetParent(parent)
+ }
+ }
e.lock.Lock()
e.currentContext = sd
e.lock.Unlock()
}
if sd.BlockOverlay() == nil {
- if err := sd.InitBlockOverlay(roTx, roTx.Debug().Dirs().Tmp); err != nil {
+ if err := sd.InitBlockOverlay(e.overlayBaseFor(roTx), roTx.Debug().Dirs().Tmp); err != nil {
return 0, fmt.Errorf("ethereumExecutionModule.InsertBlocks: %w", err)
}
} else {
diff --git a/execution/execmodule/scoped_read.go b/execution/execmodule/scoped_read.go
new file mode 100644
index 00000000000..2a71933c935
--- /dev/null
+++ b/execution/execmodule/scoped_read.go
@@ -0,0 +1,143 @@
+// 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 execmodule
+
+import (
+ "context"
+ "errors"
+ "sync/atomic"
+
+ "github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/db/rawdb"
+ "github.com/erigontech/erigon/db/state/execctx"
+)
+
+// errHeadMismatch means the current head is not the block the caller asked to
+// read/build on — the published head has not (yet) reached that block, or a
+// reorg moved it. The caller signals Busy rather than reading the wrong block.
+var errHeadMismatch = errors.New("head is not at the requested block")
+
+// ScopedReadView is a by-block-consistent read snapshot pinned to a specific
+// block under background commit: a base MDBX roTx captured at a settled instant
+// (while the caller holds the foreground semaphore, so no commit is mid-flight)
+// plus the head SharedDomains whose in-flight mem the snapshot layers on (nil
+// when the committed DB is already current). getLatest reads sd.mem → the head
+// chain → the roTx, and mem shadows the DB, so the two are one coherent view of
+// the block regardless of whether its commit has landed.
+//
+// Single-owner: exactly one consumer holds it and MUST call Release exactly
+// once. Release is idempotent; failing to call it leaks an open MDBX reader and
+// wedges DB close.
+type ScopedReadView struct {
+ roTx kv.TemporalTx
+ headSD *execctx.SharedDomains
+ blockHash common.Hash
+ blockNum uint64
+ released atomic.Bool
+}
+
+// Tx returns the pinned base roTx. Readers pass it to sd.AsGetter /
+// BlockOverlay().NewReadView / OverlayTemporalTx.
+func (v *ScopedReadView) Tx() kv.TemporalTx { return v.roTx }
+
+// HeadSD returns the head SharedDomains this view is pinned to, or nil when the
+// committed DB is already current (all generations committed).
+func (v *ScopedReadView) HeadSD() *execctx.SharedDomains { return v.headSD }
+
+// BlockHash returns the block this view reads.
+func (v *ScopedReadView) BlockHash() common.Hash { return v.blockHash }
+
+// BlockNum returns the number of the block this view reads.
+func (v *ScopedReadView) BlockNum() uint64 { return v.blockNum }
+
+// Release rolls back the pinned roTx. Idempotent; safe on nil.
+func (v *ScopedReadView) Release() {
+ if v == nil || v.released.Swap(true) {
+ return
+ }
+ if v.roTx != nil {
+ v.roTx.Rollback()
+ }
+}
+
+// captureScopedReadView pins a consistent read snapshot for wantHash. The caller
+// MUST hold the foreground semaphore (mutually exclusive with the background
+// commit worker) so the snapshot is settled. The head is the newest
+// not-yet-committed generation's SharedDomains when its block is wantHash, else
+// the committed DB. Returns errHeadMismatch when the head is not wantHash so the
+// caller can return Busy instead of reading the wrong block. The returned view
+// owns the roTx — the caller must Release it.
+func (e *ExecModule) captureScopedReadView(ctx context.Context, wantHash common.Hash) (*ScopedReadView, error) {
+ // Capture the head generation and the base roTx atomically under fgMu, so the
+ // roTx's committed snapshot is coordinated with the captured generation set:
+ // markGenCommitted runs under fgMu strictly after the DB commit, so a datum is
+ // either in the captured head's mem chain or already visible in this roTx —
+ // never in neither. Binding to the background context (not the request ctx)
+ // keeps the roTx valid for the async build, which outlives this call.
+ e.fgMu.Lock()
+ var head *execctx.SharedDomains
+ var headHash common.Hash
+ var headNum uint64
+ if n := len(e.gens); n > 0 {
+ // Use the newest generation's retained SD as the head regardless of
+ // commit status: its mem is kept after commit (drainCommittedGens no-op)
+ // and it is the same SD the txpool reads via the published SD, so the
+ // builder and the pool process the identical block state. Reads resolve
+ // mem-first over the coordinated roTx, so a committed generation's mem
+ // simply shadows the (equal) committed DB.
+ last := e.gens[n-1]
+ head, headHash, headNum = last.sd, last.blockHash, last.blockNum
+ }
+ roTx, err := e.db.BeginTemporalRo(e.bacgroundCtx) //nolint:gocritic // handed to the returned view, which owns Rollback; guard below covers all error/panic paths before handoff
+ e.fgMu.Unlock()
+ if err != nil {
+ return nil, err
+ }
+ handedOff := false
+ defer func() {
+ if !handedOff {
+ roTx.Rollback()
+ }
+ }()
+
+ if head != nil {
+ if headHash != wantHash {
+ return nil, errHeadMismatch
+ }
+ handedOff = true
+ return &ScopedReadView{roTx: roTx, headSD: head, blockHash: wantHash, blockNum: headNum}, nil
+ }
+
+ // No in-flight generation: the committed DB is the head. Require wantHash to
+ // BE the current head, not merely canonical at its own height — an older
+ // canonical ancestor is canonical but is not the tip, and building on it
+ // would only fail the async builder's parent check later. Signal Busy so the
+ // caller retries against the real head instead.
+ if headHash := rawdb.ReadHeadBlockHash(roTx); headHash != wantHash {
+ return nil, errHeadMismatch
+ }
+ num, err := e.blockReader.HeaderNumber(ctx, roTx, wantHash)
+ if err != nil {
+ return nil, err
+ }
+ if num == nil {
+ return nil, errHeadMismatch
+ }
+ handedOff = true
+ return &ScopedReadView{roTx: roTx, blockHash: wantHash, blockNum: *num}, nil
+}
diff --git a/execution/execmodule/set_head.go b/execution/execmodule/set_head.go
index 9a9c11c2004..620ace9fdf0 100644
--- a/execution/execmodule/set_head.go
+++ b/execution/execmodule/set_head.go
@@ -53,10 +53,10 @@ func getLatestBlockNumber(tx kv.Tx) (uint64, error) {
func (e *ExecModule) SetHead(ctx context.Context, targetBlock uint64) error {
acquireCtx, acquireCancel := context.WithTimeout(ctx, 5*time.Second)
defer acquireCancel()
- if err := e.semaphore.Acquire(acquireCtx, 1); err != nil {
+ if err := e.fgAcquire(acquireCtx); err != nil {
return fmt.Errorf("execution module is busy: %w", err)
}
- defer e.semaphore.Release(1)
+ defer e.fgRelease()
tx, err := e.db.BeginTemporalRw(ctx)
if err != nil {
@@ -163,6 +163,16 @@ func (e *ExecModule) SetHead(ctx context.Context, targetBlock uint64) error {
return fmt.Errorf("failed to commit shared domains: %w", err)
}
+ // The direct unwind above superseded any in-flight background-commit
+ // generations and the current context (they describe the unwound-away tip),
+ // so drop them and clear the published overlay. Readers and the next FCU
+ // then rebuild from the raw DB this unwind just committed.
+ e.closeAllGens()
+ e.closeModuleContext()
+ if dispatcher := e.pipelineExecutor.Dispatcher(); dispatcher != nil {
+ dispatcher.PublishOverlay(nil)
+ }
+
e.logger.Info("SetHead: successfully rewound chain", "targetBlock", targetBlock, "previousHead", currentHead)
return nil
}
diff --git a/execution/stagedsync/calc_state.go b/execution/stagedsync/calc_state.go
index e81ccf374f1..ade693dce65 100644
--- a/execution/stagedsync/calc_state.go
+++ b/execution/stagedsync/calc_state.go
@@ -6,6 +6,7 @@ import (
"github.com/holiman/uint256"
+ "github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/common/empty"
"github.com/erigontech/erigon/common/log/v3"
"github.com/erigontech/erigon/db/kv"
@@ -364,6 +365,54 @@ func (cs *calcState) FlushToUpdates(updates *commitment.Updates) {
}
}
+// postValue returns the account/storage value AFTER this block's changes, in the
+// serialized form the domain would store, for a key the block modified. ok is
+// false for keys the block did not change (the caller should read the baseline).
+// It gives the fold's commitment reader a post-block-consistent view: the fold
+// runs ahead of execution, so a plain as-of read would return the pre-block value,
+// which yields a wrong root when a cleared slot forces a storage-subtree collapse.
+func (cs *calcState) postValue(d kv.Domain, plainKey []byte) (enc []byte, ok bool) {
+ switch d {
+ case kv.AccountsDomain:
+ if len(plainKey) != 20 {
+ return nil, false
+ }
+ acc, exists := cs.accounts[accounts.InternAddress(common.BytesToAddress(plainKey))]
+ if !exists || !acc.dirty {
+ return nil, false
+ }
+ if acc.Deleted && acc.Balance.IsZero() && acc.Nonce == 0 && acc.CodeHash == empty.CodeHash {
+ return nil, true // removed → absent
+ }
+ a := accounts.NewAccount()
+ a.Balance = acc.Balance
+ a.Nonce = acc.Nonce
+ a.Incarnation = acc.Incarnation
+ a.CodeHash = accounts.InternCodeHash(common.Hash(acc.CodeHash))
+ return accounts.SerialiseV3(&a), true
+ case kv.StorageDomain:
+ if len(plainKey) != 20+32 {
+ return nil, false
+ }
+ addr := accounts.InternAddress(common.BytesToAddress(plainKey[:20]))
+ dirty := cs.storageDirty[addr]
+ if dirty == nil {
+ return nil, false
+ }
+ slot := accounts.InternKey(common.BytesToHash(plainKey[20:]))
+ if !dirty[slot] {
+ return nil, false
+ }
+ v := cs.storageState[addr][slot]
+ vb := v.Bytes()
+ if len(vb) == 0 {
+ return nil, true // cleared → absent
+ }
+ return vb, true
+ }
+ return nil, false
+}
+
// ResetBlockFlags clears the per-block dirty flags while keeping the
// accumulated state values. Called after commitment computation to
// prepare for the next block.
diff --git a/execution/stagedsync/changeset_builder.go b/execution/stagedsync/changeset_builder.go
new file mode 100644
index 00000000000..6b6813dd737
--- /dev/null
+++ b/execution/stagedsync/changeset_builder.go
@@ -0,0 +1,91 @@
+package stagedsync
+
+import (
+ "bytes"
+
+ "github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/db/state/changeset"
+)
+
+// prevValueReader supplies the pre-block (first-touch) value of a domain key as
+// raw stored bytes, or nil when absent. Only the first write to a key in a block
+// consults it; later writes chain their prev in memory.
+type prevValueReader interface {
+ prevValue(domain kv.Domain, key []byte, txNum uint64) ([]byte, error)
+}
+
+// changesetBuilder reconstructs a block's per-domain changeset (the ChangeSets3
+// entries) from the per-tx write stream, mirroring the domain-write recording so
+// the bytes are identical to exec's: prev values chain in memory (first touch
+// read as-of pre-block via the reader), no-op puts are skipped, and entries are
+// keyed by step = txNum/stepSize. It is a generic encoder — domain-specific
+// choices (code-delete-when-absent, account self-destruct cascade) are applied by
+// the caller before record; the builder just recreates the DomainUpdate stream.
+type changesetBuilder struct {
+ reader prevValueReader
+ stepSize uint64
+ cs *changeset.StateChangeSet
+ running [kv.DomainLen]map[string][]byte
+ firstErr error
+}
+
+func newChangesetBuilder(reader prevValueReader, stepSize uint64) *changesetBuilder {
+ b := &changesetBuilder{
+ reader: reader,
+ stepSize: stepSize,
+ cs: &changeset.StateChangeSet{},
+ }
+ for i := range b.running {
+ b.running[i] = map[string][]byte{}
+ }
+ return b
+}
+
+// record folds one domain write into the changeset. newVal == nil is a delete;
+// a non-nil newVal is a put (skipped when it equals the current value, exactly as
+// SharedDomains.DomainPut does). txNum is the write's tx position; the entry is
+// keyed by step = txNum/stepSize so a block straddling a step edge yields one
+// entry per step for the same key.
+func (b *changesetBuilder) record(domain kv.Domain, key []byte, newVal []byte, txNum uint64) {
+ ks := string(key)
+ running := b.running[domain]
+ prev, seen := running[ks]
+ if !seen {
+ p, err := b.reader.prevValue(domain, key, txNum)
+ if err != nil {
+ if b.firstErr == nil {
+ b.firstErr = err
+ }
+ return
+ }
+ prev = p
+ }
+
+ // Put whose value already matches the current value is a no-op: no history
+ // entry, and the running value is unchanged.
+ if newVal != nil && bytes.Equal(prev, newVal) {
+ running[ks] = prev
+ return
+ }
+
+ // A code delete with no prior value is a no-op (nothing to restore): mirrors
+ // SharedDomains.DomainDel, which returns early for CodeDomain when prevVal is
+ // nil. Other domains record the delete even against an absent prev.
+ if newVal == nil && domain == kv.CodeDomain && len(prev) == 0 {
+ running[ks] = nil
+ return
+ }
+
+ step := kv.Step(txNum / b.stepSize)
+ b.cs.Diffs[domain].DomainUpdate(key, step, prev)
+
+ if newVal == nil {
+ running[ks] = nil
+ } else {
+ running[ks] = bytes.Clone(newVal)
+ }
+}
+
+func (b *changesetBuilder) err() error { return b.firstErr }
+
+func (b *changesetBuilder) result() *changeset.StateChangeSet { return b.cs }
diff --git a/execution/stagedsync/changeset_builder_test.go b/execution/stagedsync/changeset_builder_test.go
new file mode 100644
index 00000000000..91e13da2a8b
--- /dev/null
+++ b/execution/stagedsync/changeset_builder_test.go
@@ -0,0 +1,176 @@
+package stagedsync
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/erigontech/erigon/db/kv"
+)
+
+// fakePrevReader serves pre-block (first-touch) values for the builder, keyed by
+// domain+key. Absent keys return nil (new key). It records how many times each
+// key was read so a test can assert the builder only reads pre-block state once
+// per key (subsequent prevs chain in-memory).
+type fakePrevReader struct {
+ vals map[kv.Domain]map[string][]byte
+ reads map[kv.Domain]map[string]int
+}
+
+func newFakePrevReader() *fakePrevReader {
+ return &fakePrevReader{
+ vals: map[kv.Domain]map[string][]byte{},
+ reads: map[kv.Domain]map[string]int{},
+ }
+}
+
+func (r *fakePrevReader) set(d kv.Domain, key string, v []byte) {
+ if r.vals[d] == nil {
+ r.vals[d] = map[string][]byte{}
+ }
+ r.vals[d][key] = v
+}
+
+func (r *fakePrevReader) prevValue(d kv.Domain, key []byte, _ uint64) ([]byte, error) {
+ ks := string(key)
+ if r.reads[d] == nil {
+ r.reads[d] = map[string]int{}
+ }
+ r.reads[d][ks]++
+ return r.vals[d][ks], nil
+}
+
+// diffEntry is a decoded (key-without-step, step, prevValue) view of one
+// DomainEntryDiff, for readable assertions independent of the ^step encoding.
+type diffEntry struct {
+ key string
+ step kv.Step
+ prev string // "" means an explicit empty ([]byte{}) restore-marker (new key)
+}
+
+func decodeDiffs(t *testing.T, diffs []kv.DomainEntryDiff) []diffEntry {
+ t.Helper()
+ out := make([]diffEntry, 0, len(diffs))
+ for _, d := range diffs {
+ require.GreaterOrEqual(t, len(d.Key), 8, "diff key must carry an 8-byte step suffix")
+ raw := []byte(d.Key)
+ body := raw[:len(raw)-8]
+ var inv uint64
+ for _, b := range raw[len(raw)-8:] {
+ inv = inv<<8 | uint64(b)
+ }
+ out = append(out, diffEntry{key: string(body), step: kv.Step(^inv), prev: string(d.Value)})
+ }
+ return out
+}
+
+const testStepSize = 4
+
+func TestChangesetBuilder_NewKeyRecordsEmptyPrev(t *testing.T) {
+ r := newFakePrevReader()
+ b := newChangesetBuilder(r, testStepSize)
+
+ b.record(kv.AccountsDomain, []byte("acct"), []byte("v1"), 0)
+ require.NoError(t, b.err())
+
+ got := decodeDiffs(t, b.result().Diffs[kv.AccountsDomain].GetDiffSet())
+ require.Equal(t, []diffEntry{{key: "acct", step: 0, prev: ""}}, got)
+}
+
+func TestChangesetBuilder_FirstTouchKeepsPreBlockPrev(t *testing.T) {
+ r := newFakePrevReader()
+ r.set(kv.AccountsDomain, "acct", []byte("pre"))
+ b := newChangesetBuilder(r, testStepSize)
+
+ b.record(kv.AccountsDomain, []byte("acct"), []byte("v1"), 0)
+ got := decodeDiffs(t, b.result().Diffs[kv.AccountsDomain].GetDiffSet())
+ require.Equal(t, []diffEntry{{key: "acct", step: 0, prev: "pre"}}, got)
+}
+
+func TestChangesetBuilder_OverwriteSameStepKeepsFirstPrev(t *testing.T) {
+ r := newFakePrevReader()
+ r.set(kv.AccountsDomain, "acct", []byte("pre"))
+ b := newChangesetBuilder(r, testStepSize)
+
+ // Two writes in the same step: only the first-per-step entry survives, and
+ // it carries the pre-block prev.
+ b.record(kv.AccountsDomain, []byte("acct"), []byte("v1"), 0)
+ b.record(kv.AccountsDomain, []byte("acct"), []byte("v2"), 1)
+ got := decodeDiffs(t, b.result().Diffs[kv.AccountsDomain].GetDiffSet())
+ require.Equal(t, []diffEntry{{key: "acct", step: 0, prev: "pre"}}, got)
+ require.Equal(t, 1, r.reads[kv.AccountsDomain]["acct"], "pre-block read must happen once; later prevs chain in memory")
+}
+
+func TestChangesetBuilder_ABAStillRecordsEntry(t *testing.T) {
+ r := newFakePrevReader()
+ r.set(kv.AccountsDomain, "acct", []byte("A"))
+ b := newChangesetBuilder(r, testStepSize)
+
+ // A -> B -> A within one step: exec's replay records the first non-noop
+ // (prev=A) and dedups the rest; the net-zero round-trip still yields an
+ // entry restoring A.
+ b.record(kv.AccountsDomain, []byte("acct"), []byte("B"), 0)
+ b.record(kv.AccountsDomain, []byte("acct"), []byte("A"), 1)
+ got := decodeDiffs(t, b.result().Diffs[kv.AccountsDomain].GetDiffSet())
+ require.Equal(t, []diffEntry{{key: "acct", step: 0, prev: "A"}}, got)
+}
+
+func TestChangesetBuilder_NoOpWriteRecordsNothing(t *testing.T) {
+ r := newFakePrevReader()
+ r.set(kv.StorageDomain, "slot", []byte("X"))
+ b := newChangesetBuilder(r, testStepSize)
+
+ // Writing the same value the slot already holds is a no-op (mirrors
+ // DomainPut's bytes.Equal(prev,v) skip): no diff entry.
+ b.record(kv.StorageDomain, []byte("slot"), []byte("X"), 0)
+ require.Empty(t, b.result().Diffs[kv.StorageDomain].GetDiffSet())
+}
+
+func TestChangesetBuilder_StraddleRecordsPerStepEntries(t *testing.T) {
+ r := newFakePrevReader()
+ r.set(kv.AccountsDomain, "acct", []byte("A"))
+ b := newChangesetBuilder(r, testStepSize)
+
+ // Block straddles a step edge (stepSize=4): txNum 3 is step 0, txNum 4 is
+ // step 1. A key written in both steps yields two entries — step 0 with the
+ // pre-block prev, step 1 with the intermediate value written in step 0.
+ b.record(kv.AccountsDomain, []byte("acct"), []byte("B"), 3)
+ b.record(kv.AccountsDomain, []byte("acct"), []byte("C"), 4)
+ got := decodeDiffs(t, b.result().Diffs[kv.AccountsDomain].GetDiffSet())
+ require.ElementsMatch(t, []diffEntry{
+ {key: "acct", step: 0, prev: "A"},
+ {key: "acct", step: 1, prev: "B"},
+ }, got)
+}
+
+func TestChangesetBuilder_CodeDeleteWhenAbsentRecordsNothing(t *testing.T) {
+ r := newFakePrevReader() // no prior code for the key
+ b := newChangesetBuilder(r, testStepSize)
+
+ // A code delete with no prior value is a no-op — the system-address code
+ // "write" of empty bytes during an EIP-4788/2935 system call. Mirrors
+ // SharedDomains.DomainDel's CodeDomain skip.
+ b.record(kv.CodeDomain, []byte("sysaddr"), nil, 0)
+ require.Empty(t, b.result().Diffs[kv.CodeDomain].GetDiffSet())
+}
+
+func TestChangesetBuilder_StorageDeleteWhenAbsentStillRecords(t *testing.T) {
+ r := newFakePrevReader() // no prior slot value
+ b := newChangesetBuilder(r, testStepSize)
+
+ // Storage deletes are NOT skipped when absent (only CodeDomain is).
+ b.record(kv.StorageDomain, []byte("slot"), nil, 0)
+ got := decodeDiffs(t, b.result().Diffs[kv.StorageDomain].GetDiffSet())
+ require.Equal(t, []diffEntry{{key: "slot", step: 0, prev: ""}}, got)
+}
+
+func TestChangesetBuilder_DeleteRecordsPrev(t *testing.T) {
+ r := newFakePrevReader()
+ r.set(kv.StorageDomain, "slot", []byte("V"))
+ b := newChangesetBuilder(r, testStepSize)
+
+ // A delete (empty new value) restores the prior value on unwind.
+ b.record(kv.StorageDomain, []byte("slot"), nil, 0)
+ got := decodeDiffs(t, b.result().Diffs[kv.StorageDomain].GetDiffSet())
+ require.Equal(t, []diffEntry{{key: "slot", step: 0, prev: "V"}}, got)
+}
diff --git a/execution/stagedsync/changeset_reconstruct.go b/execution/stagedsync/changeset_reconstruct.go
new file mode 100644
index 00000000000..4c70277688e
--- /dev/null
+++ b/execution/stagedsync/changeset_reconstruct.go
@@ -0,0 +1,190 @@
+package stagedsync
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/common/empty"
+ "github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/db/state/changeset"
+ "github.com/erigontech/erigon/db/state/execctx"
+ "github.com/erigontech/erigon/execution/state"
+ "github.com/erigontech/erigon/execution/types/accounts"
+)
+
+// csWrite is one buffered per-tx domain write (new value; nil = delete) captured
+// at feed time; prevs are resolved later, at settled compute time.
+type csWrite struct {
+ domain kv.Domain
+ key []byte
+ val []byte
+ txNum uint64
+}
+
+// bufferChangesetWrites buffers an owned block's per-tx writes (new values captured
+// now, from the post-tx state) so the changeset can be replayed and its prevs
+// resolved at settled compute time — matching exec's block-end flush timing rather
+// than the racy per-tx moment. Non-owned blocks need no changeset.
+func (cc *commitmentCalculator) bufferChangesetWrites(r *txResult) error {
+ if !cc.ownsChangeset(r.blockNum) {
+ return nil
+ }
+ if cc.csBuilderBlock != r.blockNum {
+ cc.csPending = cc.csPending[:0]
+ cc.csBuilderBlock = r.blockNum
+ }
+ return cc.feedChangeset(r.writes, r.txNum)
+}
+
+// asOfPrevReader supplies the pre-block value of an account/storage/code key as
+// raw stored bytes via GetAsOf(txNum) — the same bytes exec's DomainPut resolves
+// as prevVal through GetLatest at flush time. Only the first write to a key in a
+// block consults it; the builder chains later prevs in memory.
+type asOfPrevReader struct {
+ sd *execctx.SharedDomains
+ roTx kv.TemporalTx
+}
+
+func (r *asOfPrevReader) prevValue(domain kv.Domain, key []byte, txNum uint64) ([]byte, error) {
+ enc, ok, err := r.sd.GetAsOf(domain, key, txNum)
+ if err != nil {
+ return nil, err
+ }
+ if ok {
+ return enc, nil
+ }
+ enc, ok, err = r.roTx.GetAsOf(domain, key, txNum)
+ if err != nil {
+ return nil, err
+ }
+ if !ok {
+ return nil, nil
+ }
+ return enc, nil
+}
+
+func (cc *commitmentCalculator) appendCS(domain kv.Domain, key, val []byte, txNum uint64) {
+ w := csWrite{domain: domain, key: bytes.Clone(key), txNum: txNum}
+ if val != nil {
+ w.val = bytes.Clone(val)
+ }
+ cc.csPending = append(cc.csPending, w)
+}
+
+// feedChangeset buffers one tx's account/storage/code writes, mirroring exec's
+// per-tx BlockStateCache.Flush op emission: the account is serialized once per tx
+// from the calc's post-tx state (a Deleted+all-zero account is a delete),
+// storage/code puts carry their new bytes (empty → delete), and a self-destruct
+// cascades a storage-subtree wipe. Prevs are resolved later (at settled compute
+// time) so the replayed ChangeSets3 bytes match exec's.
+func (cc *commitmentCalculator) feedChangeset(writes *state.WriteSet, txNum uint64) error {
+ touched := map[accounts.Address]struct{}{}
+ for addr := range writes.Balances() {
+ touched[addr] = struct{}{}
+ }
+ for addr := range writes.Nonces() {
+ touched[addr] = struct{}{}
+ }
+ for addr := range writes.CodeHashes() {
+ touched[addr] = struct{}{}
+ }
+ for addr := range writes.Codes() {
+ touched[addr] = struct{}{}
+ }
+ for addr := range writes.Incarnations() {
+ touched[addr] = struct{}{}
+ }
+ for addr, vw := range writes.SelfDestructs() {
+ if vw.Val {
+ touched[addr] = struct{}{}
+ }
+ }
+
+ for addr := range touched {
+ addrVal := addr.Value()
+ acc := cc.state.accounts[addr]
+ if acc == nil {
+ continue
+ }
+ if acc.Deleted && acc.Balance.IsZero() && acc.Nonce == 0 && acc.CodeHash == empty.CodeHash {
+ cc.appendCS(kv.AccountsDomain, addrVal[:], nil, txNum)
+ } else {
+ a := accounts.NewAccount()
+ a.Balance = acc.Balance
+ a.Nonce = acc.Nonce
+ a.Incarnation = acc.Incarnation
+ a.CodeHash = accounts.InternCodeHash(common.Hash(acc.CodeHash))
+ cc.appendCS(kv.AccountsDomain, addrVal[:], accounts.SerialiseV3(&a), txNum)
+ }
+ }
+
+ for addr, vw := range writes.Codes() {
+ addrVal := addr.Value()
+ if vw.Val.Len() == 0 {
+ cc.appendCS(kv.CodeDomain, addrVal[:], nil, txNum)
+ } else {
+ cc.appendCS(kv.CodeDomain, addrVal[:], vw.Val.Bytes, txNum)
+ }
+ }
+
+ for addr, inner := range writes.Storages() {
+ addrVal := addr.Value()
+ for key, vw := range inner {
+ keyVal := key.Value()
+ composite := make([]byte, 20+32)
+ copy(composite, addrVal[:])
+ copy(composite[20:], keyVal[:])
+ vb := vw.Val.Bytes()
+ if len(vb) == 0 {
+ cc.appendCS(kv.StorageDomain, composite, nil, txNum)
+ } else {
+ cc.appendCS(kv.StorageDomain, composite, vb, txNum)
+ }
+ }
+ }
+
+ // Self-destruct wipes the whole storage subtree (exec's DomainDelPrefix) and
+ // deletes the code — reproduce both as deletes over the as-of subtree.
+ for addr, vw := range writes.SelfDestructs() {
+ if !vw.Val {
+ continue
+ }
+ addrVal := addr.Value()
+ cc.appendCS(kv.CodeDomain, addrVal[:], nil, txNum)
+ if cc.state.storageEnum == nil {
+ continue
+ }
+ if err := cc.state.storageEnum.EachStorageSlot(addr, func(key accounts.StorageKey) error {
+ keyVal := key.Value()
+ composite := make([]byte, 20+32)
+ copy(composite, addrVal[:])
+ copy(composite[20:], keyVal[:])
+ cc.appendCS(kv.StorageDomain, composite, nil, txNum)
+ return nil
+ }); err != nil {
+ return fmt.Errorf("changeset reconstruct: storage enumeration for %x: %w", addr, err)
+ }
+ }
+ return nil
+}
+
+// buildResultLocalChangeset replays the buffered per-tx writes through a fresh
+// builder AT COMPUTE TIME — resolving prevs when block N-1 is settled in sd.mem
+// (matching exec's block-end flush) — and returns the account/storage/code diffs
+// (domains 0..2) as a StateChangeSet. The commitment domain (3) is left empty for
+// the caller's compute to fill. Returns (nil, nil) when no writes were buffered
+// for the block; returns (nil, err) on a prev-read failure.
+func (cc *commitmentCalculator) buildResultLocalChangeset(blockNum uint64) (*changeset.StateChangeSet, error) {
+ if cc.csBuilderBlock != blockNum {
+ return nil, nil
+ }
+ b := newChangesetBuilder(cc.prevReader, cc.doms.StepSize())
+ for _, w := range cc.csPending {
+ b.record(w.domain, w.key, w.val, w.txNum)
+ }
+ if err := b.err(); err != nil {
+ return nil, err
+ }
+ return b.result(), nil
+}
diff --git a/execution/stagedsync/committer.go b/execution/stagedsync/committer.go
index 1e6a262054d..7f36d917654 100644
--- a/execution/stagedsync/committer.go
+++ b/execution/stagedsync/committer.go
@@ -14,6 +14,7 @@ import (
"github.com/erigontech/erigon/common/dbg"
"github.com/erigontech/erigon/common/log/v3"
"github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/db/state/changeset"
"github.com/erigontech/erigon/db/state/execctx"
"github.com/erigontech/erigon/execution/chain"
"github.com/erigontech/erigon/execution/commitment"
@@ -91,6 +92,14 @@ type commitmentCalculator struct {
// Values are lazy-loaded from the domain on first touch via asOfReader.
state *calcState
+ // csPending buffers the current owned block's per-tx writes (new values); at
+ // settled compute time they are replayed to reconstruct the account/storage/code
+ // changeset result-locally with prevs resolved. csBuilderBlock is the block
+ // being buffered.
+ csPending []csWrite
+ csBuilderBlock uint64
+ prevReader *asOfPrevReader
+
// asOfReader is shared between calcState (lazy-load) and compute
// methods (fold/unfold sibling reads). Its txNum is updated at each
// block boundary.
@@ -233,6 +242,7 @@ func newCommitmentCalculator(
asOfReader := &asOfStateReader{sd: doms, roTx: roTx, txNum: 0}
return &commitmentCalculator{
+ prevReader: &asOfPrevReader{sd: doms, roTx: roTx},
doms: doms,
db: db,
chainConfig: chainConfig,
@@ -341,6 +351,10 @@ func (cc *commitmentCalculator) handleMessage(ctx context.Context, msg applyResu
if !r.writes.IsEmpty() {
cc.asOfReader.txNum = r.txNum
cc.state.ApplyWrites(r.writes, r.rules.IsAmsterdam)
+ if err := cc.bufferChangesetWrites(r); err != nil {
+ cc.publish(ctx, commitmentResult{blockNum: r.blockNum, txNum: r.txNum, err: err})
+ return
+ }
}
// A computed-ahead block already emitted its interior step checkpoints from
@@ -401,6 +415,12 @@ func (cc *commitmentCalculator) handleMessage(ctx context.Context, msg applyResu
cc.shadowCrossCheck(ctx, r)
} else {
cc.state.ResetBlockFlags()
+ // A folded owned block recorded its root + commitment branch deltas
+ // ahead of the tx-result stream; fill its account/storage/code diffs
+ // now that the block's writes are buffered.
+ if cc.ownsChangeset(r.BlockNum) {
+ cc.finalizeFoldedChangeset(ctx, r)
+ }
}
case cc.perBlockCompute(r.BlockNum):
if cc.lastComputedBlock == 0 && r.isPartial {
@@ -490,18 +510,17 @@ func (cc *commitmentCalculator) computeAheadGateOpen(n uint64) bool {
// with the execution of block n — the parallel-commitment win. Idempotent
// (computedAhead guard).
//
-// Two safety restrictions, both preserving the well-tested incremental path:
-// - Only pre-window blocks (!ownsChangeset) compute ahead. A block that owns
-// a changeset computes incrementally at its own boundary so its per-block
-// branch deltas are captured; computing it ahead would race the exec loop's
-// changeset-accumulator install. Pre-window compute-aheads run under
-// computeIsolated (nil accumulator), so nothing leaks into a later window
-// block's changeset.
-// - Compute ahead only contiguously from the batch's first block: n's
-// baseline is read from the commitment domain, which only a prior
-// compute-ahead (or the prior cycle, for the first block) advanced. A
-// missing-BAL block accumulates without advancing the domain, so computing
-// ahead across it would read a stale trie.
+// Safety restriction preserving the well-tested path: compute ahead only
+// contiguously from the batch's first block. n's baseline is read from the
+// commitment domain, which only a prior compute-ahead (or the prior cycle, for
+// the first block) advanced. A missing-BAL block accumulates without advancing
+// the domain, so computing ahead across it would read a stale trie.
+//
+// Owned (window/tip) blocks compute ahead too: the calculator is the sole
+// changeset producer (result-local, no shared accumulator), so computing ahead
+// no longer races an exec-side accumulator install. An owned block's
+// account/storage/code diffs are filled at the block boundary by
+// finalizeFoldedChangeset once the tx-result stream is complete.
func (cc *commitmentCalculator) maybeComputeAhead(ctx context.Context, n uint64) {
pb, ok := cc.pending[n]
if !ok || pb.mode != calcModeBALDriven || cc.computedAhead[n] {
@@ -514,9 +533,6 @@ func (cc *commitmentCalculator) maybeComputeAhead(ctx context.Context, n uint64)
if sc, stopping := stopCauseOf(cc.signalCtx); stopping && n > sc.block {
return
}
- if cc.ownsChangeset(n) {
- return
- }
if !cc.computeAheadGateOpen(n) {
return
}
@@ -623,6 +639,11 @@ func (cc *commitmentCalculator) computeRootFromBAL(ctx context.Context, req *blo
}
balUpdates := cc.balUpdates
balState.FlushToUpdates(balUpdates)
+ // Overlay the fold's post-block state onto the reader for the commitment
+ // compute (set only now, so balState's own lazy-loads above read the pre-block
+ // baseline). This makes reader-sourced trie reads reflect POST-block state,
+ // matching the incremental path and fixing wrong roots on storage clears.
+ reader.postState = balState
return cc.computeRootFromUpdates(ctx, t, balUpdates, reader)
}
@@ -865,30 +886,13 @@ func (cc *commitmentCalculator) publish(ctx context.Context, r commitmentResult)
}
}
-// computeWithBlockAccumulator runs ComputeCommitment with the changeset
-// accumulator switched to block N's saved changeset (looked up by hash) so
-// that any branch writes during compute (mid-process inline flushes from
-// `pendingPrefixes` collisions, plus the [state] write at end via
-// encodeAndStoreCommitmentState) land in block N's CS rather than whatever
-// the exec loop has installed as current.
-//
-// IMPORTANT: hash-aware lookup is mandatory here. pastChangesAccumulator
-// can hold multiple changesets per block number after a fork-bounce
-// (canonical block 1 + forks[i] block 1 with different hashes), and a
-// number-only GetChangesetByBlockNum returns the first match in
-// non-deterministic map iteration order. That non-determinism caused the
-// calculator's [state] write for canonical block 1 to land in the fork's
-// block 1 CS during the TestBlockchainHeaderchainReorgConsistency
-// reproducer, leaving canonical block 1's CS without [state] and producing
-// off-by-one wrong-trie-root chains on the next iteration's re-execution.
-//
-// If block N's CS hasn't been saved yet it falls through to the live
-// accumulator, which — because the lookup is under changesetMu — is still N's
-// own (the apply loop can't rotate it while the lock is held).
-//
-// Also annotates the pending deferred update (set inside ComputeCommitment
-// when defer mode is on) with the block's hash, so the next call's
-// FlushPendingUpdates uses the same hash-aware routing.
+// computeWithBlockAccumulator computes an owned block's commitment against its
+// own result-local changeset. The block's changeset must be looked up by hash,
+// not number: after a fork-bounce pastChangesAccumulator holds multiple
+// changesets per block number, and a number-only lookup could route this block's
+// branch writes into a sibling fork's changeset — producing wrong-trie-root
+// chains on the next re-execution. It also stamps the pending deferred update
+// with the block hash so the next call's FlushPendingUpdates routes the same way.
func (cc *commitmentCalculator) computeWithBlockAccumulator(ctx context.Context, t commitTarget) ([]byte, error) {
defer func() {
// Stamp the pending update (if any was set during ComputeCommitment)
@@ -900,29 +904,66 @@ func (cc *commitmentCalculator) computeWithBlockAccumulator(ctx context.Context,
}
}()
- // Look up cs AND compute under changesetMu: reading cs before the lock races
- // the apply loop's SavePastChangesetAccumulator + accumulator rotation, which
- // would route this block's [state] write into the next block's changeset. The
- // lock is required even on the cs==nil path — the internal FlushPendingUpdates
- // mutates the same global accumulator pointer.
+ // Reuse the block's saved changeset across its multiple computes so a fold's
+ // commitment branch deltas (Diffs[3], recorded ahead of the tx-result stream)
+ // accumulate with the step-edge and block-end computes instead of being lost;
+ // account/storage/code diffs (0..2) are filled once the buffered writes are
+ // ready (later via finalizeFoldedChangeset for a folded block). The swap and
+ // save run under changesetMu — the swap mutates the global accumulator pointer
+ // that the deferred branch flush and end-of-compute marker also touch,
+ // serializing against the apply goroutine; the *Locked variants avoid
+ // re-acquiring the mutex.
cc.doms.LockChangesetAccumulator()
defer cc.doms.UnlockChangesetAccumulator()
cs := cc.doms.GetChangesetByHash(t.blockNum, t.blockHash)
if cs == nil {
- return cc.doms.ComputeCommitmentLocked(ctx, cc.roTx, true, t.blockNum, t.lastTxNum, cc.logPrefix, nil)
- }
- // LOAD-BEARING swap under the outer lock (already taken above). The
- // swap below mutates the global current-accumulator pointer; the
- // deferred branch writes from block N-1 (flushed inside
- // ComputeCommitmentLocked → FlushPendingUpdatesLocked) AND the [state]
- // marker write at end of compute also touch that same global pointer
- // and the per-domain diff fields. Holding changesetMu through all of
- // it serializes against the apply goroutine's DomainPut/DomainDel.
- //
- // Inside the lock we must use the *Locked variants — the public
- // counterparts re-acquire the same Mutex and would self-deadlock.
+ cs = &changeset.StateChangeSet{}
+ }
+ localAcc, err := cc.buildResultLocalChangeset(t.blockNum)
+ if err != nil {
+ return nil, fmt.Errorf("changeset reconstruct block %d: %w", t.blockNum, err)
+ }
+ if localAcc != nil {
+ cs.Diffs[kv.AccountsDomain] = localAcc.Diffs[kv.AccountsDomain]
+ cs.Diffs[kv.StorageDomain] = localAcc.Diffs[kv.StorageDomain]
+ cs.Diffs[kv.CodeDomain] = localAcc.Diffs[kv.CodeDomain]
+ }
defer cc.doms.SwapChangesetAccumulatorLocked(cs)()
- return cc.doms.ComputeCommitmentLocked(ctx, cc.roTx, true, t.blockNum, t.lastTxNum, cc.logPrefix, nil)
+ rh, err := cc.doms.ComputeCommitmentLocked(ctx, cc.roTx, true, t.blockNum, t.lastTxNum, cc.logPrefix, nil)
+ if err != nil {
+ return nil, err
+ }
+ cc.doms.SavePastChangesetAccumulator(t.blockHash, t.blockNum, cs)
+ return rh, nil
+}
+
+// finalizeFoldedChangeset fills a folded owned block's account/storage/code diffs
+// (0..2) into its saved changeset at the block boundary — when the buffered per-tx
+// writes are complete — preserving the commitment branch deltas (Diffs[3]) the fold
+// already recorded. A fold computes the root (and Diffs[3]) ahead of the tx-result
+// stream, so 0..2 cannot be built at fold time; this closes that gap.
+func (cc *commitmentCalculator) finalizeFoldedChangeset(ctx context.Context, r *blockResult) {
+ localAcc, err := cc.buildResultLocalChangeset(r.BlockNum)
+ if err != nil {
+ // A discarded reader error would save only the fold's commitment diffs
+ // (Diffs[3]), leaving account/storage/code state unrestorable on reorg —
+ // surface it so the apply loop fails the block instead.
+ cc.publish(ctx, commitmentResult{blockNum: r.BlockNum, txNum: r.lastTxNum, err: err})
+ return
+ }
+ if localAcc == nil {
+ return
+ }
+ cc.doms.LockChangesetAccumulator()
+ defer cc.doms.UnlockChangesetAccumulator()
+ cs := cc.doms.GetChangesetByHash(r.BlockNum, r.BlockHash)
+ if cs == nil {
+ cs = &changeset.StateChangeSet{}
+ }
+ cs.Diffs[kv.AccountsDomain] = localAcc.Diffs[kv.AccountsDomain]
+ cs.Diffs[kv.StorageDomain] = localAcc.Diffs[kv.StorageDomain]
+ cs.Diffs[kv.CodeDomain] = localAcc.Diffs[kv.CodeDomain]
+ cc.doms.SavePastChangesetAccumulator(r.BlockHash, r.BlockNum, cs)
}
// asOfStateReader reads account/storage/code at a specific txNum via
@@ -938,6 +979,11 @@ type asOfStateReader struct {
// a concurrent trie-warmup worker doesn't write the shared main accumulator
// (a race) or take the global metrics lock. Nil on the main reader.
workerCtx context.Context
+ // postState, when set, overlays this block's post-values for the account/storage
+ // keys it changed, so the fold's commitment reads reflect POST-block state. The
+ // fold runs ahead of execution, so a plain as-of read returns pre-block state,
+ // which produces a wrong root when a cleared slot forces a storage collapse.
+ postState *calcState
}
func (r *asOfStateReader) WithHistory() bool { return false }
@@ -955,6 +1001,17 @@ func (r *asOfStateReader) Read(d kv.Domain, plainKey []byte, stepSize uint64) (e
enc, step, err = r.sd.GetLatest(d, r.roTx, plainKey)
}
} else {
+ // Post-block overlay: for a key the block changed, return its post-block
+ // value so the fold (running ahead of exec) sees the same state the
+ // incremental path reads after exec's flush.
+ if r.postState != nil {
+ if v, hit := r.postState.postValue(d, plainKey); hit {
+ if stepSize > 0 {
+ step = kv.Step(r.txNum / stepSize)
+ }
+ return v, step, nil
+ }
+ }
// Account/storage/code: use GetAsOf to avoid reading future state.
// Check sd.mem first (in-memory data from current batch), then
// fall through to DB files for data not in the batch.
@@ -981,7 +1038,7 @@ func (r *asOfStateReader) Read(d kv.Domain, plainKey []byte, stepSize uint64) (e
}
func (r *asOfStateReader) Clone(tx kv.TemporalTx) commitmentdb.StateReader {
- return &asOfStateReader{sd: r.sd, roTx: tx, txNum: r.txNum}
+ return &asOfStateReader{sd: r.sd, roTx: tx, txNum: r.txNum, postState: r.postState}
}
// CloneForWorker meters the worker's CommitmentDomain reads into the per-worker
@@ -989,7 +1046,7 @@ func (r *asOfStateReader) Clone(tx kv.TemporalTx) commitmentdb.StateReader {
// reader during block assembly, where trie-warmup runs concurrently — so it
// must not write the shared main accumulator).
func (r *asOfStateReader) CloneForWorker(workerCtx context.Context, tx kv.TemporalTx) commitmentdb.StateReader {
- return &asOfStateReader{sd: r.sd, roTx: tx, txNum: r.txNum, workerCtx: workerCtx}
+ return &asOfStateReader{sd: r.sd, roTx: tx, txNum: r.txNum, workerCtx: workerCtx, postState: r.postState}
}
// Keep imports used.
diff --git a/execution/stagedsync/exec3.go b/execution/stagedsync/exec3.go
index fe9c535408e..a96b4e29d5e 100644
--- a/execution/stagedsync/exec3.go
+++ b/execution/stagedsync/exec3.go
@@ -573,19 +573,25 @@ func (te *txExecutor) executeBlocks(ctx context.Context, startBlockNum uint64, m
defer close(blockRequests)
}
- // Open a thread-local roTx for block metadata and StepsInFiles.
- // Must NOT use the stageloop's rwTx — it's thread-bound.
- execRoTx, err := te.cfg.db.BeginTemporalRo(ctx)
+ // Open a thread-local base roTx for block metadata and StepsInFiles,
+ // coordinated with the background-commit generation set so a generation
+ // dropped from the parent chain as committed is visible here (never
+ // neither). Must NOT use the stageloop's rwTx — it's thread-bound.
+ execRoTx, err := te.doms.BeginCoordinatedRo(ctx, te.cfg.db)
if err != nil {
return fmt.Errorf("executeBlocks: open roTx: %w", err)
}
defer execRoTx.Rollback()
- var blockTx kv.Tx
- if overlay := te.doms.BlockOverlay(); overlay != nil {
- blockTx = overlay.NewReadView(execRoTx)
- } else {
- blockTx = execRoTx
+ // Resolve block metadata memory-first: walk the block-overlay chain
+ // (local overlay + parent generations' overlays), falling through to the
+ // goroutine-local execRoTx for the underlying DB read only on a miss —
+ // mirroring how domain-state reads chain sd.mem → parents → tx. The parent
+ // chain must be layered ahead of the tx so an uncommitted ancestor
+ // generation's block data stays visible until its commit lands.
+ var blockTx kv.Tx = execRoTx
+ if v := te.doms.OverlayTemporalTx(execRoTx); v != nil {
+ blockTx = v
}
// Use the max of all state domain steps (not just commitment) to
diff --git a/execution/stagedsync/exec3_parallel.go b/execution/stagedsync/exec3_parallel.go
index 64433cff662..29b104b30dd 100644
--- a/execution/stagedsync/exec3_parallel.go
+++ b/execution/stagedsync/exec3_parallel.go
@@ -25,7 +25,6 @@ import (
"github.com/erigontech/erigon/db/kv"
"github.com/erigontech/erigon/db/rawdb/rawtemporaldb"
dbstate "github.com/erigontech/erigon/db/state"
- "github.com/erigontech/erigon/db/state/changeset"
"github.com/erigontech/erigon/diagnostics/metrics"
"github.com/erigontech/erigon/execution/bal"
"github.com/erigontech/erigon/execution/chain"
@@ -114,25 +113,11 @@ type parallelExecutor struct {
// accumulator for txpool state-diff notifications; set before execLoop
// starts so that AuRa system-call nonce changes are emitted per block.
accumulator *shards.Accumulator
- // changesetAccumulator state owned by the exec loop. Accessing or mutating
- // this is the exec loop's responsibility — putting it here (rather than on
- // the apply-loop side) ensures all sd.mem mutations originate from a single
- // goroutine and avoids the data race between SetChangesetAccumulator
- // (apply loop) and ApplyStateWrites (exec loop, via SysCallContract for
- // block-end system calls) on SharedDomains.mem.
- // changesetWindowStart is the first block of the batch that must capture
- // a changeset (see changesetWindowStart in exec3.go); blocks below it run
- // without an accumulator.
+ // changesetWindowStart is the first block of the batch that owns a changeset
+ // (see changesetWindowStart in exec3.go); it bounds the commitment calculator's
+ // per-block changeset reconstruction. Exec does not touch changesets — the
+ // calculator reconstructs and saves them from the tx-result stream.
changesetWindowStart uint64
- currentChangeSet *changeset.StateChangeSet
- // currentChangeSetBlock is the block number currentChangeSet belongs to
- // (0 == none). Tracked so ensureChangesetAccumulator can be a no-op when the
- // accumulator is already installed for the block whose writes are about to
- // be applied — making changeset capture robust against blocks scheduled out
- // of band (e.g. processRequest scheduling the first block of a new request
- // after the blockExecutors map went empty mid-batch, with no preceding
- // blockResult to trigger the install at the rotation site below).
- currentChangeSetBlock uint64
}
// stopKind classifies why the executor was asked to stop. It maps directly
@@ -186,35 +171,6 @@ func stopCauseOf(ctx context.Context) (*stopCause, bool) {
return nil, false
}
-// ensureChangesetAccumulator makes pe.currentChangeSet point at a fresh,
-// block-specific StateChangeSet before any of blockNum's sd.mem writes are
-// applied. Idempotent. Exec-loop only — it mutates SharedDomains.mem via
-// SetChangesetAccumulator, which must be single-writer (see the comment on
-// currentChangeSet).
-func (pe *parallelExecutor) ensureChangesetAccumulator(blockNum uint64) {
- if blockNum < pe.changesetWindowStart || blockNum == 0 || blockNum > pe.maxBlockNum {
- return
- }
- if pe.currentChangeSet != nil && pe.currentChangeSetBlock == blockNum {
- return
- }
- // A previous block's accumulator is normally saved+cleared at its
- // blockResult; if one is still installed here for a different block the
- // rotation was missed — overwrite (the previous block's changeset was
- // already saved at its blockResult, so nothing is lost).
- pe.currentChangeSet = &changeset.StateChangeSet{}
- pe.currentChangeSetBlock = blockNum
- pe.domains().SetChangesetAccumulator(pe.currentChangeSet)
-}
-
-// clearChangesetAccumulator detaches the current changeset accumulator after
-// its block's changeset has been saved. Exec-loop only.
-func (pe *parallelExecutor) clearChangesetAccumulator() {
- pe.domains().SetChangesetAccumulator(nil)
- pe.currentChangeSet = nil
- pe.currentChangeSetBlock = 0
-}
-
func (pe *parallelExecutor) exec(ctx context.Context, execStage *StageState, u Unwinder,
startBlockNum uint64, offsetFromBlockBeginning uint64, maxBlockNum uint64, blockLimit uint64,
initialTxNum uint64, inputTxNum uint64, initialCycle bool, rwTx kv.TemporalRwTx,
@@ -323,14 +279,10 @@ func (pe *parallelExecutor) execImpl(ctx context.Context, execStage *StageState,
pe.commitResultsCh = commitResults
pe.maxBlockNum = maxBlockNum
- // Configure changeset capture and seed the initial accumulator BEFORE
- // the exec loop / executeBlocks goroutines start touching sd.mem. The
- // exec loop owns all subsequent SetChangesetAccumulator transitions
- // (per-block save/clear/install) so apply-loop and exec-loop sd.mem
- // writes never race on SharedDomains.mem.
+ // Resolve the changeset window that bounds the commitment calculator's
+ // per-block changeset reconstruction; exec itself does not capture changesets.
pe.changesetWindowStart = changesetWindowStart(pe.cfg.syncCfg.AlwaysGenerateChangesets,
pe.cfg.syncCfg.MaxReorgDepth, pe.cfg.blockReader.FrozenBlocks(), startBlockNum, maxBlockNum)
- pe.ensureChangesetAccumulator(startBlockNum)
// Start the commitment calculator. It mirrors serial's per-block gate
// (exec3_serial.go: `if !dbg.BatchCommitments || shouldGenerateChangesets
@@ -387,11 +339,6 @@ func (pe *parallelExecutor) execImpl(ctx context.Context, execStage *StageState,
}
defer applyRoTx.Rollback()
- // pe.changesetWindowStart and pe.currentChangeSet were set up
- // before pe.run/executeBlocks launched their goroutines (above the
- // calculator.Start call). Per-block accumulator save/clear/install
- // transitions are driven from the exec loop's blockResult handler.
-
// appliedBlocks tracks blockNums that completed full apply-loop
// processing (including post-block validation). Used at exit to
// detect "the channel closed cleanly but a block was silently
@@ -779,11 +726,9 @@ func (pe *parallelExecutor) execImpl(ctx context.Context, execStage *StageState,
deliberateCancel()
}
- // SavePastChangesetAccumulator + SetChangesetAccumulator(nil) +
- // rotation-to-next-block accumulator are all driven by the exec
- // loop now (see execLoop's blockResult handling), so the apply
- // loop must NOT touch SharedDomains.mem here. Doing so used to
- // race with the exec loop's ApplyStateWrites for the next block.
+ // The commitment calculator owns changeset reconstruction; the
+ // apply loop must NOT touch SharedDomains.mem here — doing so
+ // used to race the exec loop's ApplyStateWrites for the next block.
if dbg.StopAfterBlock > 0 && applyResult.BlockNum == dbg.StopAfterBlock {
pe.logger.Warn(fmt.Sprintf("[%s] STOP_AFTER_BLOCK reached, exiting without commit (debug mode)", pe.logPrefix), "block", applyResult.BlockNum)
@@ -1163,31 +1108,6 @@ func (pe *parallelExecutor) completeBlock(ctx context.Context, blockResult *bloc
pe.lastExecutedBlockNum.Store(int64(blockResult.BlockNum))
pe.recordBlockExecMetrics(blockExecutor)
- // Snapshot the just-completed block's changeset BEFORE sending the
- // blockResult, so that the commitment calculator (which consumes
- // blockResults on a separate goroutine) can find this block's
- // saved changeset via GetChangesetByBlockNum at compute time.
- // In per-block compute mode (changeset window), the
- // calculator switches the accumulator to this saved CS for the
- // duration of ComputeCommitment (committer.go:computeWithBlockAccumulator)
- // so branch writes land in block N's CS rather than whatever the
- // exec loop has installed as current. If we saved AFTER sendResult,
- // the calculator could race ahead and look up an unsaved CS,
- // causing branch deltas to leak into the next block's CS and
- // produce wrong-trie-root chains on subsequent reorg-driven
- // re-execution (see TestRecreateAndRewind reproducer). Clearing
- // the live accumulator and the local pointer must still happen
- // here (in the exec loop) so the next block's accumulator install
- // below is serialized with the exec loop's other sd.mem writes
- // (system calls, finalize, ApplyStateWrites for the next block).
- // Belt-and-braces: an empty block (no tx-results reaching
- // processResults) may not have triggered the install — create
- // its (empty) accumulator so it gets saved like every other block.
- pe.ensureChangesetAccumulator(blockResult.BlockNum)
- if pe.currentChangeSet != nil {
- pe.domains().SavePastChangesetAccumulator(blockResult.BlockHash, blockResult.BlockNum, pe.currentChangeSet)
- }
-
terminal, startCatchup := pe.decideStop(blockResult, *sizeCutPending)
// mustDeliver: a terminal stop may have just published the stopCause
@@ -1195,7 +1115,6 @@ func (pe *parallelExecutor) completeBlock(ctx context.Context, blockResult *bloc
if err := blockExecutor.sendResult(ctx, blockResult, terminal); err != nil {
return false, err
}
- pe.clearChangesetAccumulator()
// Block-validity rejection: the apply loop consumes blockResult and
// returns its Err; the calculator skips the commitment compute. Exit
@@ -1231,11 +1150,6 @@ func (pe *parallelExecutor) completeBlock(ctx context.Context, blockResult *bloc
pe.RUnlock()
if ok {
- // Fast-path install of the next block's changeset accumulator,
- // still in the exec loop (single-writer). If the next block's
- // executor isn't in the map yet this is a no-op; processResults
- // then installs it lazily on the block's first apply.
- pe.ensureChangesetAccumulator(next.blockNum)
pe.onBlockStart(ctx, next.blockNum, next.blockHash)
next.execStarted = time.Now()
next.scheduleExecution(ctx, pe)
@@ -1658,11 +1572,6 @@ func (pe *parallelExecutor) processResults(ctx context.Context, applyTx kv.Tempo
return nil, fmt.Errorf("unknown block: %d", txResult.Version().BlockNum)
}
- // Ensure this block's changeset accumulator is installed before its
- // writes are applied — covers blocks scheduled out of band (with no
- // preceding blockResult to trigger the fast-path install above).
- pe.ensureChangesetAccumulator(txResult.Version().BlockNum)
-
blockResult, err = blockExecutor.nextResult(ctx, pe, txResult, applyTx)
if err != nil {
diff --git a/execution/state/database_test.go b/execution/state/database_test.go
index c37e792d31d..a7ca300f937 100644
--- a/execution/state/database_test.go
+++ b/execution/state/database_test.go
@@ -128,7 +128,7 @@ func TestCreate2Revive(t *testing.T) {
t.Fatalf("generate blocks: %v", err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(address); err != nil {
@@ -150,7 +150,7 @@ func TestCreate2Revive(t *testing.T) {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(contractAddress)); err != nil {
@@ -169,7 +169,7 @@ func TestCreate2Revive(t *testing.T) {
var key2 accounts.StorageKey
var check2 uint256.Int
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(create2address); err != nil {
@@ -192,7 +192,7 @@ func TestCreate2Revive(t *testing.T) {
if err = m.InsertChain(chain.Slice(2, 3)); err != nil {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(create2address); err != nil {
@@ -208,7 +208,7 @@ func TestCreate2Revive(t *testing.T) {
if err = m.InsertChain(chain.Slice(3, 4)); err != nil {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(create2address); err != nil {
@@ -352,7 +352,7 @@ func TestCreate2Polymorth(t *testing.T) {
t.Fatalf("generate blocks: %v", err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
@@ -375,7 +375,7 @@ func TestCreate2Polymorth(t *testing.T) {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(contractAddress)); err != nil {
@@ -392,7 +392,7 @@ func TestCreate2Polymorth(t *testing.T) {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(create2address); err != nil {
@@ -424,7 +424,7 @@ func TestCreate2Polymorth(t *testing.T) {
if err = m.InsertChain(chain.Slice(2, 3)); err != nil {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(create2address); err != nil {
@@ -440,7 +440,7 @@ func TestCreate2Polymorth(t *testing.T) {
if err = m.InsertChain(chain.Slice(3, 4)); err != nil {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(create2address); err != nil {
@@ -472,7 +472,7 @@ func TestCreate2Polymorth(t *testing.T) {
if err = m.InsertChain(chain.Slice(4, 5)); err != nil {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(create2address); err != nil {
@@ -592,7 +592,7 @@ func TestReorgOverSelfDestruct(t *testing.T) {
t.Fatalf("generate long blocks")
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
@@ -616,7 +616,7 @@ func TestReorgOverSelfDestruct(t *testing.T) {
var key0 = accounts.ZeroKey
var correctValueX uint256.Int
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(contractAddress)); err != nil {
@@ -638,7 +638,7 @@ func TestReorgOverSelfDestruct(t *testing.T) {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(contractAddress)); err != nil {
@@ -653,7 +653,7 @@ func TestReorgOverSelfDestruct(t *testing.T) {
if err = m.InsertChain(longerChain.Slice(1, 4)); err != nil {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(contractAddress)); err != nil {
@@ -752,7 +752,7 @@ func TestReorgOverStateChange(t *testing.T) {
t.Fatalf("generate longer blocks: %v", err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(address); err != nil {
@@ -777,7 +777,7 @@ func TestReorgOverStateChange(t *testing.T) {
var key0 = accounts.ZeroKey
var correctValueX uint256.Int
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(contractAddress)); err != nil {
@@ -803,7 +803,7 @@ func TestReorgOverStateChange(t *testing.T) {
if err = m.InsertChain(longerChain.Slice(1, 3)); err != nil {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(contractAddress)); err != nil {
@@ -896,7 +896,7 @@ func TestCreateOnExistingStorage(t *testing.T) {
t.Fatalf("generate blocks: %v", err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(address); err != nil {
@@ -918,7 +918,7 @@ func TestCreateOnExistingStorage(t *testing.T) {
var key0 = accounts.ZeroKey
var check0 uint256.Int
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(contractAddress)); err != nil {
@@ -1048,7 +1048,7 @@ func TestEip2200Gas(t *testing.T) {
}
var balanceBefore uint256.Int
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(address); err != nil {
@@ -1071,7 +1071,7 @@ func TestEip2200Gas(t *testing.T) {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(contractAddress)); err != nil {
@@ -1150,7 +1150,7 @@ func TestWrongIncarnation(t *testing.T) {
t.Fatalf("generate blocks: %v", err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(address); err != nil {
@@ -1172,7 +1172,7 @@ func TestWrongIncarnation(t *testing.T) {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
stateReader := m.NewStateReader(tx)
acc, err := stateReader.ReadAccountData(accounts.InternAddress(contractAddress))
if err != nil {
@@ -1201,7 +1201,7 @@ func TestWrongIncarnation(t *testing.T) {
if err = m.InsertChain(chain.Slice(1, 2)); err != nil {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
stateReader := m.NewStateReader(tx)
acc, err := stateReader.ReadAccountData(accounts.InternAddress(contractAddress))
if err != nil {
@@ -1311,7 +1311,7 @@ func TestWrongIncarnation2(t *testing.T) {
t.Fatalf("generate longer blocks: %v", err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(address); err != nil {
@@ -1333,7 +1333,7 @@ func TestWrongIncarnation2(t *testing.T) {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(contractAddress)); err != nil {
@@ -1361,7 +1361,7 @@ func TestWrongIncarnation2(t *testing.T) {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
stateReader := m.NewStateReader(tx)
acc, err := stateReader.ReadAccountData(accounts.InternAddress(contractAddress))
if err != nil {
@@ -1653,7 +1653,7 @@ func TestRecreateAndRewind(t *testing.T) {
var key0 = accounts.ZeroKey
var check0 uint256.Int
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(phoenixAddress)); err != nil {
@@ -1674,7 +1674,7 @@ func TestRecreateAndRewind(t *testing.T) {
if err = m.InsertChain(chain.Slice(2, chain.Length())); err != nil {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
@@ -1697,7 +1697,7 @@ func TestRecreateAndRewind(t *testing.T) {
if err = m.InsertChain(longerChain); err != nil {
t.Fatal(err)
}
- err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
+ err = m.OverlayDB().ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
st := state.New(m.NewStateReader(tx))
defer st.Close()
if exist, err := st.Exist(accounts.InternAddress(phoenixAddress)); err != nil {
@@ -1768,6 +1768,9 @@ func TestTxLookupUnwind(t *testing.T) {
if err = m.InsertChain(chain2); err != nil {
t.Fatal(err)
}
+ // Count is unsupported on the block-overlay tx; drain the background commit
+ // and count on the raw DB instead.
+ m.ExecModule.WaitCommitsDrained()
var count uint64
if err = m.DB.ViewTemporal(context.Background(), func(tx kv.TemporalTx) error {
var e error
diff --git a/execution/tests/blockgen/chain_makers.go b/execution/tests/blockgen/chain_makers.go
index 8ec0796b934..ef589940a0a 100644
--- a/execution/tests/blockgen/chain_makers.go
+++ b/execution/tests/blockgen/chain_makers.go
@@ -429,10 +429,19 @@ func InitPraguePreDeploys(db kv.TemporalRwDB, config *chain.Config, logger log.L
// Blocks created by GenerateChain do not contain valid proof of work
// values. Inserting them into BlockChain requires use of FakePow or
// a similar non-validating proof of work implementation.
-func GenerateChain(config *chain.Config, parent *types.Block, engine rules.Engine, db kv.TemporalRoDB, n int, gen func(int, *BlockGen)) (*ChainPack, error) {
+// GenerateChain builds n blocks on top of parent. The optional readSD is the
+// latest published SharedDomains: pass it when parent is the current tip so the
+// generator reads the tip's in-flight (not-yet-committed) state via the domain
+// chain instead of a raw DB that lags under background commit. Omit it for
+// historical-parent builds, which read committed state through their own SD.
+func GenerateChain(config *chain.Config, parent *types.Block, engine rules.Engine, db kv.TemporalRoDB, n int, gen func(int, *BlockGen), readSD ...*execctx.SharedDomains) (*ChainPack, error) {
if config == nil {
config = chain.AllProtocolChanges
}
+ var parentSD *execctx.SharedDomains
+ if len(readSD) > 0 {
+ parentSD = readSD[0]
+ }
headers, blocks, receipts := make([]*types.Header, n), make(types.Blocks, n), make([]types.Receipts, n)
chainreader := &FakeChainReader{Cfg: config, current: parent}
ctx := context.Background()
@@ -448,6 +457,9 @@ func GenerateChain(config *chain.Config, parent *types.Block, engine rules.Engin
return nil, err
}
defer domains.Close()
+ if parentSD != nil {
+ domains.SetParent(parentSD)
+ }
latestTxNum, _, err := domains.SeekCommitment(ctx, tx)
if err != nil {
return nil, err
@@ -456,9 +468,12 @@ func GenerateChain(config *chain.Config, parent *types.Block, engine rules.Engin
stateReader := state.NewReaderV3(domains.AsGetter(tx))
stateWriter := state.NewWriter(domains.AsPutDel(tx), nil, latestTxNum)
- txNum, err := rawdbv3.TxNums.Max(ctx, tx, parent.NumberU64())
- if err != nil {
- return nil, err
+ txNum := latestTxNum
+ if parentSD == nil {
+ txNum, err = rawdbv3.TxNums.Max(ctx, tx, parent.NumberU64())
+ if err != nil {
+ return nil, err
+ }
}
txNumIncrement := func() {
txNum++
diff --git a/execution/verify/history_verify_test.go b/execution/verify/history_verify_test.go
index dbf8bbec4fa..fe0696d1335 100644
--- a/execution/verify/history_verify_test.go
+++ b/execution/verify/history_verify_test.go
@@ -63,7 +63,7 @@ func TestHistoryVerification_SimpleBlocks(t *testing.T) {
batchEnd := min(batchStart+batchSize, numBlocks)
chainResult, err := blockgen.GenerateChain(m.ChainConfig, parent, m.Engine, m.DB, batchEnd-batchStart, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(chainResult))
parent = chainResult.TopBlock
@@ -159,7 +159,7 @@ func TestHistoryVerification_WithUserTransactions(t *testing.T) {
b.AddTx(signed)
nonce++
}
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(chainResult))
t.Logf("Inserted %d blocks with 2 user txs each", numBlocks)
diff --git a/node/eth/backend.go b/node/eth/backend.go
index f7ce65085bc..04d4e6a71ee 100644
--- a/node/eth/backend.go
+++ b/node/eth/backend.go
@@ -710,6 +710,15 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger
backend.stateDiffClient = direct.NewStateDiffClientDirect(backend.kvRPC)
+ // SD-wrapper state cache, shared by the txpool and the embedded
+ // rpcdaemon. Both read account state from the authoritative in-flight
+ // SharedDomains rather than an async-notification-fed cache that can
+ // diverge from it during a background commit (gate item 2). execModule
+ // is wired later by NewExecModule; until then Cache.View falls back to
+ // the published SD via SetPublishedSD.
+ execmoduleCache := &execmodule.Cache{}
+ execmoduleCache.SetPublishedSD(backend.notifications.Events.LatestSD)
+
var txnProvider txnprovider.TxnProvider
var blobGetter txpool.BlobGetter
if config.TxPool.Disable {
@@ -726,7 +735,7 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger
ctx,
config.TxPool,
backend.chainDB,
- kvcache.NewLatestBatchCache(),
+ execmoduleCache,
sentries,
backend.stateDiffClient,
blockBuilderNotifyNewTxns,
@@ -741,8 +750,6 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger
blobGetter = backend.txPool
}
- execmoduleCache := &execmodule.Cache{}
- execmoduleCache.SetPublishedSD(backend.notifications.Events.LatestSD)
httpRpcCfg := stack.Config().Http
httpRpcCfg.StateCache.LocalCache = execmoduleCache
ethRpcClient, txPoolRpcClient, miningRpcClient, rpcDaemonStateCache, rpcFilters := rpcdaemoncli.EmbeddedServices(
@@ -838,7 +845,6 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger
txnProvider,
backend.sealCancel,
latestBlockBuiltStore,
- backend.notifications.Events.LatestSD,
logger,
)
backend.pendingBlocks = blkBuilder.PendingBlockCh()
diff --git a/node/ethconfig/config.go b/node/ethconfig/config.go
index f5ae953f35e..bba1bc2e4d8 100644
--- a/node/ethconfig/config.go
+++ b/node/ethconfig/config.go
@@ -113,7 +113,7 @@ var Defaults = Config{
},
FcuTimeout: 1 * time.Second,
FcuBackgroundPrune: true,
- FcuBackgroundCommit: false, // to enable, we need to 1) have rawdb API go via execctx and 2) revive Coherent cache for rpcdaemon
+ FcuBackgroundCommit: false,
ExperimentalBAL: false,
WarmupKzgCtxOnInit: true,
}
diff --git a/rpc/gasprice/bench_test.go b/rpc/gasprice/bench_test.go
index c3f419687c5..27c5d5c32b8 100644
--- a/rpc/gasprice/bench_test.go
+++ b/rpc/gasprice/bench_test.go
@@ -73,7 +73,7 @@ func newTestBackendN(tb testing.TB, n int) *execmoduletester.ExecModuleTester {
}
b.AddTx(tx)
}
- })
+ }, m.PublishedSD())
require.NoError(tb, err)
require.NoError(tb, m.InsertChain(ch))
return m
diff --git a/rpc/gasprice/feehistory_test.go b/rpc/gasprice/feehistory_test.go
index 0cfd4deca60..5fa7c8595b3 100644
--- a/rpc/gasprice/feehistory_test.go
+++ b/rpc/gasprice/feehistory_test.go
@@ -81,12 +81,12 @@ func TestFeeHistory(t *testing.T) {
defer m.Close()
baseApi := jsonrpc.NewBaseApi(nil, kvcache.NewLatestBatchCache(), m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs})
- tx, err := m.DB.BeginTemporalRo(m.Ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(m.Ctx)
require.NoError(t, err)
defer tx.Rollback()
cache := jsonrpc.NewGasPriceCache()
- oracle := gasprice.NewOracle(jsonrpc.NewGasPriceOracleBackend(m.DB, tx, baseApi), config, cache, gasprice.NewFeeHistoryCache(), log.New())
+ oracle := gasprice.NewOracle(jsonrpc.NewGasPriceOracleBackend(m.OverlayDB(), tx, baseApi), config, cache, gasprice.NewFeeHistoryCache(), log.New())
first, reward, baseFee, ratio, blobBaseFee, blobBaseFeeRatio, err := oracle.FeeHistory(context.Background(), c.count, c.last, c.percent)
diff --git a/rpc/gasprice/gasprice_test.go b/rpc/gasprice/gasprice_test.go
index bd0100e436a..0d443efe772 100644
--- a/rpc/gasprice/gasprice_test.go
+++ b/rpc/gasprice/gasprice_test.go
@@ -66,7 +66,7 @@ func newTestBackend(t *testing.T) *execmoduletester.ExecModuleTester {
t.Fatalf("failed to create tx: %v", txErr)
}
b.AddTx(tx)
- })
+ }, m.PublishedSD())
if err != nil {
t.Error(err)
}
@@ -90,7 +90,7 @@ func TestSuggestPrice(t *testing.T) {
m := newTestBackend(t) //, big.NewInt(16), c.pending)
baseApi := jsonrpc.NewBaseApi(nil, kvcache.NewLatestBatchCache(), m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs})
- tx, err := m.DB.BeginTemporalRo(m.Ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(m.Ctx)
require.NoError(t, err)
defer tx.Rollback()
@@ -385,7 +385,7 @@ func TestSuggestTipCap_SparseBlocks(t *testing.T) {
}
b.AddTx(tx)
}
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(ch))
@@ -396,7 +396,7 @@ func TestSuggestTipCap_SparseBlocks(t *testing.T) {
}
baseApi := jsonrpc.NewBaseApi(nil, kvcache.NewLatestBatchCache(), m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs})
- dbTx, txErr := m.DB.BeginTemporalRo(m.Ctx)
+ dbTx, txErr := m.OverlayDB().BeginTemporalRo(m.Ctx)
require.NoError(t, txErr)
defer dbTx.Rollback()
@@ -422,7 +422,7 @@ func TestSuggestTipCap_AllEmptyBlocks(t *testing.T) {
ch, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, totalBlocks, func(_ int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{1})
// no transactions — all blocks are empty
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(ch))
@@ -433,7 +433,7 @@ func TestSuggestTipCap_AllEmptyBlocks(t *testing.T) {
}
baseApi := jsonrpc.NewBaseApi(nil, kvcache.NewLatestBatchCache(), m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs})
- dbTx, txErr := m.DB.BeginTemporalRo(m.Ctx)
+ dbTx, txErr := m.OverlayDB().BeginTemporalRo(m.Ctx)
require.NoError(t, txErr)
defer dbTx.Rollback()
diff --git a/rpc/jsonrpc/bor_api_impl.go b/rpc/jsonrpc/bor_api_impl.go
index 861796e65ec..f03d1b6adc5 100644
--- a/rpc/jsonrpc/bor_api_impl.go
+++ b/rpc/jsonrpc/bor_api_impl.go
@@ -60,7 +60,7 @@ func (api *BorImpl) GetSnapshot(number *rpc.BlockNumber) (*Snapshot, error) {
// Retrieve the requested block number (or current if none requested)
var header *types.Header
if number == nil || *number == rpc.LatestBlockNumber {
- header = rawdb.ReadCurrentHeader(tx)
+ header = rawdb.ReadCurrentHeader(api.filters.WithOverlay(tx))
} else {
header, _ = api.headerByNumber(ctx, *number, tx)
}
@@ -102,7 +102,7 @@ func (api *BorImpl) GetAuthor(blockNrOrHash *rpc.BlockNumberOrHash) (accounts.Ad
//nolint:nestif
if blockNrOrHash == nil {
- latestBlockNum, err2 := rpchelper.GetLatestBlockNumber(tx)
+ latestBlockNum, err2 := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
if err2 != nil {
return accounts.NilAddress, err2
}
@@ -174,7 +174,7 @@ func (api *BorImpl) GetSigners(number *rpc.BlockNumber) ([]common.Address, error
// Retrieve the requested block number (or current if none requested)
var header *types.Header
if number == nil || *number == rpc.LatestBlockNumber {
- header = rawdb.ReadCurrentHeader(tx)
+ header = rawdb.ReadCurrentHeader(api.filters.WithOverlay(tx))
} else {
header, _ = api.headerByNumber(ctx, *number, tx)
}
@@ -298,7 +298,7 @@ func (api *BorImpl) getLatestBlockNum(ctx context.Context) (uint64, error) {
}
defer tx.Rollback()
- return rpchelper.GetLatestBlockNumber(tx)
+ return rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
}
// GetSnapshotProposer retrieves the in-turn signer at a given block.
@@ -314,11 +314,11 @@ func (api *BorImpl) GetSnapshotProposer(blockNrOrHash *rpc.BlockNumberOrHash) (c
var header *types.Header
//nolint:nestif
if blockNrOrHash == nil {
- header = rawdb.ReadCurrentHeader(tx)
+ header = rawdb.ReadCurrentHeader(api.filters.WithOverlay(tx))
} else {
if blockNr, ok := blockNrOrHash.Number(); ok {
if blockNr == rpc.LatestBlockNumber {
- header = rawdb.ReadCurrentHeader(tx)
+ header = rawdb.ReadCurrentHeader(api.filters.WithOverlay(tx))
} else {
header, err = api.headerByNumber(ctx, blockNr, tx)
}
@@ -352,11 +352,11 @@ func (api *BorImpl) GetSnapshotProposerSequence(blockNrOrHash *rpc.BlockNumberOr
// Retrieve the requested block number (or current if none requested)
var header *types.Header
if blockNrOrHash == nil {
- header = rawdb.ReadCurrentHeader(tx)
+ header = rawdb.ReadCurrentHeader(api.filters.WithOverlay(tx))
} else {
if blockNr, ok := blockNrOrHash.Number(); ok {
if blockNr == rpc.LatestBlockNumber {
- header = rawdb.ReadCurrentHeader(tx)
+ header = rawdb.ReadCurrentHeader(api.filters.WithOverlay(tx))
} else {
header, err = api.headerByNumber(ctx, blockNr, tx)
}
diff --git a/rpc/jsonrpc/call_traces_test.go b/rpc/jsonrpc/call_traces_test.go
index 6bd428d1853..34201de2eda 100644
--- a/rpc/jsonrpc/call_traces_test.go
+++ b/rpc/jsonrpc/call_traces_test.go
@@ -68,7 +68,7 @@ func TestCallTraceOneByOne(t *testing.T) {
m := execmoduletester.New(t)
chain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 10, func(i int, gen *blockgen.BlockGen) {
gen.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
if err != nil {
t.Fatalf("generate chain: %v", err)
}
@@ -103,7 +103,7 @@ func TestCallTraceUnwind(t *testing.T) {
var err error
chainA, err = blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 10, func(i int, gen *blockgen.BlockGen) {
gen.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
if err != nil {
t.Fatalf("generate chainA: %v", err)
}
@@ -113,7 +113,7 @@ func TestCallTraceUnwind(t *testing.T) {
} else {
gen.SetCoinbase(common.Address{2})
}
- })
+ }, m.PublishedSD())
if err != nil {
t.Fatalf("generate chainB: %v", err)
}
@@ -178,7 +178,7 @@ func TestFilterNoAddresses(t *testing.T) {
m := execmoduletester.New(t)
chain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 10, func(i int, gen *blockgen.BlockGen) {
gen.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
if err != nil {
t.Fatalf("generate chain: %v", err)
}
@@ -230,7 +230,7 @@ func TestFilterAddressIntersection(t *testing.T) {
t.Fatal(err)
}
block.AddTx(txn)
- })
+ }, m.PublishedSD())
require.NoError(t, err, "generate chain")
err = m.InsertChain(chain)
diff --git a/rpc/jsonrpc/corner_cases_support_test.go b/rpc/jsonrpc/corner_cases_support_test.go
index 1642fb42d3c..7edfb05b3e3 100644
--- a/rpc/jsonrpc/corner_cases_support_test.go
+++ b/rpc/jsonrpc/corner_cases_support_test.go
@@ -35,7 +35,7 @@ func TestNotFoundMustReturnNil(t *testing.T) {
}
assertions := require.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
ctx := context.Background()
a, err := api.GetTransactionByBlockNumberAndIndex(ctx, 10_000, 1)
@@ -80,7 +80,7 @@ func TestNotFoundMustReturnNil(t *testing.T) {
func TestNotFoundMustReturnError(t *testing.T) {
assertions := require.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
ctx := context.Background()
a, err := api.GetBalance(ctx, common.Address{}, bnhPtr(rpc.BlockNumberOrHashWithNumber(10_000)))
diff --git a/rpc/jsonrpc/debug_api.go b/rpc/jsonrpc/debug_api.go
index e4527edb291..d5fbeb3f743 100644
--- a/rpc/jsonrpc/debug_api.go
+++ b/rpc/jsonrpc/debug_api.go
@@ -130,7 +130,7 @@ func (api *DebugAPIImpl) SetHead(ctx context.Context, number hexutil.Uint64) err
}
defer tx.Rollback()
- currentHead, err := rpchelper.GetLatestBlockNumber(tx)
+ currentHead, err := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
if err != nil {
return err
}
@@ -247,7 +247,7 @@ func (api *DebugAPIImpl) AccountRange(ctx context.Context, blockNrOrHash rpc.Blo
if number == rpc.LatestBlockNumber {
var err error
- blockNumber, err = stages.GetStageProgress(tx, stages.Execution)
+ blockNumber, err = stages.GetStageProgress(api.filters.WithOverlay(tx), stages.Execution)
if err != nil {
return state.IteratorDump{}, fmt.Errorf("last block has not found: %w", err)
}
@@ -314,7 +314,7 @@ func (api *DebugAPIImpl) GetModifiedAccountsByNumber(ctx context.Context, startN
}
defer tx.Rollback()
- latestBlock, err := stages.GetStageProgress(tx, stages.Execution)
+ latestBlock, err := stages.GetStageProgress(api.filters.WithOverlay(tx), stages.Execution)
if err != nil {
return nil, err
}
@@ -516,7 +516,7 @@ func (api *DebugAPIImpl) GetModifiedAccountsByHash(ctx context.Context, startHas
}
defer tx.Rollback()
- latestBlock, err := stages.GetStageProgress(tx, stages.Execution)
+ latestBlock, err := stages.GetStageProgress(api.filters.WithOverlay(tx), stages.Execution)
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/debug_api_test.go b/rpc/jsonrpc/debug_api_test.go
index 809f1758f98..a2a2b6595e7 100644
--- a/rpc/jsonrpc/debug_api_test.go
+++ b/rpc/jsonrpc/debug_api_test.go
@@ -121,8 +121,8 @@ func TestTraceBlockByNumber(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
stateCache := kvcache.New(kvcache.DefaultCoherentConfig)
baseApi := NewBaseApi(nil, stateCache, m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs})
- ethApi := newEthApiForTest(baseApi, m.DB, nil, nil)
- api := NewPrivateDebugAPI(baseApi, m.DB, nil, &rpccfg.DebugApiConfig{})
+ ethApi := newEthApiForTest(baseApi, m.OverlayDB(), nil, nil)
+ api := NewPrivateDebugAPI(baseApi, m.OverlayDB(), nil, &rpccfg.DebugApiConfig{})
for _, tt := range debugTraceTransactionTests {
var buf bytes.Buffer
s := jsonstream.New(jsoniter.NewStream(jsoniter.ConfigDefault, &buf, 4096))
@@ -172,7 +172,7 @@ func TestTraceBlockByNumber(t *testing.T) {
func TestTraceBlockByHash(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- ethApi := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ ethApi := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
api := newDebugApiForTest(m)
for _, tt := range debugTraceTransactionTests {
var buf bytes.Buffer
@@ -349,7 +349,7 @@ func TestTraceErrorPathsWriteNoStream(t *testing.T) {
t.Run("TraceBlockByHash_genesis", func(t *testing.T) {
var genesisHash common.Hash
- require.NoError(t, m.DB.View(m.Ctx, func(tx kv.Tx) error {
+ require.NoError(t, m.OverlayDB().View(m.Ctx, func(tx kv.Tx) error {
genesisHash, _, _ = m.BlockReader.CanonicalHash(m.Ctx, tx, 0)
return nil
}))
@@ -517,7 +517,7 @@ func TestTxResultFieldStreamLazy(t *testing.T) {
func TestTraceBlockErrorBeforeWrite(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
api := newDebugApiForTest(m)
- ethApi := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ ethApi := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
tx, err := ethApi.GetTransactionByHash(m.Ctx, common.HexToHash(debugTraceTransactionTests[0].txHash))
require.NoError(t, err)
@@ -625,7 +625,7 @@ func TestStorageRangeAt(t *testing.T) {
t.Run("invalid addr", func(t *testing.T) {
var block4 *types.Block
var err error
- err = m.DB.View(m.Ctx, func(tx kv.Tx) error {
+ err = m.OverlayDB().View(m.Ctx, func(tx kv.Tx) error {
block4, err = m.BlockReader.BlockByNumber(m.Ctx, tx, 4)
return err
})
@@ -638,7 +638,7 @@ func TestStorageRangeAt(t *testing.T) {
})
t.Run("block 4, addr 1", func(t *testing.T) {
var block4 *types.Block
- err := m.DB.View(m.Ctx, func(tx kv.Tx) error {
+ err := m.OverlayDB().View(m.Ctx, func(tx kv.Tx) error {
block4, _ = m.BlockReader.BlockByNumber(m.Ctx, tx, 4)
return nil
})
@@ -659,7 +659,7 @@ func TestStorageRangeAt(t *testing.T) {
})
t.Run("block latest, addr 1", func(t *testing.T) {
var latestBlock *types.Block
- err := m.DB.View(m.Ctx, func(tx kv.Tx) (err error) {
+ err := m.OverlayDB().View(m.Ctx, func(tx kv.Tx) (err error) {
latestBlock, err = m.BlockReader.CurrentBlock(tx)
return err
})
@@ -715,10 +715,10 @@ func TestStorageRangeAt(t *testing.T) {
func TestStorageRangeAtGethCompat(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, nil, &rpccfg.DebugApiConfig{GethCompatibility: true})
+ api := NewPrivateDebugAPI(newBaseApiForTest(m), m.OverlayDB(), nil, &rpccfg.DebugApiConfig{GethCompatibility: true})
t.Run("block latest, addr 1", func(t *testing.T) {
var latestBlock *types.Block
- err := m.DB.View(m.Ctx, func(tx kv.Tx) (err error) {
+ err := m.OverlayDB().View(m.Ctx, func(tx kv.Tx) (err error) {
latestBlock, err = m.BlockReader.CurrentBlock(tx)
return err
})
@@ -956,7 +956,7 @@ func TestMapTxNum2BlockNum(t *testing.T) {
}
}
t.Run("descend", func(t *testing.T) {
- tx, err := m.DB.BeginTemporalRo(m.Ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(m.Ctx)
require.NoError(t, err)
defer tx.Rollback()
@@ -968,7 +968,7 @@ func TestMapTxNum2BlockNum(t *testing.T) {
checkIter(t, expectTxNums, txNumsIter)
})
t.Run("ascend", func(t *testing.T) {
- tx, err := m.DB.BeginTemporalRo(m.Ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(m.Ctx)
require.NoError(t, err)
defer tx.Rollback()
@@ -980,7 +980,7 @@ func TestMapTxNum2BlockNum(t *testing.T) {
checkIter(t, expectTxNums, txNumsIter)
})
t.Run("ascend limit", func(t *testing.T) {
- tx, err := m.DB.BeginTemporalRo(m.Ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(m.Ctx)
require.NoError(t, err)
defer tx.Rollback()
@@ -998,7 +998,7 @@ func TestAccountAt(t *testing.T) {
api := newDebugApiForTest(m)
var blockHash0, blockHash1, blockHash3, blockHash10, blockHashNonExistent common.Hash
- _ = m.DB.View(m.Ctx, func(tx kv.Tx) error {
+ _ = m.OverlayDB().View(m.Ctx, func(tx kv.Tx) error {
blockHash0, _, _ = m.BlockReader.CanonicalHash(m.Ctx, tx, 0)
blockHash1, _, _ = m.BlockReader.CanonicalHash(m.Ctx, tx, 1)
blockHash3, _, _ = m.BlockReader.CanonicalHash(m.Ctx, tx, 3)
@@ -1058,7 +1058,7 @@ func TestAccountAt(t *testing.T) {
func TestGetBadBlocks(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, nil, &rpccfg.DebugApiConfig{GasCap: 5000000})
+ api := NewPrivateDebugAPI(newBaseApiForTest(m), m.OverlayDB(), nil, &rpccfg.DebugApiConfig{GasCap: 5000000})
ctx := context.Background()
require := require.New(t)
@@ -1113,7 +1113,7 @@ func TestGetBadBlocks(t *testing.T) {
tx.Commit()
// Reset the global bad block cache so it reads only from this test's DB
- tx2, err := m.DB.BeginRo(ctx)
+ tx2, err := m.OverlayDB().BeginRo(ctx)
require.NoError(err)
defer tx2.Rollback()
require.NoError(rawdb.ResetBadBlockCache(tx2, 100))
@@ -1131,7 +1131,7 @@ func TestGetBadBlocks(t *testing.T) {
func TestGetRawTransaction(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, nil, &rpccfg.DebugApiConfig{GasCap: 5000000})
+ api := NewPrivateDebugAPI(newBaseApiForTest(m), m.OverlayDB(), nil, &rpccfg.DebugApiConfig{GasCap: 5000000})
ctx := context.Background()
require := require.New(t)
@@ -1148,7 +1148,7 @@ func TestGetRawTransaction(t *testing.T) {
}
var testedOnce = false
for i := range number {
- tx, err := m.DB.BeginRo(ctx)
+ tx, err := m.OverlayDB().BeginRo(ctx)
require.NoError(err)
defer tx.Rollback()
block, err := api._blockReader.BlockByNumber(ctx, tx, i)
@@ -1172,11 +1172,11 @@ func TestGetRawTransaction(t *testing.T) {
func TestGetRawReceipts(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, nil, &rpccfg.DebugApiConfig{GasCap: 5000000})
+ api := NewPrivateDebugAPI(newBaseApiForTest(m), m.OverlayDB(), nil, &rpccfg.DebugApiConfig{GasCap: 5000000})
ctx := context.Background()
require := require.New(t)
- tx, err := m.DB.BeginTemporalRo(ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(ctx)
require.NoError(err)
defer tx.Rollback()
number := *rawdb.ReadCurrentBlockNumber(tx)
@@ -1224,7 +1224,7 @@ func TestExecutionWitness(t *testing.T) {
// Get the latest block number
var latestBlockNum uint64
- err = m.DB.View(ctx, func(tx kv.Tx) error {
+ err = m.OverlayDB().View(ctx, func(tx kv.Tx) error {
latestBlockNum, _ = stages.GetStageProgress(tx, stages.Execution)
return nil
})
@@ -1266,7 +1266,7 @@ func TestExecutionWitness(t *testing.T) {
t.Run("by block hash", func(t *testing.T) {
var blockHash common.Hash
- err := m.DB.View(ctx, func(tx kv.Tx) error {
+ err := m.OverlayDB().View(ctx, func(tx kv.Tx) error {
blockHash, _, _ = m.BlockReader.CanonicalHash(ctx, tx, 1)
return nil
})
@@ -1462,7 +1462,7 @@ func TestSetHead(t *testing.T) {
logger := log.New()
// Determine the canonical head of the test chain.
- roTx, err := m.DB.BeginRo(ctx)
+ roTx, err := m.OverlayDB().BeginRo(ctx)
require.NoError(t, err)
defer roTx.Rollback()
head, err := rpchelper.GetLatestBlockNumber(roTx)
@@ -1475,7 +1475,7 @@ func TestSetHead(t *testing.T) {
backendServer := privateapi.NewEthBackendServer(ctx, mock, m.DB, m.Notifications, m.BlockReader, nil, logger, builder.NewLatestBlockBuiltStore(), nil)
backendClient := direct.NewEthBackendClientDirect(backendServer)
backend := rpcservices.NewRemoteBackend(backendClient, m.DB, m.BlockReader)
- return NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, backend, &rpccfg.DebugApiConfig{})
+ return NewPrivateDebugAPI(newBaseApiForTest(m), m.OverlayDB(), backend, &rpccfg.DebugApiConfig{})
}
// Rewinding one block below the current head is the simplest valid rewind.
@@ -1553,7 +1553,7 @@ func TestSetHeadCanonicalCleanup(t *testing.T) {
ctx := m.Ctx
// Snapshot canonical state before the unwind.
- roTx, err := m.DB.BeginRo(ctx)
+ roTx, err := m.OverlayDB().BeginRo(ctx)
require.NoError(t, err)
defer roTx.Rollback()
head, err := rpchelper.GetLatestBlockNumber(roTx)
@@ -1582,7 +1582,7 @@ func TestSetHeadCanonicalCleanup(t *testing.T) {
require.NoError(t, err)
// --- Verify DB state after unwind ---
- roTx, err = m.DB.BeginRo(ctx)
+ roTx, err = m.OverlayDB().BeginRo(ctx)
require.NoError(t, err)
defer roTx.Rollback()
@@ -1623,7 +1623,7 @@ func TestSetHeadCanonicalCleanup(t *testing.T) {
"UpdateForkChoice back to original head should succeed after SetHead")
// Verify the chain is back at the original head.
- roTx, err = m.DB.BeginRo(ctx)
+ roTx, err = m.OverlayDB().BeginRo(ctx)
require.NoError(t, err)
defer roTx.Rollback()
diff --git a/rpc/jsonrpc/erigon_receipts.go b/rpc/jsonrpc/erigon_receipts.go
index 5265baec51c..d282563d173 100644
--- a/rpc/jsonrpc/erigon_receipts.go
+++ b/rpc/jsonrpc/erigon_receipts.go
@@ -131,7 +131,7 @@ func (api *ErigonImpl) GetLogs(ctx context.Context, crit filters.FilterCriteria)
} else {
var err error
- begin, end, err = logRangeLatestOnly(tx, crit)
+ begin, end, err = logRangeLatestOnly(api.filters.WithOverlay(tx), crit)
if err != nil {
return nil, err
}
@@ -208,7 +208,7 @@ func (api *ErigonImpl) GetLatestLogs(ctx context.Context, crit filters.FilterCri
begin = header.Number.Uint64()
end = header.Number.Uint64()
} else {
- begin, end, err = logRangeLatestOnly(tx, crit)
+ begin, end, err = logRangeLatestOnly(api.filters.WithOverlay(tx), crit)
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/erigon_receipts_test.go b/rpc/jsonrpc/erigon_receipts_test.go
index ac4ae1b2c27..75e30e9b151 100644
--- a/rpc/jsonrpc/erigon_receipts_test.go
+++ b/rpc/jsonrpc/erigon_receipts_test.go
@@ -47,7 +47,7 @@ func TestGetLogs(t *testing.T) {
assert, require := assert.New(t), require.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
{
- ethApi := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ ethApi := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
logs, err := ethApi.GetLogs(context.Background(), filters.FilterCriteria{FromBlock: big.NewInt(0), ToBlock: big.NewInt(10)})
require.NoError(err)
@@ -79,7 +79,7 @@ func TestErigonGetLatestLogs(t *testing.T) {
}
assert := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- db := m.DB
+ db := m.OverlayDB()
api := NewErigonAPI(newBaseApiForTest(m), db, nil)
expectedLogs, _ := api.GetLogs(m.Ctx, filters.FilterCriteria{FromBlock: big.NewInt(0), ToBlock: big.NewInt(rpc.LatestBlockNumber.Int64())})
@@ -119,7 +119,7 @@ func TestErigonGetLatestLogs(t *testing.T) {
func TestErigonGetLatestLogsIgnoreTopics(t *testing.T) {
assert := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- db := m.DB
+ db := m.OverlayDB()
api := NewErigonAPI(newBaseApiForTest(m), db, nil)
expectedLogs, _ := api.GetLogs(m.Ctx, filters.FilterCriteria{FromBlock: big.NewInt(0), ToBlock: big.NewInt(rpc.LatestBlockNumber.Int64())})
@@ -199,7 +199,7 @@ func TestGetBlockReceiptsByBlockHash(t *testing.T) {
}
// Assemble the test environment
m := mockWithGenerator(t, 4, generator)
- api := NewErigonAPI(newBaseApiForTest(m), m.DB, nil)
+ api := NewErigonAPI(newBaseApiForTest(m), m.OverlayDB(), nil)
expect := map[uint64]string{
0: `[]`,
@@ -208,7 +208,7 @@ func TestGetBlockReceiptsByBlockHash(t *testing.T) {
3: `[]`,
4: `[]`,
}
- err := m.DB.View(m.Ctx, func(tx kv.Tx) error {
+ err := m.OverlayDB().View(m.Ctx, func(tx kv.Tx) error {
for i := uint64(0); i <= rawdb.ReadCurrentHeader(tx).Number.Uint64(); i++ {
block := rawdb.ReadHeaderByNumber(tx, i)
@@ -230,7 +230,7 @@ func TestGetBlockReceiptsByBlockHash(t *testing.T) {
// requested block range exceeds rangeLimit.
func TestGetLogs_RangeLimitExceeded(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- ethApi := newEthApiForTest(newBaseApiWithLimits(m, 5, 0, 0), m.DB, nil, nil)
+ ethApi := newEthApiForTest(newBaseApiWithLimits(m, 5, 0, 0), m.OverlayDB(), nil, nil)
_, err := ethApi.GetLogs(context.Background(), filters.FilterCriteria{
FromBlock: big.NewInt(0),
ToBlock: big.NewInt(10),
@@ -247,7 +247,7 @@ func TestGetLogs_RangeLimitExceeded(t *testing.T) {
// range is within rangeLimit.
func TestGetLogs_RangeLimitOk(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- ethApi := newEthApiForTest(newBaseApiWithLimits(m, 11, 0, 0), m.DB, nil, nil)
+ ethApi := newEthApiForTest(newBaseApiWithLimits(m, 11, 0, 0), m.OverlayDB(), nil, nil)
logs, err := ethApi.GetLogs(context.Background(), filters.FilterCriteria{
FromBlock: big.NewInt(0),
ToBlock: big.NewInt(10),
@@ -260,7 +260,7 @@ func TestGetLogs_RangeLimitOk(t *testing.T) {
// unlimited (0).
func TestGetLogs_MaxResultsOk(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- ethApi := newEthApiForTest(newBaseApiWithLimits(m, 0, 0, 0), m.DB, nil, nil)
+ ethApi := newEthApiForTest(newBaseApiWithLimits(m, 0, 0, 0), m.OverlayDB(), nil, nil)
logs, err := ethApi.GetLogs(context.Background(), filters.FilterCriteria{
FromBlock: big.NewInt(0),
ToBlock: big.NewInt(10),
@@ -273,7 +273,7 @@ func TestGetLogs_MaxResultsOk(t *testing.T) {
// when the matching log count exceeds maxResults.
func TestGetLogs_MaxResultsExceeded(t *testing.T) {
m, _, contractAddr, _ := chainWithDeployedContract(t)
- ethApi := newEthApiForTest(newBaseApiWithLimits(m, 0, 1, 0), m.DB, nil, nil)
+ ethApi := newEthApiForTest(newBaseApiWithLimits(m, 0, 1, 0), m.OverlayDB(), nil, nil)
_, err := ethApi.GetLogs(context.Background(), filters.FilterCriteria{
FromBlock: big.NewInt(0),
ToBlock: big.NewInt(rpc.LatestBlockNumber.Int64()),
@@ -350,7 +350,7 @@ func TestGetLogs_LogQueryLimitUnlimited(t *testing.T) {
// returns an error when the requested logCount exceeds getLogsMaxResults.
func TestGetLatestLogs_LogCountExceedsMaxResults(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewErigonAPI(newBaseApiWithLimits(m, 0, 5, 0), m.DB, nil)
+ api := NewErigonAPI(newBaseApiWithLimits(m, 0, 5, 0), m.OverlayDB(), nil)
_, err := api.GetLatestLogs(context.Background(), filters.FilterCriteria{}, filters.LogFilterOptions{
LogCount: 10,
})
@@ -362,7 +362,7 @@ func TestGetLatestLogs_LogCountExceedsMaxResults(t *testing.T) {
// returns an error when the requested blockCount exceeds rangeLimit.
func TestGetLatestLogs_BlockCountExceedsRangeLimit(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewErigonAPI(newBaseApiWithLimits(m, 5, 0, 0), m.DB, nil)
+ api := NewErigonAPI(newBaseApiWithLimits(m, 5, 0, 0), m.OverlayDB(), nil)
_, err := api.GetLatestLogs(context.Background(), filters.FilterCriteria{}, filters.LogFilterOptions{
BlockCount: 10,
})
@@ -375,7 +375,7 @@ func TestGetLatestLogs_BlockCountExceedsRangeLimit(t *testing.T) {
// no logCount/blockCount is specified.
func TestGetLatestLogs_ExplicitRangeExceedsLimit(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewErigonAPI(newBaseApiWithLimits(m, 5, 0, 0), m.DB, nil)
+ api := NewErigonAPI(newBaseApiWithLimits(m, 5, 0, 0), m.OverlayDB(), nil)
_, err := api.GetLatestLogs(context.Background(), filters.FilterCriteria{
FromBlock: big.NewInt(0),
ToBlock: big.NewInt(10),
@@ -390,7 +390,7 @@ func TestGetLatestLogs_ExplicitRangeExceedsLimit(t *testing.T) {
func TestGetLatestLogs_ExplicitRangeWithLogCount_NoRangeCheck(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
// rangeLimit=5, but logCount is set so the range check should be skipped.
- api := NewErigonAPI(newBaseApiWithLimits(m, 5, 0, 0), m.DB, nil)
+ api := NewErigonAPI(newBaseApiWithLimits(m, 5, 0, 0), m.OverlayDB(), nil)
_, err := api.GetLatestLogs(context.Background(), filters.FilterCriteria{
FromBlock: big.NewInt(0),
ToBlock: big.NewInt(10),
@@ -406,7 +406,7 @@ func TestGetLatestLogs_ExplicitRangeWithLogCount_NoRangeCheck(t *testing.T) {
func TestGetLatestLogs_ExplicitRangeWithBlockCount_NoRangeCheck(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
// rangeLimit=5, blockCount=3 (≤5 so ok), explicit range 0-10 is skipped.
- api := NewErigonAPI(newBaseApiWithLimits(m, 5, 0, 0), m.DB, nil)
+ api := NewErigonAPI(newBaseApiWithLimits(m, 5, 0, 0), m.OverlayDB(), nil)
_, err := api.GetLatestLogs(context.Background(), filters.FilterCriteria{
FromBlock: big.NewInt(0),
ToBlock: big.NewInt(10),
@@ -428,7 +428,7 @@ func mockWithGenerator(t *testing.T, blocks int, generator func(int, *blockgen.B
execmoduletester.WithKey(testKey),
)
if blocks > 0 {
- chain, _ := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, blocks, generator)
+ chain, _ := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, blocks, generator, m.PublishedSD())
err := m.InsertChain(chain)
require.NoError(t, err)
}
diff --git a/rpc/jsonrpc/erigon_system.go b/rpc/jsonrpc/erigon_system.go
index ed7eb3751e7..0c109c0ec07 100644
--- a/rpc/jsonrpc/erigon_system.go
+++ b/rpc/jsonrpc/erigon_system.go
@@ -88,7 +88,7 @@ func (api *ErigonImpl) BlockNumber(ctx context.Context, rpcBlockNumPtr *rpc.Bloc
return 0, err
}
default:
- blockNum, err = rpchelper.GetLatestExecutedBlockNumber(tx)
+ blockNum, err = rpchelper.GetLatestExecutedBlockNumber(overlayTx)
if err != nil {
return 0, err
}
diff --git a/rpc/jsonrpc/eth_api.go b/rpc/jsonrpc/eth_api.go
index fcdf6ccf729..12db7717d30 100644
--- a/rpc/jsonrpc/eth_api.go
+++ b/rpc/jsonrpc/eth_api.go
@@ -452,7 +452,7 @@ func (api *BaseAPI) checkPruneField(tx kv.Tx, block uint64, field func(*prune.Mo
if !amount.Enabled() {
return nil
}
- latest, err := rpchelper.GetLatestBlockNumber(tx)
+ latest, err := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
if err != nil {
return err
}
diff --git a/rpc/jsonrpc/eth_api_test.go b/rpc/jsonrpc/eth_api_test.go
index 060d9900935..8dbb5dcbb5f 100644
--- a/rpc/jsonrpc/eth_api_test.go
+++ b/rpc/jsonrpc/eth_api_test.go
@@ -74,7 +74,7 @@ func newTraceApiForTest(m *execmoduletester.ExecModuleTester) *TraceAPIImpl {
}
func newDebugApiForTest(m *execmoduletester.ExecModuleTester) *DebugAPIImpl {
- return NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, nil, &rpccfg.DebugApiConfig{})
+ return NewPrivateDebugAPI(newBaseApiForTest(m), m.OverlayDB(), nil, &rpccfg.DebugApiConfig{})
}
func TestNewBaseApiEvmCallTimeout(t *testing.T) {
@@ -100,7 +100,7 @@ func TestGetBalanceChangesInBlock(t *testing.T) {
assert := assert.New(t)
myBlockNum := rpc.BlockNumberOrHashWithNumber(0)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- db := m.DB
+ db := m.OverlayDB()
api := NewErigonAPI(newBaseApiForTest(m), db, nil)
balances, err := api.GetBalanceChangesInBlock(context.Background(), myBlockNum)
if err != nil {
@@ -121,7 +121,7 @@ func TestGetBalanceChangesInBlock(t *testing.T) {
func TestGetTransactionReceipt(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
stateCache := kvcache.New(kvcache.DefaultCoherentConfig)
- api := newEthApiForTest(NewBaseApi(nil, stateCache, m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs}), m.DB, nil, nil)
+ api := newEthApiForTest(NewBaseApi(nil, stateCache, m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs}), m.OverlayDB(), nil, nil)
// Call GetTransactionReceipt for transaction which is not in the database
if _, err := api.GetTransactionReceipt(context.Background(), common.Hash{}); err != nil {
t.Errorf("calling GetTransactionReceipt with empty hash: %v", err)
@@ -130,7 +130,7 @@ func TestGetTransactionReceipt(t *testing.T) {
func TestGetTransactionReceiptUnprotected(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
// Call GetTransactionReceipt for un-protected transaction
if _, err := api.GetTransactionReceipt(context.Background(), common.HexToHash("0x3f3cb8a0e13ed2481f97f53f7095b9cbc78b6ffb779f2d3e565146371a8830ea")); err != nil {
t.Errorf("calling GetTransactionReceipt for unprotected tx: %v", err)
@@ -142,7 +142,7 @@ func TestGetTransactionReceiptUnprotected(t *testing.T) {
func TestGetStorageAt_ByBlockNumber_WithRequireCanonicalDefault(t *testing.T) {
assert := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
result, err := api.GetStorageAt(context.Background(), addr, "0x0", bnhPtr(rpc.BlockNumberOrHashWithNumber(0)))
@@ -156,7 +156,7 @@ func TestGetStorageAt_ByBlockNumber_WithRequireCanonicalDefault(t *testing.T) {
func TestGetStorageAt_ByBlockHash_WithRequireCanonicalDefault(t *testing.T) {
assert := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
result, err := api.GetStorageAt(context.Background(), addr, "0x0", bnhPtr(rpc.BlockNumberOrHashWithHash(m.Genesis.Hash(), false)))
@@ -170,7 +170,7 @@ func TestGetStorageAt_ByBlockHash_WithRequireCanonicalDefault(t *testing.T) {
func TestGetStorageAt_ByBlockHash_WithRequireCanonicalTrue(t *testing.T) {
assert := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
result, err := api.GetStorageAt(context.Background(), addr, "0x0", bnhPtr(rpc.BlockNumberOrHashWithHash(m.Genesis.Hash(), true)))
@@ -186,11 +186,11 @@ func TestGetStorageAt_ByBlockHash_WithRequireCanonicalDefault_BlockNotFoundError
t.Skip("slow test")
}
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
offChain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, func(i int, block *blockgen.BlockGen) {
- })
+ }, m.PublishedSD())
if err != nil {
t.Fatal(err)
}
@@ -207,11 +207,11 @@ func TestGetStorageAt_ByBlockHash_WithRequireCanonicalDefault_BlockNotFoundError
func TestGetStorageAt_ByBlockHash_WithRequireCanonicalTrue_BlockNotFoundError(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
offChain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, func(i int, block *blockgen.BlockGen) {
- })
+ }, m.PublishedSD())
if err != nil {
t.Fatal(err)
}
@@ -229,7 +229,7 @@ func TestGetStorageAt_ByBlockHash_WithRequireCanonicalTrue_BlockNotFoundError(t
func TestGetStorageAt_ByBlockHash_WithRequireCanonicalDefault_NonCanonicalBlock(t *testing.T) {
assert := assert.New(t)
m, _, orphanedChain := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
orphanedBlock := orphanedChain[0].Blocks[0]
@@ -248,7 +248,7 @@ func TestGetStorageAt_ByBlockHash_WithRequireCanonicalDefault_NonCanonicalBlock(
func TestGetStorageAt_ByBlockHash_WithRequireCanonicalTrue_NonCanonicalBlock(t *testing.T) {
m, _, orphanedChain := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
orphanedBlock := orphanedChain[0].Blocks[0]
@@ -264,7 +264,7 @@ func TestGetStorageAt_ByBlockHash_WithRequireCanonicalTrue_NonCanonicalBlock(t *
func TestCall_ByBlockHash_WithRequireCanonicalDefault_NonCanonicalBlock(t *testing.T) {
m, _, orphanedChain := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -290,7 +290,7 @@ func TestCall_ByBlockHash_WithRequireCanonicalDefault_NonCanonicalBlock(t *testi
func TestCall_ByBlockHash_WithRequireCanonicalTrue_NonCanonicalBlock(t *testing.T) {
m, _, orphanedChain := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -324,7 +324,7 @@ func (m mockBridgeReader) EventTxnLookup(context.Context, common.Hash) (uint64,
func TestGetStorageValues_HappyPath(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr1 := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
slot0 := common.Hash{}
@@ -348,7 +348,7 @@ func TestGetStorageValues_HappyPath(t *testing.T) {
func TestGetStorageValues_MultipleAddresses(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr1 := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
addr2 := common.HexToAddress("0x1000000000000000000000000000000000000001")
addr3 := common.HexToAddress("0x2000000000000000000000000000000000000002")
@@ -381,7 +381,7 @@ func TestGetStorageValues_MultipleAddresses(t *testing.T) {
func TestGetStorageValues_MissingSlotReturnsZero(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr1 := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
@@ -399,7 +399,7 @@ func TestGetStorageValues_MissingSlotReturnsZero(t *testing.T) {
func TestGetStorageValues_EmptyRequestReturnsError(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
@@ -414,7 +414,7 @@ func TestGetStorageValues_EmptyRequestReturnsError(t *testing.T) {
func TestGetStorageValues_ExceedingSlotLimitReturnsError(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr1 := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
@@ -434,7 +434,7 @@ func TestGetStorageValues_ExceedingSlotLimitReturnsError(t *testing.T) {
func TestGetStorageValues_ByBlockHash_NonCanonicalBlock(t *testing.T) {
m, _, orphanedChain := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr1 := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
orphanedBlock := orphanedChain[0].Blocks[0]
@@ -455,7 +455,7 @@ func TestGetStorageValues_ByBlockHash_NonCanonicalBlock(t *testing.T) {
func TestGetStorageValues_ByBlockHash_WithRequireCanonicalTrue_NonCanonicalBlock(t *testing.T) {
m, _, orphanedChain := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr1 := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
orphanedBlock := orphanedChain[0].Blocks[0]
@@ -476,7 +476,7 @@ func TestGetStorageValues_ByBlockHash_WithRequireCanonicalTrue_NonCanonicalBlock
func TestGetStorageValues_PrunedBlockReturnsError(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
addr1 := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
@@ -502,7 +502,7 @@ func bnhPtr(b rpc.BlockNumberOrHash) *rpc.BlockNumberOrHash { return &b }
func TestStateMethods_OmittedBlockDefaultsToLatest(t *testing.T) {
a := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
ctx := context.Background()
addr := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
diff --git a/rpc/jsonrpc/eth_block.go b/rpc/jsonrpc/eth_block.go
index 0bcc3e08ca1..9d0603ecaad 100644
--- a/rpc/jsonrpc/eth_block.go
+++ b/rpc/jsonrpc/eth_block.go
@@ -433,7 +433,9 @@ func (api *APIImpl) GetBlockTransactionCountByNumber(ctx context.Context, blockN
return nil, err
}
- latestBlockNumber, err := rpchelper.GetLatestBlockNumber(tx)
+ // Overlay-wrap so a freshly-FCU'd block whose background commit is still
+ // in flight is not mistaken for a future block (which would return null).
+ latestBlockNumber, err := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/eth_block_test.go b/rpc/jsonrpc/eth_block_test.go
index 1e1f480af52..0300eb2d72a 100644
--- a/rpc/jsonrpc/eth_block_test.go
+++ b/rpc/jsonrpc/eth_block_test.go
@@ -179,7 +179,7 @@ func TestGetBlockAccessListRegeneratesPrunedBAL(t *testing.T) {
// Gets the latest block number with the latest tag
func TestGetBlockByNumberWithLatestTag(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
b, err := api.GetBlockByNumber(context.Background(), rpc.LatestBlockNumber, false)
expected := common.HexToHash("0x9c47d5780744fa24ccdb1543a9b715e53431d5560b9e460b8b7a68f7c58310ae")
if err != nil {
@@ -191,16 +191,18 @@ func TestGetBlockByNumberWithLatestTag(t *testing.T) {
func TestGetBlockByNumberWithLatestTag_WithHeadHashInDb(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
ctx := context.Background()
+ latestBlockHash := common.HexToHash("0x6804117de2f3e6ee32953e78ced1db7b20214e0d8c745a03b8fecf7cc8ee76ef")
+ var latestBlock *types.Block
+ require.NoError(t, m.OverlayDB().View(ctx, func(rtx kv.Tx) (err error) {
+ latestBlock, err = m.BlockReader.BlockByHash(ctx, rtx, latestBlockHash)
+ return err
+ }))
+ require.NotNil(t, latestBlock, "couldn't retrieve latest block")
+
tx, err := m.DB.BeginRw(ctx)
require.NoError(t, err)
defer tx.Rollback()
- latestBlockHash := common.HexToHash("0x6804117de2f3e6ee32953e78ced1db7b20214e0d8c745a03b8fecf7cc8ee76ef")
- latestBlock, err := m.BlockReader.BlockByHash(ctx, tx, latestBlockHash)
- if err != nil {
- tx.Rollback()
- t.Errorf("couldn't retrieve latest block")
- }
rawdb.WriteHeaderNumber(tx, latestBlockHash, latestBlock.NonceU64())
rawdb.WriteForkchoiceHead(tx, latestBlockHash)
if safedHeadBlock := rawdb.ReadForkchoiceHead(tx); safedHeadBlock == (common.Hash{}) {
@@ -239,7 +241,7 @@ func TestGetBlockByNumberWithPendingTag(t *testing.T) {
RplBlock: rlpBlock,
})
- api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.OverlayDB(), nil, nil)
b, err := api.GetBlockByNumber(context.Background(), rpc.PendingBlockNumber, false)
if err != nil {
t.Errorf("error getting block number with pending tag: %s", err)
@@ -251,7 +253,7 @@ func TestGetBlockByNumberWithPendingTag(t *testing.T) {
func TestGetBlockByNumber_WithFinalizedTag_NoFinalizedBlockInDb(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
ctx := context.Background()
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
_, err := api.GetBlockByNumber(ctx, rpc.FinalizedBlockNumber, false)
if err != nil {
var customErr *rpc.CustomError
@@ -265,16 +267,18 @@ func TestGetBlockByNumber_WithFinalizedTag_NoFinalizedBlockInDb(t *testing.T) {
func TestGetBlockByNumber_WithFinalizedTag_WithFinalizedBlockInDb(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
ctx := context.Background()
+ latestBlockHash := common.HexToHash("0x6804117de2f3e6ee32953e78ced1db7b20214e0d8c745a03b8fecf7cc8ee76ef")
+ var latestBlock *types.Block
+ require.NoError(t, m.OverlayDB().View(ctx, func(rtx kv.Tx) (err error) {
+ latestBlock, err = m.BlockReader.BlockByHash(ctx, rtx, latestBlockHash)
+ return err
+ }))
+ require.NotNil(t, latestBlock, "couldn't retrieve latest block")
+
tx, err := m.DB.BeginRw(ctx)
require.NoError(t, err)
defer tx.Rollback()
- latestBlockHash := common.HexToHash("0x6804117de2f3e6ee32953e78ced1db7b20214e0d8c745a03b8fecf7cc8ee76ef")
- latestBlock, err := m.BlockReader.BlockByHash(ctx, tx, latestBlockHash)
- if err != nil {
- tx.Rollback()
- t.Errorf("couldn't retrieve latest block")
- }
rawdb.WriteHeaderNumber(tx, latestBlockHash, latestBlock.NonceU64())
rawdb.WriteForkchoiceFinalized(tx, latestBlockHash)
if safedFinalizedBlock := rawdb.ReadForkchoiceFinalized(tx); safedFinalizedBlock == (common.Hash{}) {
@@ -295,7 +299,7 @@ func TestGetBlockByNumber_WithFinalizedTag_WithFinalizedBlockInDb(t *testing.T)
func TestGetBlockByNumber_WithSafeTag_NoSafeBlockInDb(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
ctx := context.Background()
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
_, err := api.GetBlockByNumber(ctx, rpc.SafeBlockNumber, false)
if err != nil {
var customErr *rpc.CustomError
@@ -309,16 +313,18 @@ func TestGetBlockByNumber_WithSafeTag_NoSafeBlockInDb(t *testing.T) {
func TestGetBlockByNumber_WithSafeTag_WithSafeBlockInDb(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
ctx := context.Background()
+ latestBlockHash := common.HexToHash("0x6804117de2f3e6ee32953e78ced1db7b20214e0d8c745a03b8fecf7cc8ee76ef")
+ var latestBlock *types.Block
+ require.NoError(t, m.OverlayDB().View(ctx, func(rtx kv.Tx) (err error) {
+ latestBlock, err = m.BlockReader.BlockByHash(ctx, rtx, latestBlockHash)
+ return err
+ }))
+ require.NotNil(t, latestBlock, "couldn't retrieve latest block")
+
tx, err := m.DB.BeginRw(ctx)
require.NoError(t, err)
defer tx.Rollback()
- latestBlockHash := common.HexToHash("0x6804117de2f3e6ee32953e78ced1db7b20214e0d8c745a03b8fecf7cc8ee76ef")
- latestBlock, err := m.BlockReader.BlockByHash(ctx, tx, latestBlockHash)
- if err != nil {
- tx.Rollback()
- t.Errorf("couldn't retrieve latest block")
- }
rawdb.WriteHeaderNumber(tx, latestBlockHash, latestBlock.NonceU64())
rawdb.WriteForkchoiceSafe(tx, latestBlockHash)
if safedSafeBlock := rawdb.ReadForkchoiceSafe(tx); safedSafeBlock == (common.Hash{}) {
@@ -340,7 +346,7 @@ func TestGetBlockTransactionCountByHash(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
ctx := context.Background()
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
blockHash := common.HexToHash("0x6804117de2f3e6ee32953e78ced1db7b20214e0d8c745a03b8fecf7cc8ee76ef")
tx, err := m.DB.BeginRw(ctx)
@@ -372,7 +378,7 @@ func TestGetBlockTransactionCountByHash(t *testing.T) {
func TestGetBlockTransactionCountByHash_ZeroTx(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
ctx := context.Background()
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
blockHash := common.HexToHash("0x5883164d4100b95e1d8e931b8b9574586a1dea7507941e6ad3c1e3a2591485fd")
tx, err := m.DB.BeginRw(ctx)
@@ -404,7 +410,7 @@ func TestGetBlockTransactionCountByHash_ZeroTx(t *testing.T) {
func TestGetBlockTransactionCountByNumber(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
ctx := context.Background()
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
blockHash := common.HexToHash("0x6804117de2f3e6ee32953e78ced1db7b20214e0d8c745a03b8fecf7cc8ee76ef")
tx, err := m.DB.BeginRw(ctx)
@@ -436,7 +442,7 @@ func TestGetBlockTransactionCountByNumber(t *testing.T) {
func TestGetBlockTransactionCountByNumber_ZeroTx(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
ctx := context.Background()
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
blockHash := common.HexToHash("0x5883164d4100b95e1d8e931b8b9574586a1dea7507941e6ad3c1e3a2591485fd")
@@ -478,7 +484,7 @@ func TestGetBlockByNumber_BlockPruneGating(t *testing.T) {
setup := func(t *testing.T, pm prune.Mode) *APIImpl {
t.Helper()
m := execmoduletester.New(t, execmoduletester.WithPruneMode(pm))
- c, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, chainSize, func(_ int, _ *blockgen.BlockGen) {})
+ c, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, chainSize, func(_ int, _ *blockgen.BlockGen) {}, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(c))
@@ -490,7 +496,7 @@ func TestGetBlockByNumber_BlockPruneGating(t *testing.T) {
require.NoError(t, err)
require.NoError(t, tx.Commit())
- return newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ return newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
}
legacyFull := prune.Mode{
diff --git a/rpc/jsonrpc/eth_call.go b/rpc/jsonrpc/eth_call.go
index c52c6d84060..40a2116ec82 100644
--- a/rpc/jsonrpc/eth_call.go
+++ b/rpc/jsonrpc/eth_call.go
@@ -87,7 +87,7 @@ func (api *APIImpl) Call(ctx context.Context, args ethapi2.CallArgs, requestedBl
var tx kv.TemporalTx = roTx
if api.filters != nil {
if sd := api.filters.LatestSD(); sd != nil {
- if overlayTx := sd.BlockOverlayTemporalTx(roTx); overlayTx != nil {
+ if overlayTx := sd.OverlayTemporalTx(roTx); overlayTx != nil {
tx = overlayTx
}
}
@@ -487,7 +487,7 @@ func (api *APIImpl) getProof(ctx context.Context, roTx kv.TemporalTx, address co
defer domains.Close()
sdCtx := domains.GetCommitmentContext()
- latestBlock, err := rpchelper.GetLatestBlockNumber(roTx)
+ latestBlock, err := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(roTx))
if err != nil {
return nil, err
}
@@ -509,6 +509,19 @@ func (api *APIImpl) getProof(ctx context.Context, roTx kv.TemporalTx, address co
if _, _, err := domains.SeekCommitment(context.Background(), roTx); err != nil {
return nil, err
}
+ } else if p, ok := tx.(interface {
+ PublishedSharedDomains() *execctx.SharedDomains
+ }); ok {
+ // Requested block is the tip. Under background commit its commitment
+ // still lives in the published SharedDomains, so chain the fresh domains
+ // to it and seek commitment through the chain to build the proof against
+ // the in-flight trie root.
+ if psd := p.PublishedSharedDomains(); psd != nil {
+ domains.SetParent(psd)
+ if _, _, err := domains.SeekCommitment(context.Background(), roTx); err != nil {
+ return nil, err
+ }
+ }
}
// touch account
diff --git a/rpc/jsonrpc/eth_callMany_test.go b/rpc/jsonrpc/eth_callMany_test.go
index 1d2fdd3fa9d..b086d8e51da 100644
--- a/rpc/jsonrpc/eth_callMany_test.go
+++ b/rpc/jsonrpc/eth_callMany_test.go
@@ -67,8 +67,8 @@ func TestSetupEVMTimeoutCancelsEVMStoredAfterExpiry(t *testing.T) {
func TestCallManyEmptyBundles(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
baseApi := newBaseApiForTest(m)
- api := newEthApiForTest(baseApi, m.DB, nil, nil)
- debugApi := NewPrivateDebugAPI(baseApi, m.DB, nil, &rpccfg.DebugApiConfig{GasCap: 5000000})
+ api := newEthApiForTest(baseApi, m.OverlayDB(), nil, nil)
+ debugApi := NewPrivateDebugAPI(baseApi, m.OverlayDB(), nil, &rpccfg.DebugApiConfig{GasCap: 5000000})
ctx := context.Background()
txIndex := -1
diff --git a/rpc/jsonrpc/eth_call_test.go b/rpc/jsonrpc/eth_call_test.go
index 58779dbad54..e4dfab76c66 100644
--- a/rpc/jsonrpc/eth_call_test.go
+++ b/rpc/jsonrpc/eth_call_test.go
@@ -217,7 +217,7 @@ func newTestEthAPIWithFilters(t *testing.T, m *execmoduletester.ExecModuleTester
ctx, conn := rpcdaemontest.CreateTestGrpcConn(t, execmoduletester.New(t))
mining := txpoolproto.NewMiningClient(conn)
filters := rpchelper.New(ctx, rpchelper.DefaultFiltersConfig, nil, nil, mining, func() {}, m.Log, nil)
- return newEthApiForTest(newBaseApiWithFiltersForTest(filters, stateCache, m), m.DB, nil, nil)
+ return newEthApiForTest(newBaseApiWithFiltersForTest(filters, stateCache, m), m.OverlayDB(), nil, nil)
}
type stubTxPoolClient struct{ txpoolproto.TxpoolClient }
@@ -228,7 +228,7 @@ func (stubTxPoolClient) Nonce(context.Context, *txpoolproto.NonceRequest, ...grp
func TestCreateAccessListContractCreationWithoutFromDoesNotPanic(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
var (
res *accessListResult
@@ -244,7 +244,7 @@ func TestCreateAccessListContractCreationWithoutFromDoesNotPanic(t *testing.T) {
func TestEthCallNonCanonical(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
stateCache := kvcache.New(kvcache.DefaultCoherentConfig)
- api := newEthApiForTest(newBaseApiWithFiltersForTest(nil, stateCache, m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiWithFiltersForTest(nil, stateCache, m), m.OverlayDB(), nil, nil)
var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
blockNumberOrHash := rpc.BlockNumberOrHashWithHash(common.HexToHash("0x3fcb7c0d4569fddc89cbea54b42f163e0c789351d98810a513895ab44b47020b"), true)
@@ -263,7 +263,7 @@ func TestEthCallToPrunedBlock(t *testing.T) {
m, bankAddress, contractAddress, _ := chainWithDeployedContract(t)
doPrune(t, m.DB, pruneTo)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
callData := hexutil.MustDecode("0x2e64cec1")
callDataBytes := hexutil.Bytes(callData)
@@ -293,7 +293,7 @@ func TestGetProof(t *testing.T) {
RpcTxSyncDefaultTimeout: 20 * time.Second,
RpcTxSyncMaxTimeout: 1 * time.Minute,
}
- api := NewEthAPI(newBaseApiForTest(m), m.DB, nil, nil, nil, cfg, log.New())
+ api := NewEthAPI(newBaseApiForTest(m), m.OverlayDB(), nil, nil, nil, cfg, log.New())
key := func(b byte) hexutil.Bytes {
result := common.Hash{}
@@ -410,7 +410,7 @@ func TestGetProof(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, proof)
- tx, err := m.DB.BeginTemporalRo(context.Background())
+ tx, err := m.OverlayDB().BeginTemporalRo(context.Background())
require.NoError(t, err)
defer tx.Rollback()
header, err := api.headerByNumber(context.Background(), rpc.BlockNumber(tt.blockNum), tx)
@@ -474,10 +474,10 @@ func TestGetProofGenesisPrunedCommitmentHistory(t *testing.T) {
func TestGetBlockByTimestampLatestTime(t *testing.T) {
ctx := context.Background()
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- tx, err := m.DB.BeginTemporalRo(ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(ctx)
require.NoError(t, err)
defer tx.Rollback()
- api := NewErigonAPI(newBaseApiForTest(m), m.DB, nil)
+ api := NewErigonAPI(newBaseApiForTest(m), m.OverlayDB(), nil)
latestBlock, err := m.BlockReader.CurrentBlock(tx)
require.NoError(t, err)
@@ -501,10 +501,10 @@ func TestGetBlockByTimestampLatestTime(t *testing.T) {
func TestGetBlockByTimestampOldestTime(t *testing.T) {
ctx := context.Background()
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- tx, err := m.DB.BeginTemporalRo(ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(ctx)
require.NoError(t, err)
defer tx.Rollback()
- api := NewErigonAPI(newBaseApiForTest(m), m.DB, nil)
+ api := NewErigonAPI(newBaseApiForTest(m), m.OverlayDB(), nil)
oldestBlock, err := m.BlockReader.BlockByNumber(m.Ctx, tx, 0)
require.NoError(t, err)
@@ -529,10 +529,10 @@ func TestGetBlockByTimestampOldestTime(t *testing.T) {
func TestGetBlockByTimeHigherThanLatestBlock(t *testing.T) {
ctx := context.Background()
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- tx, err := m.DB.BeginTemporalRo(ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(ctx)
require.NoError(t, err)
defer tx.Rollback()
- api := NewErigonAPI(newBaseApiForTest(m), m.DB, nil)
+ api := NewErigonAPI(newBaseApiForTest(m), m.OverlayDB(), nil)
latestBlock, err := m.BlockReader.CurrentBlock(tx)
require.NoError(t, err)
@@ -557,10 +557,10 @@ func TestGetBlockByTimeHigherThanLatestBlock(t *testing.T) {
func TestGetBlockByTimeMiddle(t *testing.T) {
ctx := context.Background()
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- tx, err := m.DB.BeginTemporalRo(ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(ctx)
require.NoError(t, err)
defer tx.Rollback()
- api := NewErigonAPI(newBaseApiForTest(m), m.DB, nil)
+ api := NewErigonAPI(newBaseApiForTest(m), m.OverlayDB(), nil)
currentHeader := rawdb.ReadCurrentHeader(tx)
oldestHeader, err := api._blockReader.HeaderByNumber(ctx, tx, 0)
@@ -590,10 +590,10 @@ func TestGetBlockByTimeMiddle(t *testing.T) {
func TestGetBlockByTimestamp(t *testing.T) {
ctx := context.Background()
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- tx, err := m.DB.BeginTemporalRo(ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(ctx)
require.NoError(t, err)
defer tx.Rollback()
- api := NewErigonAPI(newBaseApiForTest(m), m.DB, nil)
+ api := NewErigonAPI(newBaseApiForTest(m), m.OverlayDB(), nil)
highestBlockNumber := rawdb.ReadCurrentHeader(tx).Number
pickedBlock, err := m.BlockReader.BlockByNumber(m.Ctx, tx, highestBlockNumber.Uint64()/3)
@@ -767,7 +767,7 @@ func chainWithDeployedContractAndConfig(t *testing.T, cfg *chain.Config) (*execm
_, fillerPublicKeys, err := generatePseudoRandomECDSAKeyPairs(rng, nFillerAccounts)
require.NoError(t, err)
- db := m.DB
+ db := m.OverlayDB()
var contractAddr common.Address
@@ -869,7 +869,7 @@ func chainWithDeployedContractAndConfig(t *testing.T, cfg *chain.Config) (*execm
case 5:
// empty block
}
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(chain)
diff --git a/rpc/jsonrpc/eth_fill_transaction_test.go b/rpc/jsonrpc/eth_fill_transaction_test.go
index 3334a1b63a2..d790dd4d9e4 100644
--- a/rpc/jsonrpc/eth_fill_transaction_test.go
+++ b/rpc/jsonrpc/eth_fill_transaction_test.go
@@ -73,7 +73,7 @@ func newLondonApiForTest(t *testing.T) *APIImpl {
Ethash: new(chain.EthashConfig),
}
m := execmoduletester.New(t, execmoduletester.WithChainConfig(londonCfg))
- return newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ return newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
}
func testJsonAuthorization(addr common.Address) types.JsonAuthorization {
@@ -89,7 +89,7 @@ func testJsonAuthorization(addr common.Address) types.JsonAuthorization {
func TestFillTransactionFillsDefaults(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -108,7 +108,7 @@ func TestFillTransactionFillsDefaults(t *testing.T) {
func TestFillTransactionConflictingFees(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -127,7 +127,7 @@ func TestFillTransactionConflictingFees(t *testing.T) {
func TestFillTransactionChainIDMismatch(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -144,7 +144,7 @@ func TestFillTransactionChainIDMismatch(t *testing.T) {
func TestFillTransactionContractCreationNoData(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
@@ -157,7 +157,7 @@ func TestFillTransactionContractCreationNoData(t *testing.T) {
func TestFillTransactionNoFrom(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -171,7 +171,7 @@ func TestFillTransactionNoFrom(t *testing.T) {
func TestFillTransactionExplicitNoncePreserved(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -188,7 +188,7 @@ func TestFillTransactionExplicitNoncePreserved(t *testing.T) {
func TestFillTransactionExplicitGasPreserved(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -207,7 +207,7 @@ func TestFillTransactionBlobPreCancun(t *testing.T) {
// TestChainBerlinConfig has no Cancun (ExcessBlobGas == nil on head).
// A blob tx request must return a clear error, not panic.
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -225,7 +225,7 @@ func TestFillTransactionBlobPreCancun(t *testing.T) {
func TestFillTransactionBlobPreCancunExplicitBlobFee(t *testing.T) {
// Even with an explicit maxFeePerBlobGas, blob txs on a pre-Cancun chain must error.
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -245,7 +245,7 @@ func TestFillTransactionBlobPreCancunExplicitBlobFee(t *testing.T) {
func TestFillTransactionPoolErrorPropagates(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, errPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), errPoolClient{}, nil)
from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -324,7 +324,7 @@ func TestFillTransactionEmptyAuthorizationList(t *testing.T) {
func TestFillTransactionUserGasAboveCapPreserved(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -343,7 +343,7 @@ func TestFillTransactionUserGasAboveCapPreserved(t *testing.T) {
func TestFillTransactionPoolNonce(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
const pendingNonce = uint64(5)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, fixedNoncePoolClient{nonce: pendingNonce}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), fixedNoncePoolClient{nonce: pendingNonce}, nil)
from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
@@ -448,7 +448,7 @@ func TestFillTransactionBlobFeeUsesHeadExcess(t *testing.T) {
}
m := execmoduletester.New(t, execmoduletester.WithGenesisSpec(gspec))
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), stubTxPoolClient{}, nil)
to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
gas := hexutil.Uint64(21000)
diff --git a/rpc/jsonrpc/eth_filters_test.go b/rpc/jsonrpc/eth_filters_test.go
index 4534b2bf863..46c0adfb2b8 100644
--- a/rpc/jsonrpc/eth_filters_test.go
+++ b/rpc/jsonrpc/eth_filters_test.go
@@ -41,8 +41,10 @@ import (
"github.com/erigontech/erigon/rpc/rpchelper"
)
-func newBaseApiWithFiltersForTest(f *rpchelper.Filters, stateCache *kvcache.Coherent, m *execmoduletester.ExecModuleTester) *BaseAPI {
- return NewBaseApi(f, stateCache, m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs})
+func newBaseApiWithFiltersForTest(f *rpchelper.Filters, _ *kvcache.Coherent, m *execmoduletester.ExecModuleTester) *BaseAPI {
+ // Use the SD-wired state cache so reads observe the in-flight tip under
+ // background commit, not a plain coherent cache fed by stale notifications.
+ return NewBaseApi(f, m.StateCache, m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs})
}
func TestSubscriptionsRequireFiltersAndNotifier(t *testing.T) {
@@ -53,8 +55,8 @@ func TestSubscriptionsRequireFiltersAndNotifier(t *testing.T) {
stateCache := kvcache.New(kvcache.DefaultCoherentConfig)
apis := map[string]*APIImpl{
- "withFilters": newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.DB, nil, nil),
- "nilFilters": newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil),
+ "withFilters": newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.OverlayDB(), nil, nil),
+ "nilFilters": newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil),
}
for apiName, api := range apis {
// ctx carries no rpc notifier, so every subscription method must refuse
@@ -85,7 +87,7 @@ func TestNewFilters(t *testing.T) {
ctx, conn := rpcdaemontest.CreateTestGrpcConn(t, execmoduletester.New(t))
mining := txpoolproto.NewMiningClient(conn)
ff := rpchelper.New(ctx, rpchelper.DefaultFiltersConfig, nil, nil, mining, func() {}, m.Log, nil)
- api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.OverlayDB(), nil, nil)
ptf, err := api.NewPendingTransactionFilter(ctx)
assert.NoError(err)
@@ -166,7 +168,7 @@ func TestBlockFilterGetFilterChangesInitiallyEmpty(t *testing.T) {
ctx, conn := rpcdaemontest.CreateTestGrpcConn(t, execmoduletester.New(t))
mining := txpoolproto.NewMiningClient(conn)
ff := rpchelper.New(ctx, rpchelper.DefaultFiltersConfig, nil, nil, mining, func() {}, m.Log, nil)
- api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.OverlayDB(), nil, nil)
// Create a new block filter
bf, err := api.NewBlockFilter(ctx)
@@ -194,7 +196,7 @@ func TestCompositeFiltersGetFilterChangesInitiallyEmpty(t *testing.T) {
ctx, conn := rpcdaemontest.CreateTestGrpcConn(t, execmoduletester.New(t))
mining := txpoolproto.NewMiningClient(conn)
ff := rpchelper.New(ctx, rpchelper.DefaultFiltersConfig, nil, nil, mining, func() {}, m.Log, nil)
- api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.OverlayDB(), nil, nil)
// Create all three filter types
ptf, err := api.NewPendingTransactionFilter(ctx)
@@ -249,7 +251,7 @@ func TestPendingTxsFilterChangesReturnsAllBatches(t *testing.T) {
mining := txpoolproto.NewMiningClient(conn)
ff := rpchelper.New(ctx, rpchelper.DefaultFiltersConfig, nil, nil, mining, func() {}, m.Log, nil)
stateCache := kvcache.New(kvcache.DefaultCoherentConfig)
- api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.OverlayDB(), nil, nil)
ptf, err := api.NewPendingTransactionFilter(ctx)
require.NoError(t, err)
@@ -290,7 +292,7 @@ func TestGetFilterChangesReturnsFilterNotFoundForUnknownID(t *testing.T) {
ctx, conn := rpcdaemontest.CreateTestGrpcConn(t, execmoduletester.New(t))
mining := txpoolproto.NewMiningClient(conn)
ff := rpchelper.New(ctx, rpchelper.DefaultFiltersConfig, nil, nil, mining, func() {}, m.Log, nil)
- api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiWithFiltersForTest(ff, stateCache, m), m.OverlayDB(), nil, nil)
// Use a bogus id that does not correspond to any subscription
_, err := api.GetFilterChanges(ctx, "0xdeadbeefcafebabe")
diff --git a/rpc/jsonrpc/eth_receipts.go b/rpc/jsonrpc/eth_receipts.go
index 91e995147e6..0c0c22b0a45 100644
--- a/rpc/jsonrpc/eth_receipts.go
+++ b/rpc/jsonrpc/eth_receipts.go
@@ -228,7 +228,7 @@ func (api *APIImpl) GetLogs(ctx context.Context, crit filters.FilterCriteria) (t
return nil, &rpc.CustomError{Message: errInvalidBlockRange, Code: rpc.ErrCodeInvalidParams}
}
if end > roaring.MaxUint32 {
- latest, err := rpchelper.GetLatestBlockNumber(tx)
+ latest, err := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
if err != nil {
return nil, err
}
@@ -240,7 +240,7 @@ func (api *APIImpl) GetLogs(ctx context.Context, crit filters.FilterCriteria) (t
// Check if the requested blocks have been executed.
// This prevents returning empty results when blocks exist but haven't been executed yet.
- latestExecuted, err := rpchelper.GetLatestExecutedBlockNumber(tx)
+ latestExecuted, err := rpchelper.GetLatestExecutedBlockNumber(api.filters.WithOverlay(tx))
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/eth_simulation.go b/rpc/jsonrpc/eth_simulation.go
index 2489a08993c..30ced60ca7a 100644
--- a/rpc/jsonrpc/eth_simulation.go
+++ b/rpc/jsonrpc/eth_simulation.go
@@ -129,7 +129,7 @@ func (api *APIImpl) SimulateV1(ctx context.Context, req SimulationRequest, block
if err != nil {
return nil, err
}
- latestBlockNumber, err := rpchelper.GetLatestBlockNumber(tx)
+ latestBlockNumber, err := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/eth_simulation_test.go b/rpc/jsonrpc/eth_simulation_test.go
index a53fde5b7c6..e20a4992f54 100644
--- a/rpc/jsonrpc/eth_simulation_test.go
+++ b/rpc/jsonrpc/eth_simulation_test.go
@@ -525,7 +525,7 @@ func TestSimulationRequestTypes(t *testing.T) {
func TestSimulateV1PopulatesMaxUsedGas(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
diff --git a/rpc/jsonrpc/eth_subscribe_test.go b/rpc/jsonrpc/eth_subscribe_test.go
index df521fe6483..2ce429a51b5 100644
--- a/rpc/jsonrpc/eth_subscribe_test.go
+++ b/rpc/jsonrpc/eth_subscribe_test.go
@@ -47,7 +47,7 @@ func TestEthSubscribe(t *testing.T) {
m := execmoduletester.New(t)
chain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 7, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
backendServer := privateapi.NewEthBackendServer(ctx, nil, m.DB, m.Notifications, m.BlockReader, nil, logger, builder.NewLatestBlockBuiltStore(), nil)
backendClient := direct.NewEthBackendClientDirect(backendServer)
@@ -81,7 +81,7 @@ func TestEthSubscribeReceipts(t *testing.T) {
tx, err := types.SignTx(types.NewTransaction(uint64(i), m.Address, uint256.NewInt(1), params.TxGas, uint256.NewInt(1), nil), *types.LatestSignerForChainID(m.ChainConfig.ChainID), m.Key)
require.NoError(t, err)
b.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
backendServer := privateapi.NewEthBackendServer(ctx, nil, m.DB, m.Notifications, m.BlockReader, nil, logger, builder.NewLatestBlockBuiltStore(), nil)
backendClient := direct.NewEthBackendClientDirect(backendServer)
diff --git a/rpc/jsonrpc/eth_system.go b/rpc/jsonrpc/eth_system.go
index 9292a6f23de..75e32cc068f 100644
--- a/rpc/jsonrpc/eth_system.go
+++ b/rpc/jsonrpc/eth_system.go
@@ -535,7 +535,7 @@ func (b *GasPriceOracleBackend) ChainConfig() *chain.Config {
}
func (b *GasPriceOracleBackend) GetLatestBlockNumber() (uint64, error) {
- return rpchelper.GetLatestBlockNumber(b.tx)
+ return rpchelper.GetLatestBlockNumber(b.baseApi.filters.WithOverlay(b.tx))
}
func (b *GasPriceOracleBackend) GetReceipts(ctx context.Context, block *types.Block) (types.Receipts, error) {
@@ -557,7 +557,7 @@ func (b *GasPriceOracleBackend) PendingBlockAndReceipts() (*types.Block, types.R
if block := b.baseApi.pendingBlock(); block != nil {
return block, nil
}
- latestNum, err := rpchelper.GetLatestBlockNumber(b.tx)
+ latestNum, err := rpchelper.GetLatestBlockNumber(b.baseApi.filters.WithOverlay(b.tx))
if err != nil {
return nil, nil
}
diff --git a/rpc/jsonrpc/eth_system_test.go b/rpc/jsonrpc/eth_system_test.go
index 24b0a004638..c70354f44c2 100644
--- a/rpc/jsonrpc/eth_system_test.go
+++ b/rpc/jsonrpc/eth_system_test.go
@@ -87,7 +87,7 @@ func TestCapabilities(t *testing.T) {
t.Fatal(txErr)
}
b.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(c))
@@ -105,13 +105,13 @@ func TestCapabilities(t *testing.T) {
}
require.NoError(t, tx.Commit())
- roTx, err := m.DB.BeginTemporalRo(ctx)
+ roTx, err := m.OverlayDB().BeginTemporalRo(ctx)
require.NoError(t, err)
defer roTx.Rollback()
head, err := stages.GetStageProgress(roTx, stages.Execution)
require.NoError(t, err)
- return newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil), head
+ return newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil), head
}
setupAPIWithMerge := func(t *testing.T, mergeAt uint64, persistReceipts bool) *APIImpl {
@@ -134,7 +134,7 @@ func TestCapabilities(t *testing.T) {
t.Fatal(txErr)
}
b.AddTx(tx)
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(c))
ctx := t.Context()
@@ -148,7 +148,7 @@ func TestCapabilities(t *testing.T) {
require.NoError(t, kvcfg.PersistReceipts.ForceWrite(dbTx, true))
}
require.NoError(t, dbTx.Commit())
- return newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ return newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
}
oldest := func(t *testing.T, f CapabilityField) uint64 {
@@ -409,7 +409,7 @@ func TestCapabilities(t *testing.T) {
require.NoError(t, rawdb.WriteDBCommitmentHistoryEnabled(dbTx, false))
require.NoError(t, dbTx.Commit())
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
result, err := api.Capabilities(ctx)
require.NoError(t, err)
require.Equal(t, uint64(0), uint64(result.Head.Number))
@@ -446,7 +446,7 @@ func TestGasPrice(t *testing.T) {
t.Run(testCase.description, func(t *testing.T) {
m := createGasPriceTestKV(t, testCase.chainSize)
defer m.DB.Close()
- eth := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ eth := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
ctx := context.Background()
result, err := eth.GasPrice(ctx)
@@ -600,7 +600,7 @@ func TestBaseFee(t *testing.T) {
Config: chain.TestChainBerlinConfig,
Alloc: types.GenesisAlloc{addr: {Balance: big.NewInt(math.MaxInt64)}},
}), execmoduletester.WithKey(key))
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
result, err := api.BaseFee(context.Background())
require.NoError(t, err)
require.Nil(t, result)
@@ -612,7 +612,7 @@ func TestBaseFee(t *testing.T) {
Config: chain.TestChainOsakaConfig,
Alloc: types.GenesisAlloc{addr: {Balance: big.NewInt(math.MaxInt64)}},
}), execmoduletester.WithKey(key))
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
result, err := api.BaseFee(context.Background())
require.NoError(t, err)
require.NotNil(t, result)
@@ -631,7 +631,7 @@ func TestBlobBaseFee(t *testing.T) {
Config: chain.TestChainBerlinConfig,
Alloc: types.GenesisAlloc{addr: {Balance: big.NewInt(math.MaxInt64)}},
}), execmoduletester.WithKey(key))
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
result, err := api.BlobBaseFee(context.Background())
require.NoError(t, err)
require.Nil(t, result)
@@ -643,7 +643,7 @@ func TestBlobBaseFee(t *testing.T) {
Config: chain.TestChainOsakaConfig,
Alloc: types.GenesisAlloc{addr: {Balance: big.NewInt(math.MaxInt64)}},
}), execmoduletester.WithKey(key))
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), nil, nil)
result, err := api.BlobBaseFee(context.Background())
require.NoError(t, err)
require.NotNil(t, result)
@@ -670,7 +670,7 @@ func createGasPriceTestKV(t *testing.T, chainSize int) *execmoduletester.ExecMod
t.Fatalf("failed to create tx: %v", txErr)
}
b.AddTx(tx)
- })
+ }, m.PublishedSD())
if err != nil {
t.Error(err)
}
diff --git a/rpc/jsonrpc/gen_traces_test.go b/rpc/jsonrpc/gen_traces_test.go
index a3fef0461ec..0d66442cffd 100644
--- a/rpc/jsonrpc/gen_traces_test.go
+++ b/rpc/jsonrpc/gen_traces_test.go
@@ -46,7 +46,7 @@ func TestGeneratedDebugApi(t *testing.T) {
m := rpcdaemontest.CreateTestExecModuleForTraces(t)
stateCache := kvcache.New(kvcache.DefaultCoherentConfig)
baseApi := NewBaseApi(nil, stateCache, m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs})
- api := NewPrivateDebugAPI(baseApi, m.DB, nil, &rpccfg.DebugApiConfig{})
+ api := NewPrivateDebugAPI(baseApi, m.OverlayDB(), nil, &rpccfg.DebugApiConfig{})
var buf bytes.Buffer
stream := jsonstream.New(jsoniter.NewStream(jsoniter.ConfigDefault, &buf, 4096))
callTracer := "callTracer"
diff --git a/rpc/jsonrpc/graphql_api.go b/rpc/jsonrpc/graphql_api.go
index f9b87c0e5e3..241817167e9 100644
--- a/rpc/jsonrpc/graphql_api.go
+++ b/rpc/jsonrpc/graphql_api.go
@@ -90,7 +90,7 @@ func (api *GraphQLAPIImpl) GetLatestBlockNumber(ctx context.Context) (uint64, er
return 0, err
}
defer tx.Rollback()
- return rpchelper.GetLatestBlockNumber(tx)
+ return rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
}
func (api *GraphQLAPIImpl) GetBlockNumberForTx(ctx context.Context, hash common.Hash) (uint64, bool, error) {
@@ -391,7 +391,7 @@ func (api *GraphQLAPIImpl) Call(ctx context.Context, blockNumber rpc.BlockNumber
var tx kv.TemporalTx = roTx
if api.filters != nil {
if sd := api.filters.LatestSD(); sd != nil {
- if overlayTx := sd.BlockOverlayTemporalTx(roTx); overlayTx != nil {
+ if overlayTx := sd.OverlayTemporalTx(roTx); overlayTx != nil {
tx = overlayTx
}
}
diff --git a/rpc/jsonrpc/otterscan_contract_creator_test.go b/rpc/jsonrpc/otterscan_contract_creator_test.go
index 0bdbc033cc4..77f2540bd88 100644
--- a/rpc/jsonrpc/otterscan_contract_creator_test.go
+++ b/rpc/jsonrpc/otterscan_contract_creator_test.go
@@ -27,7 +27,7 @@ import (
func TestGetContractCreator(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewOtterscanAPI(newBaseApiForTest(m), m.DB, 25)
+ api := NewOtterscanAPI(newBaseApiForTest(m), m.OverlayDB(), 25)
addr := common.HexToAddress("0x537e697c7ab75a26f9ecf0ce810e3154dfcaaf44")
expectCreator := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
diff --git a/rpc/jsonrpc/otterscan_search_backward_test.go b/rpc/jsonrpc/otterscan_search_backward_test.go
index 26ffcf23ff0..304eff15495 100644
--- a/rpc/jsonrpc/otterscan_search_backward_test.go
+++ b/rpc/jsonrpc/otterscan_search_backward_test.go
@@ -167,7 +167,7 @@ func TestBackwardBlockProviderWithMultipleChunksBlockNotFound(t *testing.T) {
func TestSearchTransactionsBefore(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewOtterscanAPI(newBaseApiForTest(m), m.DB, 25)
+ api := NewOtterscanAPI(newBaseApiForTest(m), m.OverlayDB(), 25)
addr := common.HexToAddress("0x537e697c7ab75a26f9ecf0ce810e3154dfcaaf44")
t.Run("small page size", func(t *testing.T) {
diff --git a/rpc/jsonrpc/otterscan_search_forward_test.go b/rpc/jsonrpc/otterscan_search_forward_test.go
index 0579c46aa8b..cf5f7ee6e83 100644
--- a/rpc/jsonrpc/otterscan_search_forward_test.go
+++ b/rpc/jsonrpc/otterscan_search_forward_test.go
@@ -166,7 +166,7 @@ func TestForwardBlockProviderWithMultipleChunksBlockNotFound(t *testing.T) {
func TestSearchTransactionsAfter(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewOtterscanAPI(newBaseApiForTest(m), m.DB, 25)
+ api := NewOtterscanAPI(newBaseApiForTest(m), m.OverlayDB(), 25)
addr := common.HexToAddress("0x537e697c7ab75a26f9ecf0ce810e3154dfcaaf44")
t.Run("small page size", func(t *testing.T) {
diff --git a/rpc/jsonrpc/otterscan_transaction_by_sender_and_nonce_test.go b/rpc/jsonrpc/otterscan_transaction_by_sender_and_nonce_test.go
index 4fbb8f9b794..ee1b2f990bf 100644
--- a/rpc/jsonrpc/otterscan_transaction_by_sender_and_nonce_test.go
+++ b/rpc/jsonrpc/otterscan_transaction_by_sender_and_nonce_test.go
@@ -31,7 +31,7 @@ func TestGetTransactionBySenderAndNonce(t *testing.T) {
t.Skip("slow test")
}
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewOtterscanAPI(NewBaseApi(nil, nil, m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs}), m.DB, 25)
+ api := NewOtterscanAPI(NewBaseApi(nil, nil, m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs}), m.OverlayDB(), 25)
addr := common.HexToAddress("0x537e697c7ab75a26f9ecf0ce810e3154dfcaaf44")
expectCreator := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
diff --git a/rpc/jsonrpc/overlay_api.go b/rpc/jsonrpc/overlay_api.go
index 0fbccd6825e..6c73578f86a 100644
--- a/rpc/jsonrpc/overlay_api.go
+++ b/rpc/jsonrpc/overlay_api.go
@@ -557,7 +557,7 @@ func getBeginEnd(ctx context.Context, tx kv.Tx, api *OverlayAPIImpl, crit filter
return 0, 0, fmt.Errorf("end (%d) < begin (%d)", end, begin)
}
if end > roaring.MaxUint32 {
- latest, err := rpchelper.GetLatestBlockNumber(tx)
+ latest, err := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
if err != nil {
return 0, 0, err
}
diff --git a/rpc/jsonrpc/overlay_api_test.go b/rpc/jsonrpc/overlay_api_test.go
index 0ead49695da..ec6c04ca0e6 100644
--- a/rpc/jsonrpc/overlay_api_test.go
+++ b/rpc/jsonrpc/overlay_api_test.go
@@ -34,7 +34,7 @@ import (
func TestOverlayGetBeginEnd(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
api := &OverlayAPIImpl{BaseAPI: newBaseApiForTest(m)}
- tx, err := m.DB.BeginTemporalRo(m.Ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(m.Ctx)
require.NoError(t, err)
defer tx.Rollback()
diff --git a/rpc/jsonrpc/overlay_race_test.go b/rpc/jsonrpc/overlay_race_test.go
index b37b3133fa1..849be59133c 100644
--- a/rpc/jsonrpc/overlay_race_test.go
+++ b/rpc/jsonrpc/overlay_race_test.go
@@ -68,12 +68,12 @@ func newOverlayAheadTestAPI(t *testing.T) (base *BaseAPI, m *execmoduletester.Ex
c, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, overlayRaceChainSize, func(i int, gen *blockgen.BlockGen) {
gen.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(c))
ctx := m.Ctx
- overlayRoTx, err := m.DB.BeginTemporalRo(ctx)
+ overlayRoTx, err := m.OverlayDB().BeginTemporalRo(ctx)
require.NoError(t, err)
t.Cleanup(overlayRoTx.Rollback)
doms, err := execctx.NewSharedDomains(ctx, overlayRoTx, m.Log)
@@ -150,7 +150,7 @@ func marshalOverlayRaceTestTx(t *testing.T, txn types.Transaction) []byte {
func TestGetBlockByTimestamp_SeesOverlayHead(t *testing.T) {
t.Parallel()
base, m, overlayHeader := newOverlayAheadTestAPI(t)
- api := NewErigonAPI(base, m.DB, nil)
+ api := NewErigonAPI(base, m.OverlayDB(), nil)
resp, err := api.GetBlockByTimestamp(m.Ctx, rpc.Timestamp(overlayHeader.Time), false)
require.NoError(t, err)
@@ -171,7 +171,7 @@ func TestGetTransactionByHash_PendingTx_UsesOverlayHead(t *testing.T) {
pool := &overlayRaceTxPoolClient{
transactionsReply: &txpoolproto.TransactionsReply{RlpTxs: [][]byte{marshalOverlayRaceTestTx(t, pendingTxn)}},
}
- api := newEthApiForTest(base, m.DB, pool, nil)
+ api := newEthApiForTest(base, m.OverlayDB(), pool, nil)
got, err := api.GetTransactionByHash(m.Ctx, pendingTxn.Hash())
require.NoError(t, err)
@@ -201,7 +201,7 @@ func TestTxPoolContent_UsesOverlayHead(t *testing.T) {
t.Parallel()
base, m, overlayHeader := newOverlayAheadTestAPI(t)
pool, txn := newOverlayRacePendingPool(t, m)
- api := NewTxPoolAPI(base, m.DB, pool)
+ api := NewTxPoolAPI(base, m.OverlayDB(), pool)
content, err := api.Content(m.Ctx)
require.NoError(t, err)
@@ -217,7 +217,7 @@ func TestTxPoolContentFrom_UsesOverlayHead(t *testing.T) {
t.Parallel()
base, m, overlayHeader := newOverlayAheadTestAPI(t)
pool, txn := newOverlayRacePendingPool(t, m)
- api := NewTxPoolAPI(base, m.DB, pool)
+ api := NewTxPoolAPI(base, m.OverlayDB(), pool)
content, err := api.ContentFrom(m.Ctx, m.Address)
require.NoError(t, err)
diff --git a/rpc/jsonrpc/parity_api_test.go b/rpc/jsonrpc/parity_api_test.go
index 274e48b73b9..73807912109 100644
--- a/rpc/jsonrpc/parity_api_test.go
+++ b/rpc/jsonrpc/parity_api_test.go
@@ -37,7 +37,7 @@ func TestParityAPIImpl_ListStorageKeys_NoOffset(t *testing.T) {
assert := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
baseApi := NewBaseApi(nil, nil, m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs})
- api := NewParityAPIImpl(baseApi, m.DB)
+ api := NewParityAPIImpl(baseApi, m.OverlayDB())
answers := []string{
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000002",
@@ -59,7 +59,7 @@ func TestParityAPIImpl_ListStorageKeys_NoOffset(t *testing.T) {
func TestParityAPIImpl_ListStorageKeys_WithOffset_ExistingPrefix(t *testing.T) {
assert := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewParityAPIImpl(newBaseApiForTest(m), m.DB)
+ api := NewParityAPIImpl(newBaseApiForTest(m), m.OverlayDB())
answers := []string{
"29d05770ca9ee7088a64e18c8e5160fc62c3c2179dc8ef9b4dbc970c9e51b4d8",
"29edc84535d98b29835079d685b97b41ee8e831e343cc80793057e462353a26d",
@@ -83,7 +83,7 @@ func TestParityAPIImpl_ListStorageKeys_WithOffset_ExistingPrefix(t *testing.T) {
func TestParityAPIImpl_ListStorageKeys_WithOffset_NonExistingPrefix(t *testing.T) {
assert := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewParityAPIImpl(newBaseApiForTest(m), m.DB)
+ api := NewParityAPIImpl(newBaseApiForTest(m), m.OverlayDB())
answers := []string{
"4644be453c81744b6842ddf615d7fca0e14a23b09734be63d44c23452de95631",
"4974416255391052161ba8184fe652f3bf8c915592c65f7de127af8e637dce5d",
@@ -104,7 +104,7 @@ func TestParityAPIImpl_ListStorageKeys_WithOffset_NonExistingPrefix(t *testing.T
func TestParityAPIImpl_ListStorageKeys_WithOffset_EmptyResponse(t *testing.T) {
assert := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewParityAPIImpl(newBaseApiForTest(m), m.DB)
+ api := NewParityAPIImpl(newBaseApiForTest(m), m.OverlayDB())
addr := common.HexToAddress("0x920fd5070602feaea2e251e9e7238b6c376bcae5")
offset := common.Hex2Bytes("ff")
b := hexutil.Bytes(offset)
@@ -118,7 +118,7 @@ func TestParityAPIImpl_ListStorageKeys_WithOffset_EmptyResponse(t *testing.T) {
func TestParityAPIImpl_ListStorageKeys_AccNotFound(t *testing.T) {
assert := assert.New(t)
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
- api := NewParityAPIImpl(newBaseApiForTest(m), m.DB)
+ api := NewParityAPIImpl(newBaseApiForTest(m), m.OverlayDB())
addr := common.HexToAddress("0x920fd5070602feaea2e251e9e7238b6c376bcaef")
_, err := api.ListStorageKeys(context.Background(), addr, 2, nil, latestBlock)
assert.Error(err, errors.New("acc not found"))
diff --git a/rpc/jsonrpc/receipts/handler_test.go b/rpc/jsonrpc/receipts/handler_test.go
index 7fa3c373a65..da0f2b2411f 100644
--- a/rpc/jsonrpc/receipts/handler_test.go
+++ b/rpc/jsonrpc/receipts/handler_test.go
@@ -356,7 +356,7 @@ func mockWithGenerator(t *testing.T, blocks int, generator func(int, *blockgen.B
execmoduletester.WithKey(testKey),
)
if blocks > 0 {
- chain, _ := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, blocks, generator)
+ chain, _ := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, blocks, generator, m.PublishedSD())
err := m.InsertChain(chain)
require.NoError(t, err)
}
diff --git a/rpc/jsonrpc/send_transaction_test.go b/rpc/jsonrpc/send_transaction_test.go
index eb88075f5a7..f00929b500e 100644
--- a/rpc/jsonrpc/send_transaction_test.go
+++ b/rpc/jsonrpc/send_transaction_test.go
@@ -39,7 +39,7 @@ import (
func oneBlockStep(m *execmoduletester.ExecModuleTester, require *require.Assertions) {
chain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1 /*number of blocks:*/, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
require.NoError(err)
err = m.InsertChain(chain)
require.NoError(err)
@@ -55,7 +55,7 @@ func TestSendRawTransaction(t *testing.T) {
ctx, conn := rpcdaemontest.CreateTestGrpcConn(t, m)
txPool := txpoolproto.NewTxpoolClient(conn)
ff := rpchelper.New(ctx, rpchelper.DefaultFiltersConfig, nil, txPool, txpoolproto.NewMiningClient(conn), func() {}, m.Log, nil)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, txPool, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), txPool, nil)
buf := bytes.NewBuffer(nil)
err = txn.MarshalBinary(buf)
require.NoError(err)
@@ -96,7 +96,7 @@ func TestSendRawTransactionUnprotected(t *testing.T) {
ctx, conn := rpcdaemontest.CreateTestGrpcConn(t, m)
txPool := txpoolproto.NewTxpoolClient(conn)
ff := rpchelper.New(ctx, rpchelper.DefaultFiltersConfig, nil, txPool, txpoolproto.NewMiningClient(conn), func() {}, m.Log, nil)
- api := newEthApiForTest(newBaseApiForTest(m), m.DB, txPool, nil)
+ api := newEthApiForTest(newBaseApiForTest(m), m.OverlayDB(), txPool, nil)
// Enable unprotected txs flag
api.AllowUnprotectedTxs = true
buf := bytes.NewBuffer(nil)
diff --git a/rpc/jsonrpc/trace_adhoc_test.go b/rpc/jsonrpc/trace_adhoc_test.go
index d321b58b80f..c7d881d72f8 100644
--- a/rpc/jsonrpc/trace_adhoc_test.go
+++ b/rpc/jsonrpc/trace_adhoc_test.go
@@ -319,7 +319,7 @@ func TestReplayTransaction(t *testing.T) {
m, _, _ := rpcdaemontest.CreateTestExecModule(t)
api := newTraceApiForTest(m)
var txnHash common.Hash
- if err := m.DB.View(context.Background(), func(tx kv.Tx) error {
+ if err := m.OverlayDB().View(context.Background(), func(tx kv.Tx) error {
b, err := m.BlockReader.BlockByNumber(m.Ctx, tx, 6)
if err != nil {
return err
@@ -470,7 +470,7 @@ func TestOeTracer(t *testing.T) {
// returns its binary encoding together with the sender and recipient addresses.
func rawTxFromBlock(t *testing.T, m *execmoduletester.ExecModuleTester, blockNum uint64) (encoded []byte, from, to accounts.Address) {
t.Helper()
- if err := m.DB.View(context.Background(), func(tx kv.Tx) error {
+ if err := m.OverlayDB().View(context.Background(), func(tx kv.Tx) error {
b, err := m.BlockReader.BlockByNumber(m.Ctx, tx, blockNum)
if err != nil {
return err
@@ -713,7 +713,7 @@ func (c *baseFeeTestChain) mineBlock(t *testing.T, gen func(*blockgen.BlockGen))
chainB, err := blockgen.GenerateChain(c.m.ChainConfig, c.head, c.m.Engine, c.m.DB, 1, func(_ int, block *blockgen.BlockGen) {
gen(block)
- })
+ }, c.m.PublishedSD())
require.NoError(t, err)
require.NoError(t, c.m.InsertChain(chainB))
c.head = chainB.TopBlock
diff --git a/rpc/jsonrpc/trace_filtering_test.go b/rpc/jsonrpc/trace_filtering_test.go
index 065e35d6bf1..e4a8f2994dd 100644
--- a/rpc/jsonrpc/trace_filtering_test.go
+++ b/rpc/jsonrpc/trace_filtering_test.go
@@ -55,7 +55,7 @@ func TestCallBlockParallelMatchesSequential(t *testing.T) {
const blockNum = uint64(6) // block 6 has 32 txs (case i=5 in test chain generation)
traceTypes := []string{TraceTypeTrace}
- tx, err := m.DB.BeginTemporalRo(ctx)
+ tx, err := m.OverlayDB().BeginTemporalRo(ctx)
require.NoError(t, err)
defer tx.Rollback()
@@ -212,7 +212,7 @@ func chainWithWithdrawal(t *testing.T, withdrawalAddr common.Address, withdrawal
Address: withdrawalAddr,
Amount: withdrawalGwei,
})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(generated)
require.NoError(t, err)
@@ -343,7 +343,7 @@ func TestReplayBlockTransactionsMultiWithdrawalSameAddr(t *testing.T) {
generated, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, func(_ int, b *blockgen.BlockGen) {
b.AddWithdrawal(&types.Withdrawal{Index: 0, Validator: 42, Address: withdrawalAddr, Amount: wd1Gwei})
b.AddWithdrawal(&types.Withdrawal{Index: 1, Validator: 43, Address: withdrawalAddr, Amount: wd2Gwei})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
err = m.InsertChain(generated)
require.NoError(t, err)
@@ -389,7 +389,7 @@ func TestReplayBlockTransactionsWithdrawalNewAddress(t *testing.T) {
m := execmoduletester.New(t, execmoduletester.WithGenesisSpec(gspec))
generated, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, func(_ int, b *blockgen.BlockGen) {
b.AddWithdrawal(&types.Withdrawal{Index: 0, Validator: 42, Address: newAddr, Amount: withdrawalGwei})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(generated))
@@ -434,7 +434,7 @@ func TestReplayBlockTransactionsMultiWithdrawalNewAddress(t *testing.T) {
generated, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, func(_ int, b *blockgen.BlockGen) {
b.AddWithdrawal(&types.Withdrawal{Index: 0, Validator: 1, Address: newAddr, Amount: wd1Gwei})
b.AddWithdrawal(&types.Withdrawal{Index: 1, Validator: 2, Address: newAddr, Amount: wd2Gwei})
- })
+ }, m.PublishedSD())
require.NoError(t, err)
require.NoError(t, m.InsertChain(generated))
diff --git a/rpc/jsonrpc/txpool_api_test.go b/rpc/jsonrpc/txpool_api_test.go
index 8bd68f3a041..6b4482b13cf 100644
--- a/rpc/jsonrpc/txpool_api_test.go
+++ b/rpc/jsonrpc/txpool_api_test.go
@@ -41,7 +41,7 @@ func TestTxPoolContent(t *testing.T) {
require := require.New(t)
chain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, func(i int, b *blockgen.BlockGen) {
b.SetCoinbase(common.Address{1})
- })
+ }, m.PublishedSD())
require.NoError(err)
err = m.InsertChain(chain)
require.NoError(err)
@@ -49,7 +49,7 @@ func TestTxPoolContent(t *testing.T) {
ctx, conn := rpcdaemontest.CreateTestGrpcConn(t, m)
txPool := txpoolproto.NewTxpoolClient(conn)
ff := rpchelper.New(ctx, rpchelper.DefaultFiltersConfig, nil, txPool, txpoolproto.NewMiningClient(conn), func() {}, m.Log, nil)
- api := NewTxPoolAPI(NewBaseApi(ff, kvcache.New(kvcache.DefaultCoherentConfig), m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs}), m.DB, txPool)
+ api := NewTxPoolAPI(NewBaseApi(ff, kvcache.New(kvcache.DefaultCoherentConfig), m.BlockReader, m.Engine, nil, &rpccfg.BaseApiConfig{Dirs: m.Dirs}), m.OverlayDB(), txPool)
expectValue := uint64(1234)
txn, err := types.SignTx(types.NewTransaction(0, common.Address{1}, uint256.NewInt(expectValue), params.TxGas, uint256.NewInt(10*common.GWei), nil), *types.LatestSignerForChainID(m.ChainConfig.ChainID), m.Key)
diff --git a/rpc/rpchelper/filters.go b/rpc/rpchelper/filters.go
index 4645e6661f9..2133db20f72 100644
--- a/rpc/rpchelper/filters.go
+++ b/rpc/rpchelper/filters.go
@@ -1183,6 +1183,13 @@ func (ff *Filters) WithOverlay(tx kv.Tx) kv.Tx {
return tx
}
if overlay := sd.BlockOverlay(); overlay != nil {
+ // Chain every ancestor generation's overlay when the base tx is temporal;
+ // a non-temporal tx cannot carry the chain, so fall back to the leaf.
+ if ttx, ok := tx.(kv.TemporalTx); ok {
+ if v := sd.OverlayTemporalTx(ttx); v != nil {
+ return v
+ }
+ }
return overlay.NewReadView(tx)
}
return tx
@@ -1198,8 +1205,13 @@ func (ff *Filters) WithTemporalOverlay(tx kv.TemporalTx) kv.TemporalTx {
if sd == nil {
return tx
}
- if overlay := sd.BlockOverlay(); overlay != nil {
- return overlay.NewReadView(tx)
+ // Route through OverlayTemporalTx so the view chains every ancestor
+ // generation's block overlay, not just the leaf — otherwise reads miss
+ // uncommitted block data from earlier FCUs.
+ if sd.BlockOverlay() != nil {
+ if v := sd.OverlayTemporalTx(tx); v != nil {
+ return v
+ }
}
return tx
}