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 }