diff --git a/eth/filters/api.go b/eth/filters/api.go index 3ca35276fa..de932d01be 100644 --- a/eth/filters/api.go +++ b/eth/filters/api.go @@ -471,14 +471,90 @@ func (api *FilterAPI) NewFilter(crit FilterCriteria) (rpc.ID, error) { return logsSub.ID, nil } +func (api *FilterAPI) borLogsFilterForHistoricalQuery(ctx context.Context, blockHash *common.Hash, begin, end int64, addresses []common.Address, topics [][]common.Hash) *BorBlockLogsFilter { + borConfig := api.sys.backend.ChainConfig().Bor + if blockHash != nil { + return api.borLogsFilterForBlock(ctx, borConfig, *blockHash, addresses, topics) + } + + return api.borLogsFilterForRange(ctx, borConfig, begin, end, addresses, topics) +} + +func (api *FilterAPI) borLogsFilterForBlock(ctx context.Context, borConfig *params.BorConfig, blockHash common.Hash, addresses []common.Address, topics [][]common.Hash) *BorBlockLogsFilter { + if borConfig != nil && borConfig.MadhugiriBlock != nil { + header, err := api.sys.backend.HeaderByHash(ctx, blockHash) + if err != nil || header == nil || borConfig.IsMadhugiri(header.Number) { + return nil + } + } else if !api.borLogs { + return nil + } + + return NewBorBlockLogsFilter(api.sys.backend, borConfig, blockHash, addresses, topics) +} + +func (api *FilterAPI) borLogsFilterForRange(ctx context.Context, borConfig *params.BorConfig, begin, end int64, addresses []common.Address, topics [][]common.Hash) *BorBlockLogsFilter { + if !api.borLogs && (borConfig == nil || borConfig.MadhugiriBlock == nil) { + return nil + } + + resolvedBegin, ok := api.resolveHistoricalLogBlockNumber(ctx, begin) + if !ok { + return nil + } + resolvedEnd, ok := api.resolveHistoricalLogBlockNumber(ctx, end) + if !ok || resolvedBegin > resolvedEnd { + return nil + } + + if borConfig != nil && borConfig.MadhugiriBlock != nil { + madhugiriBlock := borConfig.MadhugiriBlock.Uint64() + if resolvedBegin >= madhugiriBlock { + return nil + } + if resolvedEnd >= madhugiriBlock { + resolvedEnd = madhugiriBlock - 1 + } + } + + return NewBorBlockLogsRangeFilter(api.sys.backend, borConfig, int64(resolvedBegin), int64(resolvedEnd), addresses, topics) +} + +// resolveHistoricalLogBlockNumber mirrors the standard filter's block-tag handling +// closely enough for Bor sidecar compatibility decisions, while keeping the +// canonical filter path authoritative for public RPC errors. +func (api *FilterAPI) resolveHistoricalLogBlockNumber(ctx context.Context, number int64) (uint64, bool) { + resolveHeaderNumber := func(number rpc.BlockNumber) (uint64, bool) { + header, err := api.sys.backend.HeaderByNumber(ctx, number) + if err != nil || header == nil { + return 0, false + } + return header.Number.Uint64(), true + } + + switch number { + case rpc.LatestBlockNumber.Int64(): + return resolveHeaderNumber(rpc.LatestBlockNumber) + case rpc.FinalizedBlockNumber.Int64(): + return resolveHeaderNumber(rpc.FinalizedBlockNumber) + case rpc.SafeBlockNumber.Int64(): + return resolveHeaderNumber(rpc.SafeBlockNumber) + case rpc.EarliestBlockNumber.Int64(): + return api.sys.backend.HistoryPruningCutoff(), true + default: + if number < 0 { + return 0, false + } + return uint64(number), true + } +} + // GetLogs returns logs matching the given argument that are stored within the state. func (api *FilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*types.Log, error) { if len(crit.Topics) > maxTopics { return nil, errExceedMaxTopics } - borConfig := api.sys.backend.ChainConfig().Bor - if api.logQueryLimit != 0 { if len(crit.Addresses) > api.logQueryLimit { return nil, errExceedLogQueryLimit @@ -492,8 +568,6 @@ func (api *FilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*type var filter *Filter - var borLogsFilter *BorBlockLogsFilter - if crit.BlockHash != nil { if crit.FromBlock != nil || crit.ToBlock != nil { return nil, errBlockHashWithRange @@ -501,10 +575,6 @@ func (api *FilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*type // Block filter requested, construct a single-shot filter filter = api.sys.NewBlockFilter(*crit.BlockHash, crit.Addresses, crit.Topics) - // Block bor filter - if api.borLogs { - borLogsFilter = NewBorBlockLogsFilter(api.sys.backend, borConfig, *crit.BlockHash, crit.Addresses, crit.Topics) - } } else { // Convert the RPC block numbers into internal representations begin := rpc.LatestBlockNumber.Int64() @@ -529,10 +599,6 @@ func (api *FilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*type } // Construct the range filter filter = api.sys.NewRangeFilter(begin, end, crit.Addresses, crit.Topics) - // Block bor filter - if api.borLogs { - borLogsFilter = NewBorBlockLogsRangeFilter(api.sys.backend, borConfig, begin, end, crit.Addresses, crit.Topics) - } } // Run the filter and return all the logs @@ -541,6 +607,7 @@ func (api *FilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*type return nil, err } + borLogsFilter := api.borLogsFilterForHistoricalQuery(ctx, crit.BlockHash, filter.begin, filter.end, crit.Addresses, crit.Topics) if borLogsFilter != nil { // Run the filter and return all the logs borBlockLogs, err := borLogsFilter.Logs(ctx) @@ -583,20 +650,11 @@ func (api *FilterAPI) GetFilterLogs(ctx context.Context, id rpc.ID) ([]*types.Lo return nil, errFilterNotFound } - borConfig := api.sys.backend.ChainConfig().Bor - var filter *Filter - var borLogsFilter *BorBlockLogsFilter - if f.crit.BlockHash != nil { // Block filter requested, construct a single-shot filter filter = api.sys.NewBlockFilter(*f.crit.BlockHash, f.crit.Addresses, f.crit.Topics) - - // Block bor filter - if api.borLogs { - borLogsFilter = NewBorBlockLogsFilter(api.sys.backend, borConfig, *f.crit.BlockHash, f.crit.Addresses, f.crit.Topics) - } } else { // Convert the RPC block numbers into internal representations begin := rpc.LatestBlockNumber.Int64() @@ -614,10 +672,6 @@ func (api *FilterAPI) GetFilterLogs(ctx context.Context, id rpc.ID) ([]*types.Lo } // Construct the range filter filter = api.sys.NewRangeFilter(begin, end, f.crit.Addresses, f.crit.Topics) - - if api.borLogs { - borLogsFilter = NewBorBlockLogsRangeFilter(api.sys.backend, borConfig, begin, end, f.crit.Addresses, f.crit.Topics) - } } // Run the filter and return all the logs logs, err := filter.Logs(ctx) @@ -625,6 +679,7 @@ func (api *FilterAPI) GetFilterLogs(ctx context.Context, id rpc.ID) ([]*types.Lo return nil, err } + borLogsFilter := api.borLogsFilterForHistoricalQuery(ctx, f.crit.BlockHash, filter.begin, filter.end, f.crit.Addresses, f.crit.Topics) if borLogsFilter != nil { // Run the filter and return all the logs borBlockLogs, err := borLogsFilter.Logs(ctx) diff --git a/eth/filters/filter_system_test.go b/eth/filters/filter_system_test.go index 07497c6805..3db8d573f5 100644 --- a/eth/filters/filter_system_test.go +++ b/eth/filters/filter_system_test.go @@ -52,6 +52,8 @@ type testBackend struct { pendingReceipts types.Receipts stateSyncFeed event.Feed + chainConfig *params.ChainConfig + historyCutoff uint64 } func (b *testBackend) SubscribeStateSyncEvent(ch chan<- core.StateSyncEvent) event.Subscription { @@ -59,6 +61,10 @@ func (b *testBackend) SubscribeStateSyncEvent(ch chan<- core.StateSyncEvent) eve } func (b *testBackend) ChainConfig() *params.ChainConfig { + if b.chainConfig != nil { + return b.chainConfig + } + return params.TestChainConfig } @@ -193,7 +199,7 @@ func (b *testBackend) GetBorBlockReceipt(_ context.Context, blockHash common.Has return &types.Receipt{}, nil } - receipt := rawdb.ReadBorReceipt(b.db, blockHash, number, nil) + receipt := rawdb.ReadBorReceipt(b.db, blockHash, number, b.ChainConfig()) if receipt == nil { return &types.Receipt{}, nil } @@ -238,7 +244,7 @@ func (b *testBackend) setPending(block *types.Block, receipts types.Receipts) { } func (b *testBackend) HistoryPruningCutoff() uint64 { - return 0 + return b.historyCutoff } func newTestFilterSystem(db ethdb.Database, cfg Config) (*testBackend, *FilterSystem) { @@ -248,6 +254,263 @@ func newTestFilterSystem(db ethdb.Database, cfg Config) (*testBackend, *FilterSy return backend, sys } +type historicalBorLogsHarness struct { + api *FilterAPI + preBlock *types.Block + missingBorBlock *types.Block + postBlock *types.Block + preBorAddr common.Address + preBorTopic common.Hash + postAddr common.Address + postTopic common.Hash + postBorAddr common.Address +} + +type historicalBorLogsFetcher struct { + name string + fetch func(*testing.T, *historicalBorLogsHarness, FilterCriteria) []*types.Log +} + +func makeReceiptWithTopic(addr common.Address, topic common.Hash) *types.Receipt { + receipt := types.NewReceipt(nil, false, 0) + receipt.Logs = []*types.Log{{ + Address: addr, + Topics: []common.Hash{topic}, + }} + receipt.Bloom = types.CreateBloom(receipt) + + return receipt +} + +func makeBorLogs(addr common.Address, topic common.Hash) []*types.Log { + return []*types.Log{{ + Address: addr, + Topics: []common.Hash{topic}, + }} +} + +func getHistoricalBorLogsFetchers() []historicalBorLogsFetcher { + return []historicalBorLogsFetcher{ + { + name: "GetLogs", + fetch: func(t *testing.T, harness *historicalBorLogsHarness, crit FilterCriteria) []*types.Log { + t.Helper() + + logs, err := harness.api.GetLogs(t.Context(), crit) + if err != nil { + t.Fatalf("GetLogs returned error: %v", err) + } + + return logs + }, + }, + { + name: "GetFilterLogs", + fetch: func(t *testing.T, harness *historicalBorLogsHarness, crit FilterCriteria) []*types.Log { + t.Helper() + + id, err := harness.api.NewFilter(crit) + if err != nil { + t.Fatalf("NewFilter returned error: %v", err) + } + t.Cleanup(func() { + harness.api.UninstallFilter(id) + }) + + logs, err := harness.api.GetFilterLogs(t.Context(), id) + if err != nil { + t.Fatalf("GetFilterLogs returned error: %v", err) + } + + return logs + }, + }, + } +} + +func requireHistoricalBorLogCount(t *testing.T, logs []*types.Log, want int, context string) { + t.Helper() + + if len(logs) != want { + t.Fatalf("%s: expected %d logs, got %d", context, want, len(logs)) + } +} + +func requireHistoricalBorSingleLog(t *testing.T, logs []*types.Log, wantAddr common.Address, wantTopic common.Hash, context string) { + t.Helper() + + requireHistoricalBorLogCount(t, logs, 1, context) + if logs[0].Address != wantAddr { + t.Fatalf("%s: expected log address %s, got %s", context, wantAddr.Hex(), logs[0].Address.Hex()) + } + if len(logs[0].Topics) != 1 || logs[0].Topics[0] != wantTopic { + t.Fatalf("%s: expected log topic %s, got %v", context, wantTopic.Hex(), logs[0].Topics) + } +} + +func requireHistoricalBorRangeLogs(t *testing.T, logs []*types.Log, harness *historicalBorLogsHarness, context string) { + t.Helper() + + requireHistoricalBorLogCount(t, logs, 2, context) + if logs[0].Address != harness.preBorAddr { + t.Fatalf("%s: expected first log to be pre-fork bor log %s, got %s", context, harness.preBorAddr.Hex(), logs[0].Address.Hex()) + } + if logs[1].Address != harness.postAddr { + t.Fatalf("%s: expected second log to be post-fork canonical log %s, got %s", context, harness.postAddr.Hex(), logs[1].Address.Hex()) + } + for _, log := range logs { + if log.Address == harness.postBorAddr { + t.Fatalf("%s: unexpected post-fork bor sidecar log leaked into standard historical query", context) + } + } +} + +func cloneBorHistoricalTestConfig(madhugiriBlock uint64) *params.ChainConfig { + cfgCopy := *params.TestChainConfig + borCopy := *params.BorTestChainConfig.Bor + sprintCopy := make(map[string]uint64, len(borCopy.Sprint)) + for k, v := range borCopy.Sprint { + sprintCopy[k] = v + } + borCopy.Sprint = sprintCopy + borCopy.Sprint["0"] = 1 + borCopy.MadhugiriBlock = new(big.Int).SetUint64(madhugiriBlock) + cfgCopy.Bor = &borCopy + + return &cfgCopy +} + +func writeBorReceiptForTest(t *testing.T, db ethdb.Database, _ *params.ChainConfig, block *types.Block, receipts types.Receipts, logs []*types.Log) { + t.Helper() + + logIndex := uint(0) + for _, receipt := range receipts { + logIndex += uint(len(receipt.Logs)) + } + + types.DeriveFieldsForBorLogs(logs, block.Hash(), block.NumberU64(), uint(len(block.Transactions())), logIndex) + + batch := db.NewBatch() + rawdb.WriteBorReceipt(batch, block.Hash(), block.NumberU64(), &types.ReceiptForStorage{ + Status: types.ReceiptStatusSuccessful, + Logs: logs, + }) + rawdb.WriteBorTxLookupEntry(batch, block.Hash(), block.NumberU64()) + + if err := batch.Write(); err != nil { + t.Fatalf("failed to write bor receipt: %v", err) + } +} + +func writeHistoricalBorChain(t *testing.T, db ethdb.Database, gspec *core.Genesis, chain []*types.Block, receipts []types.Receipts) { + t.Helper() + + gspec.MustCommit(db, triedb.NewDatabase(db, triedb.HashDefaults)) + + for idx := range chain { + block := chain[idx] + hash := block.Hash() + number := block.NumberU64() + + rawdb.WriteBlock(db, block) + rawdb.WriteCanonicalHash(db, hash, number) + rawdb.WriteHeadBlockHash(db, hash) + rawdb.WriteReceipts(db, hash, number, receipts[idx]) + } +} + +func newHistoricalBorLogsHarness(t *testing.T, enableBorLogs bool) *historicalBorLogsHarness { + t.Helper() + + var ( + db = rawdb.NewMemoryDatabase() + backend, sys = newTestFilterSystem(db, Config{}) + api = NewFilterAPI(sys, enableBorLogs) + cfg = cloneBorHistoricalTestConfig(4) + + preBorAddr = common.HexToAddress("0x1000000000000000000000000000000000000001") + postAddr = common.HexToAddress("0x2000000000000000000000000000000000000002") + postBorAddr = common.HexToAddress("0x3000000000000000000000000000000000000003") + + preBorTopic = common.HexToHash("0x1000000000000000000000000000000000000000000000000000000000000001") + postTopic = common.HexToHash("0x2000000000000000000000000000000000000000000000000000000000000002") + postBorTopic = common.HexToHash("0x3000000000000000000000000000000000000000000000000000000000000003") + + gspec = &core.Genesis{ + Config: cfg, + Alloc: types.GenesisAlloc{}, + BaseFee: big.NewInt(params.InitialBaseFee), + } + ) + + backend.chainConfig = cfg + api.SetChainConfig(cfg) + t.Cleanup(func() { _ = db.Close() }) + + _, chain, receipts := core.GenerateChainWithGenesis(gspec, ethash.NewFaker(), 5, func(i int, gen *core.BlockGen) { + if i == 3 { + receipt := makeReceiptWithTopic(postAddr, postTopic) + gen.AddUncheckedReceipt(receipt) + gen.AddUncheckedTx(types.NewTransaction(999, common.HexToAddress("0x999"), big.NewInt(999), 999, gen.BaseFee(), nil)) + } + }) + + writeHistoricalBorChain(t, db, gspec, chain, receipts) + + preBorLogs := makeBorLogs(preBorAddr, preBorTopic) + writeBorReceiptForTest(t, db, cfg, chain[1], receipts[1], preBorLogs) + + postBorLogs := makeBorLogs(postBorAddr, postBorTopic) + writeBorReceiptForTest(t, db, cfg, chain[3], receipts[3], postBorLogs) + + backend.startFilterMaps(16, false, filtermaps.RangeTestParams) + t.Cleanup(backend.stopFilterMaps) + + return &historicalBorLogsHarness{ + api: api, + preBlock: chain[1], + missingBorBlock: chain[2], + postBlock: chain[3], + preBorAddr: preBorAddr, + preBorTopic: preBorTopic, + postAddr: postAddr, + postTopic: postTopic, + postBorAddr: postBorAddr, + } +} + +func newHistoricalBorDecisionAPI(t *testing.T, enableBorLogs bool, cfg *params.ChainConfig) (*testBackend, *FilterAPI) { + t.Helper() + + db := rawdb.NewMemoryDatabase() + backend, sys := newTestFilterSystem(db, Config{}) + backend.chainConfig = cfg + + api := NewFilterAPI(sys, enableBorLogs) + if cfg != nil { + api.SetChainConfig(cfg) + } + + t.Cleanup(func() { _ = db.Close() }) + + return backend, api +} + +func writeHistoricalBorDecisionChain(t *testing.T, backend *testBackend, cfg *params.ChainConfig, blocks int) []*types.Block { + t.Helper() + + gspec := &core.Genesis{ + Config: cfg, + Alloc: types.GenesisAlloc{}, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + _, chain, receipts := core.GenerateChainWithGenesis(gspec, ethash.NewFaker(), blocks, func(i int, gen *core.BlockGen) {}) + writeHistoricalBorChain(t, backend.db, gspec, chain, receipts) + + return chain +} + // TestBlockSubscription tests if a block subscription returns block hashes for posted chain events. // It creates multiple subscriptions: // - one at the start and should receive all posted chain events and a second (blockHashes) @@ -591,6 +854,201 @@ func TestInvalidGetRangeLogsRequest(t *testing.T) { } } +func TestHistoricalQueriesAutoMergePreMadhugiriBorLogs(t *testing.T) { + t.Parallel() + + for _, fetcher := range getHistoricalBorLogsFetchers() { + t.Run(fetcher.name, func(t *testing.T) { + harness := newHistoricalBorLogsHarness(t, false) + preBlockHash := harness.preBlock.Hash() + missingBorBlockHash := harness.missingBorBlock.Hash() + + logs := fetcher.fetch(t, harness, FilterCriteria{BlockHash: &preBlockHash}) + requireHistoricalBorSingleLog(t, logs, harness.preBorAddr, harness.preBorTopic, fetcher.name+" pre-fork blockHash") + + logs = fetcher.fetch(t, harness, FilterCriteria{BlockHash: &missingBorBlockHash}) + requireHistoricalBorLogCount(t, logs, 0, fetcher.name+" missing-sidecar blockHash") + }) + } +} + +func TestHistoricalQueriesKeepPostMadhugiriResultsCanonical(t *testing.T) { + t.Parallel() + + for _, fetcher := range getHistoricalBorLogsFetchers() { + t.Run(fetcher.name, func(t *testing.T) { + harness := newHistoricalBorLogsHarness(t, false) + postBlockHash := harness.postBlock.Hash() + + logs := fetcher.fetch(t, harness, FilterCriteria{BlockHash: &postBlockHash}) + requireHistoricalBorSingleLog(t, logs, harness.postAddr, harness.postTopic, fetcher.name+" post-fork blockHash") + + logs = fetcher.fetch(t, harness, FilterCriteria{ + FromBlock: new(big.Int).SetUint64(harness.preBlock.NumberU64()), + ToBlock: big.NewInt(rpc.LatestBlockNumber.Int64()), + }) + requireHistoricalBorRangeLogs(t, logs, harness, fetcher.name+" cross-fork range") + }) + } +} + +func TestHistoricalQueryHelperGuards(t *testing.T) { + t.Parallel() + + t.Run("BlockFilterDisabledWithoutBorSupport", func(t *testing.T) { + _, api := newHistoricalBorDecisionAPI(t, false, nil) + + filter := api.borLogsFilterForBlock(t.Context(), nil, common.HexToHash("0x1"), nil, nil) + if filter != nil { + t.Fatal("expected nil bor block filter when bor logs are disabled and no fork config is available") + } + }) + + t.Run("BlockFilterSkipsMissingHistoricalHeader", func(t *testing.T) { + cfg := cloneBorHistoricalTestConfig(4) + _, api := newHistoricalBorDecisionAPI(t, false, cfg) + + filter := api.borLogsFilterForBlock(t.Context(), cfg.Bor, common.HexToHash("0xdeadbeef"), nil, nil) + if filter != nil { + t.Fatal("expected nil bor block filter when the historical block header cannot be resolved") + } + }) + + t.Run("RangeFilterDisabledWithoutBorSupport", func(t *testing.T) { + _, api := newHistoricalBorDecisionAPI(t, false, nil) + + filter := api.borLogsFilterForRange(t.Context(), nil, 1, 2, nil, nil) + if filter != nil { + t.Fatal("expected nil bor range filter when bor logs are disabled and no fork config is available") + } + }) + + t.Run("RangeFilterDisabledWithoutMadhugiriFork", func(t *testing.T) { + cfgCopy := *params.TestChainConfig + borCopy := *params.BorTestChainConfig.Bor + sprintCopy := make(map[string]uint64, len(borCopy.Sprint)) + for k, v := range borCopy.Sprint { + sprintCopy[k] = v + } + borCopy.Sprint = sprintCopy + borCopy.MadhugiriBlock = nil + cfgCopy.Bor = &borCopy + + _, api := newHistoricalBorDecisionAPI(t, false, &cfgCopy) + filter := api.borLogsFilterForRange(t.Context(), cfgCopy.Bor, 1, 2, nil, nil) + if filter != nil { + t.Fatal("expected nil bor range filter when bor logs are disabled and no Madhugiri fork boundary exists") + } + }) + + t.Run("RangeFilterSkipsUnresolvedBegin", func(t *testing.T) { + cfg := cloneBorHistoricalTestConfig(4) + _, api := newHistoricalBorDecisionAPI(t, false, cfg) + + filter := api.borLogsFilterForRange(t.Context(), cfg.Bor, rpc.SafeBlockNumber.Int64(), 1, nil, nil) + if filter != nil { + t.Fatal("expected nil bor range filter when the beginning block tag cannot be resolved") + } + }) + + t.Run("RangeFilterSkipsUnresolvedEnd", func(t *testing.T) { + cfg := cloneBorHistoricalTestConfig(4) + _, api := newHistoricalBorDecisionAPI(t, false, cfg) + + filter := api.borLogsFilterForRange(t.Context(), cfg.Bor, 1, rpc.SafeBlockNumber.Int64(), nil, nil) + if filter != nil { + t.Fatal("expected nil bor range filter when the ending block tag cannot be resolved") + } + }) + + t.Run("RangeFilterSkipsInvertedResolvedRange", func(t *testing.T) { + cfg := cloneBorHistoricalTestConfig(4) + _, api := newHistoricalBorDecisionAPI(t, false, cfg) + + filter := api.borLogsFilterForRange(t.Context(), cfg.Bor, 3, 2, nil, nil) + if filter != nil { + t.Fatal("expected nil bor range filter when the resolved range is inverted") + } + }) + + t.Run("RangeFilterSkipsPostForkOnlyRanges", func(t *testing.T) { + cfg := cloneBorHistoricalTestConfig(4) + _, api := newHistoricalBorDecisionAPI(t, false, cfg) + + filter := api.borLogsFilterForRange(t.Context(), cfg.Bor, 4, 5, nil, nil) + if filter != nil { + t.Fatal("expected nil bor range filter when the range starts at Madhugiri or later") + } + }) +} + +func TestResolveHistoricalLogBlockNumber(t *testing.T) { + t.Parallel() + + cfg := cloneBorHistoricalTestConfig(8) + backend, api := newHistoricalBorDecisionAPI(t, false, cfg) + chain := writeHistoricalBorDecisionChain(t, backend, cfg, 5) + backend.historyCutoff = chain[1].NumberU64() + rawdb.WriteFinalizedBlockHash(backend.db, chain[2].Hash()) + + testCases := []struct { + name string + number int64 + want uint64 + ok bool + }{ + { + name: "Latest", + number: rpc.LatestBlockNumber.Int64(), + want: chain[len(chain)-1].NumberU64(), + ok: true, + }, + { + name: "Finalized", + number: rpc.FinalizedBlockNumber.Int64(), + want: chain[2].NumberU64(), + ok: true, + }, + { + name: "SafeMissing", + number: rpc.SafeBlockNumber.Int64(), + want: 0, + ok: false, + }, + { + name: "EarliestUsesHistoryCutoff", + number: rpc.EarliestBlockNumber.Int64(), + want: backend.historyCutoff, + ok: true, + }, + { + name: "NegativeRejected", + number: -42, + want: 0, + ok: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, ok := api.resolveHistoricalLogBlockNumber(t.Context(), tc.number) + if ok != tc.ok || got != tc.want { + t.Fatalf("resolveHistoricalLogBlockNumber(%d) = (%d, %t), want (%d, %t)", tc.number, got, ok, tc.want, tc.ok) + } + }) + } + + t.Run("MissingFinalizedHeaderReturnsFalse", func(t *testing.T) { + emptyBackend, emptyAPI := newHistoricalBorDecisionAPI(t, false, cfg) + rawdb.WriteFinalizedBlockHash(emptyBackend.db, common.HexToHash("0xbeef")) + + got, ok := emptyAPI.resolveHistoricalLogBlockNumber(t.Context(), rpc.FinalizedBlockNumber.Int64()) + if ok || got != 0 { + t.Fatalf("resolveHistoricalLogBlockNumber(finalized) = (%d, %t), want (0, false) when the finalized header is missing", got, ok) + } + }) +} + // TestExceedLogQueryLimit tests getLogs with too many addresses or topics func TestExceedLogQueryLimit(t *testing.T) { t.Parallel()