Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions db/kv/kv_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ 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
}
Expand Down
56 changes: 56 additions & 0 deletions db/kv/membatchwithdb/memory_mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ import (

var _ kv.TemporalRwTx = &MemoryMutation{}

type DomainReader interface {
GetAsOf(name kv.Domain, k []byte, ts uint64) ([]byte, bool, error)
HistorySeek(name kv.Domain, k []byte, ts uint64) ([]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,
Expand All @@ -52,6 +57,7 @@ type MemoryMutation struct {
clearedTables map[string]struct{}
db kv.TemporalTx
statelessCursors map[string]kv.RwCursor
DomainReader DomainReader
}

// NewMemoryBatch creates a pure Go in-memory batch with no OS-thread affinity.
Expand Down Expand Up @@ -913,6 +919,15 @@ func (m *MemoryMutation) GetLatest(name kv.Domain, k []byte) (v []byte, step kv.
}

func (m *MemoryMutation) GetAsOf(name kv.Domain, k []byte, ts uint64) (v []byte, ok bool, err error) {
if m.DomainReader != nil {
val, ok, err := m.DomainReader.GetAsOf(name, k, ts)
if err != nil {
return nil, false, err
}
if ok {
return val, true, nil
}
}
if m.db == nil {
return nil, false, fmt.Errorf("MemoryMutation: domain read requires backing tx (detached overlay)")
}
Expand Down Expand Up @@ -941,6 +956,15 @@ func (m *MemoryMutation) RangeAsOf(name kv.Domain, fromKey, toKey []byte, ts uin
}

func (m *MemoryMutation) HistorySeek(name kv.Domain, k []byte, ts uint64) (v []byte, ok bool, err error) {
if m.DomainReader != nil {
val, ok, err := m.DomainReader.HistorySeek(name, k, ts)
if err != nil {
return nil, false, err
}
if ok {
return val, true, nil
}
}
if m.db == nil {
return nil, false, fmt.Errorf("MemoryMutation: history read requires backing tx (detached overlay)")
}
Expand Down Expand Up @@ -1061,6 +1085,7 @@ func (m *MemoryMutation) newReadViewMut(tx kv.Tx) *MemoryMutation {
deletedDups: m.deletedDups,
clearedTables: m.clearedTables,
db: dbTx,
DomainReader: m.DomainReader,
}
}

Expand Down Expand Up @@ -1113,30 +1138,61 @@ func (v *OverlayTemporalReadView) Apply(_ context.Context, f func(tx kv.Tx) erro
func (v *OverlayTemporalReadView) GetLatest(name kv.Domain, k []byte) ([]byte, kv.Step, error) {
return v.temporalTx.GetLatest(name, k)
}

func (v *OverlayTemporalReadView) HasPrefix(name kv.Domain, prefix []byte) ([]byte, []byte, bool, error) {
return v.temporalTx.HasPrefix(name, prefix)
}

func (v *OverlayTemporalReadView) StepsInFiles(entitySet ...kv.Domain) kv.Step {
return v.temporalTx.StepsInFiles(entitySet...)
}

func (v *OverlayTemporalReadView) GetAsOf(name kv.Domain, k []byte, ts uint64) ([]byte, bool, error) {
// Check DomainReader independently — this method shadows MemoryMutation.GetAsOf
// and falls through to v.temporalTx (not m.db), so the embedded check never fires.
if v.MemoryMutation != nil && v.MemoryMutation.DomainReader != nil {
val, ok, err := v.MemoryMutation.DomainReader.GetAsOf(name, k, ts)
if err != nil {
return nil, false, err
}
if ok {
return val, true, nil
}
}
return v.temporalTx.GetAsOf(name, k, ts)
}

func (v *OverlayTemporalReadView) RangeAsOf(name kv.Domain, fromKey, toKey []byte, ts uint64, asc order.By, limit int) (stream.KV, error) {
return v.temporalTx.RangeAsOf(name, fromKey, toKey, ts, asc, limit)
}

func (v *OverlayTemporalReadView) IndexRange(name kv.InvertedIdx, k []byte, fromTs, toTs int, asc order.By, limit int) (stream.U64, error) {
return v.temporalTx.IndexRange(name, k, fromTs, toTs, asc, limit)
}

func (v *OverlayTemporalReadView) HistorySeek(name kv.Domain, k []byte, ts uint64) ([]byte, bool, error) {
// Check DomainReader independently — this method shadows MemoryMutation.HistorySeek
// and falls through to v.temporalTx (not m.db), so the embedded check never fires.
if v.MemoryMutation != nil && v.MemoryMutation.DomainReader != nil {
val, ok, err := v.MemoryMutation.DomainReader.HistorySeek(name, k, ts)
if err != nil {
return nil, false, err
}
if ok {
return val, true, nil
}
}
return v.temporalTx.HistorySeek(name, k, ts)
}

func (v *OverlayTemporalReadView) HistoryRange(name kv.Domain, fromTs, toTs int, asc order.By, limit int) (stream.KV, error) {
return v.temporalTx.HistoryRange(name, fromTs, toTs, asc, limit)
}

func (v *OverlayTemporalReadView) Debug() kv.TemporalDebugTx {
return v.temporalTx.Debug()
}

func (v *OverlayTemporalReadView) AggTx() any {
return v.temporalTx.AggTx()
}
Expand Down
42 changes: 42 additions & 0 deletions db/kv/membatchwithdb/memory_mutation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package membatchwithdb_test

import (
"errors"
"fmt"
"sync"
"testing"
Expand Down Expand Up @@ -859,3 +860,44 @@ func TestMemoryMutationConcurrentDeleteAndRead(t *testing.T) {

wg.Wait()
}

// erroringDomainReader fails every domain read, so a caller that swallows the
// error is indistinguishable from a caller that saw no value at all.
type erroringDomainReader struct{ err error }

func (r erroringDomainReader) GetAsOf(kv.Domain, []byte, uint64) ([]byte, bool, error) {
return nil, false, r.err
}

func (r erroringDomainReader) HistorySeek(kv.Domain, []byte, uint64) ([]byte, bool, error) {
return nil, false, r.err
}

// TestDomainReadErrorsPropagate covers both overlay read views: a DomainReader
// error must reach the caller rather than fall through to the committed tx,
// which would silently answer with stale data.
func TestDomainReadErrorsPropagate(t *testing.T) {
t.Parallel()

_, rwTx := newTestTx(t)
batch, err := membatchwithdb.NewMemoryBatch(rwTx, "", log.Root())
require.NoError(t, err)
defer batch.Close()

wantErr := errors.New("domain reader unavailable")
batch.DomainReader = erroringDomainReader{err: wantErr}

key := []byte{0x2}
for name, tx := range map[string]kv.TemporalTx{
"MemoryMutation": batch,
"OverlayTemporalReadView": batch.NewTemporalReadView(rwTx),
} {
t.Run(name, func(t *testing.T) {
_, _, err := tx.GetAsOf(kv.ReceiptDomain, key, 1)
require.ErrorIs(t, err, wantErr, "GetAsOf must propagate the DomainReader error")

_, _, err = tx.HistorySeek(kv.ReceiptDomain, key, 1)
require.ErrorIs(t, err, wantErr, "HistorySeek must propagate the DomainReader error")
})
}
}
7 changes: 7 additions & 0 deletions db/state/execctx/domain_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,12 +568,15 @@ func (sd *SharedDomains) InitBlockOverlay(tx kv.TemporalTx, tmpDir string) error
if err != nil {
return fmt.Errorf("init block overlay: %w", err)
}
overlay.DomainReader = sd
sd.blockOverlay.Store(overlay)
return nil
}

func (sd *SharedDomains) GetCommitmentCtx() *commitmentdb.SharedDomainsCommitmentContext {
return sd.sdCtx
}

func (sd *SharedDomains) Logger() log.Logger { return sd.logger }

// SetStateCache sets the state cache for faster lookups.
Expand Down Expand Up @@ -840,6 +843,10 @@ func (sd *SharedDomains) GetAsOf(domain kv.Domain, key []byte, ts uint64) (v []b
return sd.mem.GetAsOf(domain, key, ts)
}

func (sd *SharedDomains) HistorySeek(domain kv.Domain, key []byte, ts uint64) (v []byte, ok bool, err error) {
return sd.mem.HistorySeek(domain, key, ts)
}

// DomainPut
// Optimizations:
// - user can provide `prevVal != nil` - then it will not read prev value from storage
Expand Down
103 changes: 103 additions & 0 deletions db/state/execctx/domain_shared_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"github.com/erigontech/erigon/db/kv/mdbx"
"github.com/erigontech/erigon/db/kv/rawdbv3"
"github.com/erigontech/erigon/db/kv/temporal"
"github.com/erigontech/erigon/db/rawdb/rawtemporaldb"
"github.com/erigontech/erigon/db/state"
"github.com/erigontech/erigon/db/state/changeset"
"github.com/erigontech/erigon/db/state/execctx"
Expand Down Expand Up @@ -1740,3 +1741,105 @@ func TestSharedDomain_TouchChangedKeysFromHistory(t *testing.T) {
require.Equal(t, expectedRootHash, rootHash)
}
}

func TestBlockOverlay_DomainReadsRegression(t *testing.T) {
ctx := context.Background()
stepSize := uint64(10)
db := newTestDb(t, stepSize)

tx, err := db.BeginTemporalRw(ctx)
require.NoError(t, err)
defer tx.Rollback()

sd, err := execctx.NewSharedDomains(ctx, tx, log.New())
require.NoError(t, err)
defer sd.Close()

err = sd.InitBlockOverlay(tx, t.TempDir())
require.NoError(t, err)

txNum := uint64(42)
key := []byte("some-test-key")
value := []byte("some-test-value")

// Put value into a domain (e.g. ReceiptDomain) in sd
err = sd.DomainPut(kv.ReceiptDomain, tx, key, value, txNum, nil)
require.NoError(t, err)

// --- Production path: overlay.NewReadView returns *MemoryMutation ---
// This is the path exercised by Filters.WithTemporalOverlay and
// Filters.WithOverlay in the RPC layer.
overlay := sd.BlockOverlay()
require.NotNil(t, overlay)
readViewTx := overlay.NewReadView(tx)
require.NotNil(t, readViewTx)

gotVal, ok, err := readViewTx.GetAsOf(kv.ReceiptDomain, key, txNum+1)
require.NoError(t, err)
require.True(t, ok, "NewReadView (*MemoryMutation) GetAsOf must find in-memory receipt data")
require.Equal(t, value, gotVal)

gotValHist, ok, err := readViewTx.HistorySeek(kv.ReceiptDomain, key, txNum+1)
require.NoError(t, err)
require.True(t, ok, "NewReadView (*MemoryMutation) HistorySeek must find in-memory receipt data")
require.Equal(t, value, gotValHist)

// --- Secondary path: overlay.NewTemporalReadView returns *OverlayTemporalReadView ---
overlayTx := sd.BlockOverlayTemporalTx(tx)
require.NotNil(t, overlayTx)

gotVal2, ok, err := overlayTx.GetAsOf(kv.ReceiptDomain, key, txNum+1)
require.NoError(t, err)
require.True(t, ok, "NewTemporalReadView GetAsOf must find in-memory receipt data")
require.Equal(t, value, gotVal2)

gotValHist2, ok, err := overlayTx.HistorySeek(kv.ReceiptDomain, key, txNum+1)
require.NoError(t, err)
require.True(t, ok, "NewTemporalReadView HistorySeek must find in-memory receipt data")
require.Equal(t, value, gotValHist2)
}

// TestReceiptAsOf_InFlightBlockLogIndex pins the read that seeds per-transaction log
// indexes. A block whose commit is in flight has its receipt metadata only in
// SharedDomains, and on a history miss DomainRoTx.GetAsOf falls back to GetLatest — so
// a bare read answers with the last committed block's value. The overlay read view must
// see the in-flight value instead.
func TestReceiptAsOf_InFlightBlockLogIndex(t *testing.T) {
t.Parallel()

ctx := t.Context()
logger := log.New()
db := newTestDb(t, 10)

tx, err := db.BeginTemporalRw(ctx)
require.NoError(t, err)
defer tx.Rollback()

const (
committedTxNum = uint64(5)
committedLogIdx = uint32(7)
inFlightTxNum = uint64(9)
inFlightLogIdx = uint32(3)
)

committed, err := execctx.NewSharedDomains(ctx, tx, logger)
require.NoError(t, err)
defer committed.Close()
require.NoError(t, rawtemporaldb.AppendReceipt(committed.AsPutDel(tx), committedLogIdx, 0, 0, committedTxNum))
require.NoError(t, committed.Flush(ctx, tx))
committed.Close()

_, _, stale, err := rawtemporaldb.ReceiptAsOf(tx, inFlightTxNum+1)
require.NoError(t, err)
require.Equal(t, committedLogIdx, stale, "precondition: a bare read must return the stale committed value")

sd, err := execctx.NewSharedDomains(ctx, tx, logger)
require.NoError(t, err)
defer sd.Close()
require.NoError(t, sd.InitBlockOverlay(tx, t.TempDir()))
require.NoError(t, rawtemporaldb.AppendReceipt(sd.AsPutDel(tx), inFlightLogIdx, 0, 0, inFlightTxNum))

_, _, got, err := rawtemporaldb.ReceiptAsOf(sd.BlockOverlay().NewReadView(tx), inFlightTxNum+1)
require.NoError(t, err)
require.Equal(t, inFlightLogIdx, got, "must serve the in-flight block's log index, not the last committed one")
}
6 changes: 5 additions & 1 deletion db/state/temporal_mem_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func (sd *TemporalMemBatch) getLatest(domain kv.Domain, key []byte) (v []byte, s
}

func (sd *TemporalMemBatch) GetAsOf(domain kv.Domain, key []byte, ts uint64) (v []byte, ok bool, err error) {
if !sd.inMemHistoryReads {
if !sd.inMemHistoryReads && domain != kv.ReceiptDomain {
return nil, false, errors.New("GetAsOf called on TemporalMemBatch with inMemHistoryReads disabled")
}
sd.latestStateLock.RLock()
Expand Down Expand Up @@ -324,6 +324,10 @@ 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)
}

func (sd *TemporalMemBatch) SizeEstimate() uint64 {
sd.latestStateLock.RLock()
defer sd.latestStateLock.RUnlock()
Expand Down
3 changes: 3 additions & 0 deletions rpc/jsonrpc/receipts/receipts_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ type PostStateInfo struct {
}

func (g *Generator) GetReceipt(ctx context.Context, cfg *chain.Config, tx kv.TemporalTx, header *types.Header, txn types.Transaction, index int, txNum uint64, postState *PostStateInfo) (_ *types.Receipt, err error) {
tx = g.filters.WithTemporalOverlay(tx)
blockHash := header.Hash()
blockNum := header.Number.Uint64()
txnHash := txn.Hash()
Expand Down Expand Up @@ -450,6 +451,7 @@ func (g *Generator) GetReceipt(ctx context.Context, cfg *chain.Config, tx kv.Tem
}

func (g *Generator) GetReceipts(ctx context.Context, cfg *chain.Config, tx kv.TemporalTx, block *types.Block, opts eth.ReceiptsOpts) (_ types.Receipts, err error) {
tx = g.filters.WithTemporalOverlay(tx)
blockHash := block.Hash()
blockNum := block.NumberU64()

Expand Down Expand Up @@ -680,6 +682,7 @@ func (g *Generator) assertEqualReceipts(fromExecution, fromDB *types.Receipt) {
}

func (g *Generator) GetReceiptsGasUsed(ctx context.Context, tx kv.TemporalTx, block *types.Block, txNumsReader rawdbv3.TxNumsReader) (types.Receipts, error) {
tx = g.filters.WithTemporalOverlay(tx)
if receipts, ok := g.receiptsCache.Get(block.Hash()); ok {
return receipts, nil
}
Expand Down
Loading
Loading