From 6128cf824e65bcb92ff8829718359860ddd3e75d Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Wed, 8 Apr 2026 13:28:43 -0700 Subject: [PATCH 1/6] added earliest version to receipt store --- evmrpc/block.go | 6 +++ evmrpc/watermark_manager.go | 18 +++++++++ evmrpc/watermark_manager_test.go | 38 ++++++++++++++++++- sei-db/ledger_db/parquet/store.go | 5 +++ .../ledger_db/receipt/cached_receipt_store.go | 4 ++ sei-db/ledger_db/receipt/parquet_store.go | 4 ++ sei-db/ledger_db/receipt/receipt_store.go | 5 +++ 7 files changed, 79 insertions(+), 1 deletion(-) diff --git a/evmrpc/block.go b/evmrpc/block.go index dfe02a5694..523886941c 100644 --- a/evmrpc/block.go +++ b/evmrpc/block.go @@ -178,6 +178,9 @@ func (a *BlockAPI) GetBlockTransactionCountByNumber(ctx context.Context, number if err != nil { return nil, err } + if err := a.watermarks.EnsureReceiptHeightAvailable(ctx, block.Block.Height); err != nil { + return nil, err + } return a.getEvmTxCount(block), nil } @@ -191,6 +194,9 @@ func (a *BlockAPI) GetBlockTransactionCountByHash(ctx context.Context, blockHash if err != nil { return nil, err } + if err := a.watermarks.EnsureReceiptHeightAvailable(ctx, block.Block.Height); err != nil { + return nil, err + } return a.getEvmTxCount(block), nil } diff --git a/evmrpc/watermark_manager.go b/evmrpc/watermark_manager.go index d751d3421d..616b689ba9 100644 --- a/evmrpc/watermark_manager.go +++ b/evmrpc/watermark_manager.go @@ -219,6 +219,24 @@ func (m *WatermarkManager) EnsureBlockHeightAvailable(ctx context.Context, heigh return m.ensureWithinWatermarks(height, blockEarliest, latest) } +// EnsureReceiptHeightAvailable verifies that receipts for the given block height +// have not been pruned from the receipt store. This is a separate check from +// EnsureBlockHeightAvailable because the receipt store can be configured with a +// smaller KeepRecent than the block or state stores. +func (m *WatermarkManager) EnsureReceiptHeightAvailable(_ context.Context, height int64) error { + if m.receiptStore == nil { + return nil + } + earliest := m.receiptStore.EarliestVersion() + if earliest <= 0 { + return nil + } + if height < earliest { + return fmt.Errorf("requested height %d receipts have been pruned; earliest available is %d", height, earliest) + } + return nil +} + func (m *WatermarkManager) ensureWithinWatermarks(height, earliest, latest int64) error { if height > latest { return fmt.Errorf("requested height %d is not yet available; safe latest is %d: %w", height, latest, ErrBlockHeightNotYetAvailable) diff --git a/evmrpc/watermark_manager_test.go b/evmrpc/watermark_manager_test.go index 025ef8ba05..af03a6b1cf 100644 --- a/evmrpc/watermark_manager_test.go +++ b/evmrpc/watermark_manager_test.go @@ -104,6 +104,37 @@ func TestEnsureBlockHeightAvailableBounds(t *testing.T) { require.ErrorContains(t, wm.EnsureBlockHeightAvailable(context.Background(), 2), "has been pruned") } +func TestEnsureReceiptHeightAvailable(t *testing.T) { + tmClient := &fakeTMClient{ + status: &coretypes.ResultStatus{SyncInfo: coretypes.SyncInfo{LatestBlockHeight: 200, EarliestBlockHeight: 1}}, + } + + t.Run("no receipt store allows any height", func(t *testing.T) { + wm := NewWatermarkManager(tmClient, nil, nil, nil) + require.NoError(t, wm.EnsureReceiptHeightAvailable(context.Background(), 5)) + }) + + t.Run("receipt store with no pruning allows any height", func(t *testing.T) { + rs := &fakeReceiptStore{latest: 200, earliest: 0} + wm := NewWatermarkManager(tmClient, nil, nil, rs) + require.NoError(t, wm.EnsureReceiptHeightAvailable(context.Background(), 5)) + }) + + t.Run("pruned receipt height returns error", func(t *testing.T) { + rs := &fakeReceiptStore{latest: 200, earliest: 150} + wm := NewWatermarkManager(tmClient, nil, nil, rs) + require.ErrorContains(t, wm.EnsureReceiptHeightAvailable(context.Background(), 100), "receipts have been pruned") + require.ErrorContains(t, wm.EnsureReceiptHeightAvailable(context.Background(), 149), "receipts have been pruned") + }) + + t.Run("height within receipt retention succeeds", func(t *testing.T) { + rs := &fakeReceiptStore{latest: 200, earliest: 150} + wm := NewWatermarkManager(tmClient, nil, nil, rs) + require.NoError(t, wm.EnsureReceiptHeightAvailable(context.Background(), 150)) + require.NoError(t, wm.EnsureReceiptHeightAvailable(context.Background(), 175)) + }) +} + func TestLatestAndEarliestHeightHelpers(t *testing.T) { tmClient := &fakeTMClient{ status: &coretypes.ResultStatus{SyncInfo: coretypes.SyncInfo{LatestBlockHeight: 22, EarliestBlockHeight: 11}}, @@ -233,13 +264,18 @@ func (f *fakeStateStore) Prune(_ int64) error func (f *fakeStateStore) Close() error { return nil } type fakeReceiptStore struct { - latest int64 + latest int64 + earliest int64 } func (f *fakeReceiptStore) LatestVersion() int64 { return f.latest } +func (f *fakeReceiptStore) EarliestVersion() int64 { + return f.earliest +} + func (f *fakeReceiptStore) SetLatestVersion(version int64) error { f.latest = version return nil diff --git a/sei-db/ledger_db/parquet/store.go b/sei-db/ledger_db/parquet/store.go index 5dcfe48950..cea6f5772f 100644 --- a/sei-db/ledger_db/parquet/store.go +++ b/sei-db/ledger_db/parquet/store.go @@ -175,6 +175,11 @@ func (s *Store) SetEarliestVersion(version int64) { s.earliestVersion.Store(version) } +// EarliestVersion returns the earliest version retained in the store. +func (s *Store) EarliestVersion() int64 { + return s.earliestVersion.Load() +} + // CacheRotateInterval returns the interval at which the cache should rotate. func (s *Store) CacheRotateInterval() uint64 { return s.config.MaxBlocksPerFile diff --git a/sei-db/ledger_db/receipt/cached_receipt_store.go b/sei-db/ledger_db/receipt/cached_receipt_store.go index a71cbd1e61..ccb0e97bc0 100644 --- a/sei-db/ledger_db/receipt/cached_receipt_store.go +++ b/sei-db/ledger_db/receipt/cached_receipt_store.go @@ -60,6 +60,10 @@ func (s *cachedReceiptStore) SetLatestVersion(version int64) error { return s.backend.SetLatestVersion(version) } +func (s *cachedReceiptStore) EarliestVersion() int64 { + return s.backend.EarliestVersion() +} + func (s *cachedReceiptStore) SetEarliestVersion(version int64) error { return s.backend.SetEarliestVersion(version) } diff --git a/sei-db/ledger_db/receipt/parquet_store.go b/sei-db/ledger_db/receipt/parquet_store.go index bdf88b2fd0..e88d05725f 100644 --- a/sei-db/ledger_db/receipt/parquet_store.go +++ b/sei-db/ledger_db/receipt/parquet_store.go @@ -52,6 +52,10 @@ func (s *parquetReceiptStore) SetLatestVersion(version int64) error { return nil } +func (s *parquetReceiptStore) EarliestVersion() int64 { + return s.store.EarliestVersion() +} + func (s *parquetReceiptStore) SetEarliestVersion(version int64) error { s.store.SetEarliestVersion(version) return nil diff --git a/sei-db/ledger_db/receipt/receipt_store.go b/sei-db/ledger_db/receipt/receipt_store.go index 9ce457765b..ba61132fd4 100644 --- a/sei-db/ledger_db/receipt/receipt_store.go +++ b/sei-db/ledger_db/receipt/receipt_store.go @@ -35,6 +35,7 @@ var ( // ReceiptStore exposes receipt-specific operations without leaking the StateStore interface. type ReceiptStore interface { LatestVersion() int64 + EarliestVersion() int64 SetLatestVersion(version int64) error SetEarliestVersion(version int64) error GetReceipt(ctx sdk.Context, txHash common.Hash) (*types.Receipt, error) @@ -151,6 +152,10 @@ func (s *receiptStore) SetLatestVersion(version int64) error { return s.db.SetLatestVersion(version) } +func (s *receiptStore) EarliestVersion() int64 { + return s.db.GetEarliestVersion() +} + func (s *receiptStore) SetEarliestVersion(version int64) error { return s.db.SetEarliestVersion(version, true) } From 7564da32fde65db103615e066aae4c3c26882570 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Wed, 8 Apr 2026 15:37:33 -0700 Subject: [PATCH 2/6] fixing cached_receipt_store_test --- sei-db/ledger_db/receipt/cached_receipt_store_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sei-db/ledger_db/receipt/cached_receipt_store_test.go b/sei-db/ledger_db/receipt/cached_receipt_store_test.go index 808cff1d8f..13747df9ad 100644 --- a/sei-db/ledger_db/receipt/cached_receipt_store_test.go +++ b/sei-db/ledger_db/receipt/cached_receipt_store_test.go @@ -31,6 +31,10 @@ func (f *fakeReceiptBackend) LatestVersion() int64 { return 0 } +func (f *fakeReceiptBackend) EarliestVersion() int64 { + return 0 +} + func (f *fakeReceiptBackend) SetLatestVersion(int64) error { return nil } From 8fdda3d5072f8b1d09f5443175e6707507d40d1a Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Wed, 8 Apr 2026 16:21:43 -0700 Subject: [PATCH 3/6] adding test coverage for EarliestVersion method --- evmrpc/simulate_test.go | 1 + evmrpc/state_test.go | 1 + sei-db/ledger_db/receipt/receipt_store_test.go | 1 + 3 files changed, 3 insertions(+) diff --git a/evmrpc/simulate_test.go b/evmrpc/simulate_test.go index a00df9b966..12a2768e73 100644 --- a/evmrpc/simulate_test.go +++ b/evmrpc/simulate_test.go @@ -50,6 +50,7 @@ func primeReceiptStore(t *testing.T, store receipt.ReceiptStore, latest int64) { } require.NoError(t, store.SetLatestVersion(latest)) require.NoError(t, store.SetEarliestVersion(1)) + require.Equal(t, int64(1), store.EarliestVersion()) } func (bc *bcFailClient) Block(ctx context.Context, h *int64) (*coretypes.ResultBlock, error) { diff --git a/evmrpc/state_test.go b/evmrpc/state_test.go index c066522da3..66a8cbd039 100644 --- a/evmrpc/state_test.go +++ b/evmrpc/state_test.go @@ -221,6 +221,7 @@ func TestGetProof(t *testing.T) { if store := testApp.EvmKeeper.ReceiptStore(); store != nil { require.NoError(t, store.SetLatestVersion(MockHeight8)) require.NoError(t, store.SetEarliestVersion(1)) + require.Equal(t, int64(1), store.EarliestVersion()) } client := &MockClient{} ctxProvider := func(height int64) sdk.Context { diff --git a/sei-db/ledger_db/receipt/receipt_store_test.go b/sei-db/ledger_db/receipt/receipt_store_test.go index 862453be97..fab8308218 100644 --- a/sei-db/ledger_db/receipt/receipt_store_test.go +++ b/sei-db/ledger_db/receipt/receipt_store_test.go @@ -109,6 +109,7 @@ func TestSetReceiptsAndGet(t *testing.T) { require.NoError(t, store.SetLatestVersion(10)) require.Equal(t, int64(10), store.LatestVersion()) require.NoError(t, store.SetEarliestVersion(1)) + require.Equal(t, int64(1), store.EarliestVersion()) } func TestReceiptStoreLegacyFallback(t *testing.T) { From 7f454f4a092b4e440e1d4ceb01e9dd500733f437 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Wed, 8 Apr 2026 17:05:05 -0700 Subject: [PATCH 4/6] added missing code coverage test --- evmrpc/block.go | 4 +-- evmrpc/height_availability_test.go | 26 +++++++++++++++++++ evmrpc/watermark_manager_test.go | 5 +++- sei-db/ledger_db/parquet/store_config_test.go | 11 ++++++++ .../ledger_db/receipt/parquet_store_test.go | 18 +++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/evmrpc/block.go b/evmrpc/block.go index 523886941c..7b4db48a3a 100644 --- a/evmrpc/block.go +++ b/evmrpc/block.go @@ -178,7 +178,7 @@ func (a *BlockAPI) GetBlockTransactionCountByNumber(ctx context.Context, number if err != nil { return nil, err } - if err := a.watermarks.EnsureReceiptHeightAvailable(ctx, block.Block.Height); err != nil { + if err = a.watermarks.EnsureReceiptHeightAvailable(ctx, block.Block.Height); err != nil { return nil, err } return a.getEvmTxCount(block), nil @@ -194,7 +194,7 @@ func (a *BlockAPI) GetBlockTransactionCountByHash(ctx context.Context, blockHash if err != nil { return nil, err } - if err := a.watermarks.EnsureReceiptHeightAvailable(ctx, block.Block.Height); err != nil { + if err = a.watermarks.EnsureReceiptHeightAvailable(ctx, block.Block.Height); err != nil { return nil, err } return a.getEvmTxCount(block), nil diff --git a/evmrpc/height_availability_test.go b/evmrpc/height_availability_test.go index 68a6dda94d..f0b42fde48 100644 --- a/evmrpc/height_availability_test.go +++ b/evmrpc/height_availability_test.go @@ -281,6 +281,32 @@ func TestLogFetcherSkipsUnavailableCachedBlock(t *testing.T) { } } +func TestGetBlockTransactionCountByNumberReceiptsPruned(t *testing.T) { + t.Parallel() + + client := newHeightTestClient(100, 1, 200) + rs := &fakeReceiptStore{latest: 200, earliest: 150} + watermarks := NewWatermarkManager(client, testCtxProvider, nil, rs) + api := NewBlockAPI(client, nil, testCtxProvider, testTxConfigProvider, ConnectionTypeHTTP, watermarks, nil, nil) + + _, err := api.GetBlockTransactionCountByNumber(context.Background(), rpc.BlockNumber(100)) + require.Error(t, err) + require.Contains(t, err.Error(), "receipts have been pruned") +} + +func TestGetBlockTransactionCountByHashReceiptsPruned(t *testing.T) { + t.Parallel() + + client := newHeightTestClient(100, 1, 200) + rs := &fakeReceiptStore{latest: 200, earliest: 150} + watermarks := NewWatermarkManager(client, testCtxProvider, nil, rs) + api := NewBlockAPI(client, nil, testCtxProvider, testTxConfigProvider, ConnectionTypeHTTP, watermarks, nil, nil) + + _, err := api.GetBlockTransactionCountByHash(context.Background(), common.HexToHash(highBlockHashHex)) + require.Error(t, err) + require.Contains(t, err.Error(), "receipts have been pruned") +} + func TestStateAPIGetProofUnavailableHeight(t *testing.T) { t.Parallel() diff --git a/evmrpc/watermark_manager_test.go b/evmrpc/watermark_manager_test.go index af03a6b1cf..deae651448 100644 --- a/evmrpc/watermark_manager_test.go +++ b/evmrpc/watermark_manager_test.go @@ -281,7 +281,10 @@ func (f *fakeReceiptStore) SetLatestVersion(version int64) error { return nil } -func (f *fakeReceiptStore) SetEarliestVersion(_ int64) error { return nil } +func (f *fakeReceiptStore) SetEarliestVersion(version int64) error { + f.earliest = version + return nil +} func (f *fakeReceiptStore) GetReceipt(sdk.Context, common.Hash) (*evmtypes.Receipt, error) { return nil, errors.New("not found") diff --git a/sei-db/ledger_db/parquet/store_config_test.go b/sei-db/ledger_db/parquet/store_config_test.go index 4b891a2ab6..5997668cdc 100644 --- a/sei-db/ledger_db/parquet/store_config_test.go +++ b/sei-db/ledger_db/parquet/store_config_test.go @@ -218,6 +218,17 @@ func TestLazyInitCreatesFileOnFirstWrite(t *testing.T) { require.Contains(t, logs[0], "logs_5000.parquet") } +func TestStoreEarliestVersion(t *testing.T) { + store, err := NewStore(StoreConfig{DBDirectory: t.TempDir()}) + require.NoError(t, err) + t.Cleanup(func() { _ = store.Close() }) + + require.Equal(t, int64(0), store.EarliestVersion()) + + store.SetEarliestVersion(55) + require.Equal(t, int64(55), store.EarliestVersion()) +} + func writeValidParquetFile(t *testing.T, dir, name string) { t.Helper() path := filepath.Join(dir, name) diff --git a/sei-db/ledger_db/receipt/parquet_store_test.go b/sei-db/ledger_db/receipt/parquet_store_test.go index c9faa5a615..a73b06b72f 100644 --- a/sei-db/ledger_db/receipt/parquet_store_test.go +++ b/sei-db/ledger_db/receipt/parquet_store_test.go @@ -402,6 +402,24 @@ func TestParquetPruneOldFiles(t *testing.T) { require.NoError(t, err) } +func TestParquetReceiptStoreEarliestVersion(t *testing.T) { + _, storeKey := newTestContext() + cfg := dbconfig.DefaultReceiptStoreConfig() + cfg.Backend = "parquet" + cfg.DBDirectory = t.TempDir() + + store, err := NewReceiptStore(cfg, storeKey) + require.NoError(t, err) + t.Cleanup(func() { _ = store.Close() }) + + pqStore := store.(*cachedReceiptStore).backend.(*parquetReceiptStore) + + require.Equal(t, int64(0), pqStore.EarliestVersion()) + + require.NoError(t, pqStore.SetEarliestVersion(77)) + require.Equal(t, int64(77), pqStore.EarliestVersion()) +} + func TestExtractBlockNumber(t *testing.T) { tests := []struct { path string From 20438fe1044866b11834a42e1c8351bf3007267d Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Thu, 9 Apr 2026 09:03:00 -0700 Subject: [PATCH 5/6] fixing the broken test --- sei-db/ledger_db/parquet/store_config_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sei-db/ledger_db/parquet/store_config_test.go b/sei-db/ledger_db/parquet/store_config_test.go index 6ffa70b5c8..55599c1732 100644 --- a/sei-db/ledger_db/parquet/store_config_test.go +++ b/sei-db/ledger_db/parquet/store_config_test.go @@ -237,7 +237,10 @@ func TestLazyInitCreatesFileOnFirstWrite(t *testing.T) { } func TestStoreEarliestVersion(t *testing.T) { - store, err := NewStore(StoreConfig{DBDirectory: t.TempDir()}) + store, err := NewStore(StoreConfig{ + DBDirectory: t.TempDir(), + DisableTxIndexLookup: true, + }) require.NoError(t, err) t.Cleanup(func() { _ = store.Close() }) From 64af9f327f66936ed77b42bdb5e8f1c8681094da Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Thu, 9 Apr 2026 12:13:50 -0700 Subject: [PATCH 6/6] remove redundant if check --- evmrpc/watermark_manager.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/evmrpc/watermark_manager.go b/evmrpc/watermark_manager.go index 616b689ba9..2ec33fb4ea 100644 --- a/evmrpc/watermark_manager.go +++ b/evmrpc/watermark_manager.go @@ -228,9 +228,6 @@ func (m *WatermarkManager) EnsureReceiptHeightAvailable(_ context.Context, heigh return nil } earliest := m.receiptStore.EarliestVersion() - if earliest <= 0 { - return nil - } if height < earliest { return fmt.Errorf("requested height %d receipts have been pruned; earliest available is %d", height, earliest) }