This commit is contained in:
locoholy 2026-05-21 21:55:10 -07:00 committed by GitHub
commit 07f67a3f45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 168 additions and 12 deletions

View file

@ -484,8 +484,12 @@ func (api *FilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*type
if begin >= 0 && begin < int64(api.events.backend.HistoryPruningCutoff()) { if begin >= 0 && begin < int64(api.events.backend.HistoryPruningCutoff()) {
return nil, &history.PrunedHistoryError{} return nil, &history.PrunedHistoryError{}
} }
// Construct the range filter // Construct the range filter, respecting optional result-count limit
filter = api.sys.NewRangeFilter(begin, end, crit.Addresses, crit.Topics, api.rangeLimit) if crit.Limit != nil && *crit.Limit > 0 {
filter = api.sys.NewRangeFilterWithLimit(begin, end, crit.Addresses, crit.Topics, api.rangeLimit, *crit.Limit)
} else {
filter = api.sys.NewRangeFilter(begin, end, crit.Addresses, crit.Topics, api.rangeLimit)
}
} }
// Run the filter and return all the logs // Run the filter and return all the logs
@ -493,7 +497,7 @@ func (api *FilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*type
if err != nil { if err != nil {
return nil, err return nil, err
} }
return returnLogs(logs), err return returnLogs(logs), nil
} }
// UninstallFilter removes the filter with the given filter id. // UninstallFilter removes the filter with the given filter id.
@ -627,6 +631,7 @@ func (args *FilterCriteria) UnmarshalJSON(data []byte) error {
ToBlock *rpc.BlockNumber `json:"toBlock"` ToBlock *rpc.BlockNumber `json:"toBlock"`
Addresses interface{} `json:"address"` Addresses interface{} `json:"address"`
Topics []interface{} `json:"topics"` Topics []interface{} `json:"topics"`
Limit *hexutil.Uint64 `json:"limit"`
} }
var raw input var raw input
@ -725,6 +730,11 @@ func (args *FilterCriteria) UnmarshalJSON(data []byte) error {
} }
} }
if raw.Limit != nil {
limitVal := uint64(*raw.Limit)
args.Limit = &limitVal
}
return nil return nil
} }

View file

@ -29,6 +29,7 @@ func TestUnmarshalJSONNewFilterArgs(t *testing.T) {
var ( var (
fromBlock rpc.BlockNumber = 0x123435 fromBlock rpc.BlockNumber = 0x123435
toBlock rpc.BlockNumber = 0xabcdef toBlock rpc.BlockNumber = 0xabcdef
limit uint64 = 0x2
address0 = common.HexToAddress("70c87d191324e6712a591f304b4eedef6ad9bb9d") address0 = common.HexToAddress("70c87d191324e6712a591f304b4eedef6ad9bb9d")
address1 = common.HexToAddress("9b2055d370f73ec7d8a03e965129118dc8f5bf83") address1 = common.HexToAddress("9b2055d370f73ec7d8a03e965129118dc8f5bf83")
topic0 = common.HexToHash("3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1ca") topic0 = common.HexToHash("3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1ca")
@ -53,6 +54,9 @@ func TestUnmarshalJSONNewFilterArgs(t *testing.T) {
if len(test0.Topics) != 0 { if len(test0.Topics) != 0 {
t.Fatalf("expected 0 topics, got %d topics", len(test0.Topics)) t.Fatalf("expected 0 topics, got %d topics", len(test0.Topics))
} }
if test0.Limit != nil {
t.Fatalf("expected nil limit, got %d", *test0.Limit)
}
// from, to block number // from, to block number
var test1 FilterCriteria var test1 FilterCriteria
@ -66,6 +70,22 @@ func TestUnmarshalJSONNewFilterArgs(t *testing.T) {
if test1.ToBlock.Int64() != toBlock.Int64() { if test1.ToBlock.Int64() != toBlock.Int64() {
t.Fatalf("expected ToBlock %d, got %d", toBlock, test1.ToBlock) t.Fatalf("expected ToBlock %d, got %d", toBlock, test1.ToBlock)
} }
if test1.Limit != nil {
t.Fatalf("expected nil limit, got %d", *test1.Limit)
}
// limit
var testLimit FilterCriteria
vector = fmt.Sprintf(`{"limit":"0x%x"}`, limit)
if err := json.Unmarshal([]byte(vector), &testLimit); err != nil {
t.Fatal(err)
}
if testLimit.Limit == nil {
t.Fatal("expected non-nil limit")
}
if *testLimit.Limit != limit {
t.Fatalf("expected limit %d, got %d", limit, *testLimit.Limit)
}
// single address // single address
var test2 FilterCriteria var test2 FilterCriteria

View file

@ -45,6 +45,7 @@ type Filter struct {
rangeLogsTestHook chan rangeLogsTestEvent rangeLogsTestHook chan rangeLogsTestEvent
rangeLimit uint64 rangeLimit uint64
limit uint64 // Maximum number of logs to return; 0 means no limit
} }
// NewRangeFilter creates a new filter which uses a bloom filter on blocks to // NewRangeFilter creates a new filter which uses a bloom filter on blocks to
@ -59,6 +60,14 @@ func (sys *FilterSystem) NewRangeFilter(begin, end int64, addresses []common.Add
return filter return filter
} }
// NewRangeFilterWithLimit creates a new filter with a maximum result count.
// When limit is non-zero, the returned result is capped to that many logs.
func (sys *FilterSystem) NewRangeFilterWithLimit(begin, end int64, addresses []common.Address, topics [][]common.Hash, rangeLimit, limit uint64) *Filter {
filter := sys.NewRangeFilter(begin, end, addresses, topics, rangeLimit)
filter.limit = limit
return filter
}
// NewBlockFilter creates a new filter which directly inspects the contents of // NewBlockFilter creates a new filter which directly inspects the contents of
// a block to figure out whether it is interesting or not. // a block to figure out whether it is interesting or not.
func (sys *FilterSystem) NewBlockFilter(block common.Hash, addresses []common.Address, topics [][]common.Hash) *Filter { func (sys *FilterSystem) NewBlockFilter(block common.Hash, addresses []common.Address, topics [][]common.Hash) *Filter {
@ -148,7 +157,14 @@ func (f *Filter) Logs(ctx context.Context) ([]*types.Log, error) {
if f.rangeLimit != 0 && (end-begin) > f.rangeLimit { if f.rangeLimit != 0 && (end-begin) > f.rangeLimit {
return nil, invalidParamsErr("exceed maximum block range %d", f.rangeLimit) return nil, invalidParamsErr("exceed maximum block range %d", f.rangeLimit)
} }
return f.rangeLogs(ctx, begin, end) logs, err := f.rangeLogs(ctx, begin, end)
if err != nil {
return nil, err
}
if f.limit > 0 && uint64(len(logs)) > f.limit {
logs = logs[:f.limit]
}
return logs, nil
} }
const ( const (
@ -450,6 +466,11 @@ func (f *Filter) unindexedLogs(ctx context.Context, chainView *filtermaps.ChainV
return matches, err return matches, err
} }
matches = append(matches, found...) matches = append(matches, found...)
// Early exit once the limit is reached on the unindexed path, avoiding
// a full raw-block scan when callers only need the first N matches.
if f.limit > 0 && uint64(len(matches)) >= f.limit {
break
}
} }
log.Debug("Performed unindexed log search", "begin", begin, "end", end, "matches", len(matches), "elapsed", common.PrettyDuration(time.Since(start))) log.Debug("Performed unindexed log search", "begin", begin, "end", end, "matches", len(matches), "elapsed", common.PrettyDuration(time.Since(start)))
return matches, nil return matches, nil

View file

@ -48,18 +48,30 @@ func makeReceipt(addr common.Address) *types.Receipt {
} }
func BenchmarkFiltersIndexed(b *testing.B) { func BenchmarkFiltersIndexed(b *testing.B) {
benchmarkFilters(b, 0, false) benchmarkFilters(b, 0, false, 0, 4)
} }
func BenchmarkFiltersHalfIndexed(b *testing.B) { func BenchmarkFiltersHalfIndexed(b *testing.B) {
benchmarkFilters(b, 50000, false) benchmarkFilters(b, 50000, false, 0, 4)
} }
func BenchmarkFiltersUnindexed(b *testing.B) { func BenchmarkFiltersUnindexed(b *testing.B) {
benchmarkFilters(b, 0, true) benchmarkFilters(b, 0, true, 0, 4)
} }
func benchmarkFilters(b *testing.B, history uint64, noHistory bool) { func BenchmarkFiltersIndexedLimit1(b *testing.B) {
benchmarkFilters(b, 0, false, 1, 1)
}
func BenchmarkFiltersHalfIndexedLimit1(b *testing.B) {
benchmarkFilters(b, 50000, false, 1, 1)
}
func BenchmarkFiltersUnindexedLimit1(b *testing.B) {
benchmarkFilters(b, 0, true, 1, 1)
}
func benchmarkFilters(b *testing.B, history uint64, noHistory bool, limit uint64, expected int) {
var ( var (
db = rawdb.NewMemoryDatabase() db = rawdb.NewMemoryDatabase()
backend, sys = newTestFilterSystem(db, Config{}) backend, sys = newTestFilterSystem(db, Config{})
@ -110,12 +122,18 @@ func benchmarkFilters(b *testing.B, history uint64, noHistory bool) {
backend.startFilterMaps(history, noHistory, filtermaps.DefaultParams) backend.startFilterMaps(history, noHistory, filtermaps.DefaultParams)
defer backend.stopFilterMaps() defer backend.stopFilterMaps()
filter := sys.NewRangeFilter(0, int64(rpc.LatestBlockNumber), []common.Address{addr1, addr2, addr3, addr4}, nil, 0) var filter *Filter
if limit > 0 {
filter = sys.NewRangeFilterWithLimit(0, int64(rpc.LatestBlockNumber), []common.Address{addr1, addr2, addr3, addr4}, nil, 0, limit)
} else {
filter = sys.NewRangeFilter(0, int64(rpc.LatestBlockNumber), []common.Address{addr1, addr2, addr3, addr4}, nil, 0)
}
ctx := context.Background()
for b.Loop() { for b.Loop() {
filter.begin = 0 filter.begin = 0
logs, _ := filter.Logs(context.Background()) logs, _ := filter.Logs(ctx)
if len(logs) != 4 { if len(logs) != expected {
b.Fatal("expected 4 logs, got", len(logs)) b.Fatal("expected", expected, "logs, got", len(logs))
} }
} }
} }
@ -132,6 +150,88 @@ func TestFiltersUnindexed(t *testing.T) {
testFilters(t, 0, true) testFilters(t, 0, true)
} }
func TestFiltersIndexedWithLimit(t *testing.T) {
testFiltersWithLimit(t, 0, false)
}
func TestFiltersHalfIndexedWithLimit(t *testing.T) {
testFiltersWithLimit(t, 500, false)
}
func TestFiltersUnindexedWithLimit(t *testing.T) {
testFiltersWithLimit(t, 0, true)
}
func testFiltersWithLimit(t *testing.T, history uint64, noHistory bool) {
const (
totalBlocks = 1000
queryLimit uint64 = 2
)
var (
db = rawdb.NewMemoryDatabase()
backend, sys = newTestFilterSystem(db, Config{})
target = common.BytesToAddress([]byte("target"))
other = common.BytesToAddress([]byte("other"))
gspec = &core.Genesis{
BaseFee: big.NewInt(params.InitialBaseFee),
Config: params.TestChainConfig,
}
)
defer db.Close()
_, err := gspec.Commit(db, triedb.NewDatabase(db, nil), nil)
if err != nil {
t.Fatal(err)
}
addReceiptLog := func(gen *core.BlockGen, nonce uint64, addr common.Address) {
gen.AddUncheckedReceipt(makeReceipt(addr))
gen.AddUncheckedTx(types.NewTransaction(nonce, common.HexToAddress("0x999"), big.NewInt(1), 21000, gen.BaseFee(), nil))
}
chain, receipts := core.GenerateChain(gspec.Config, gspec.ToBlock(), ethash.NewFaker(), db, totalBlocks, func(i int, gen *core.BlockGen) {
switch i {
case 2, 5, 900:
addReceiptLog(gen, 999, target)
case 990:
addReceiptLog(gen, 1000, other)
}
})
for i, block := range chain {
rawdb.WriteBlock(db, block)
rawdb.WriteCanonicalHash(db, block.Hash(), block.NumberU64())
rawdb.WriteHeadBlockHash(db, block.Hash())
rawdb.WriteReceipts(db, block.Hash(), block.NumberU64(), receipts[i])
}
backend.startFilterMaps(history, noHistory, filtermaps.DefaultParams)
defer backend.stopFilterMaps()
allFilter := sys.NewRangeFilter(0, int64(rpc.LatestBlockNumber), []common.Address{target}, nil, 0)
allLogs, err := allFilter.Logs(context.Background())
if err != nil {
t.Fatal(err)
}
if len(allLogs) != 3 {
t.Fatalf("expected 3 total logs, got %d", len(allLogs))
}
limitedFilter := sys.NewRangeFilterWithLimit(0, int64(rpc.LatestBlockNumber), []common.Address{target}, nil, 0, queryLimit)
limitedLogs, err := limitedFilter.Logs(context.Background())
if err != nil {
t.Fatal(err)
}
if len(limitedLogs) != int(queryLimit) {
t.Fatalf("expected %d logs with limit, got %d", queryLimit, len(limitedLogs))
}
if limitedLogs[0].BlockNumber != 3 || limitedLogs[1].BlockNumber != 6 {
t.Fatalf("unexpected limited block numbers: got [%d %d], want [3 6]", limitedLogs[0].BlockNumber, limitedLogs[1].BlockNumber)
}
for i, log := range limitedLogs {
if log.Address != target {
t.Fatalf("unexpected address in limited log #%d: got %x, want %x", i, log.Address, target)
}
}
}
func testFilters(t *testing.T, history uint64, noHistory bool) { func testFilters(t *testing.T, history uint64, noHistory bool) {
var ( var (
db = rawdb.NewMemoryDatabase() db = rawdb.NewMemoryDatabase()

View file

@ -206,6 +206,11 @@ type FilterQuery struct {
// {{A}, {B}} matches topic A in first position AND B in second position // {{A}, {B}} matches topic A in first position AND B in second position
// {{A, B}, {C, D}} matches topic (A OR B) in first position AND (C OR D) in second position // {{A, B}, {C, D}} matches topic (A OR B) in first position AND (C OR D) in second position
Topics [][]common.Hash Topics [][]common.Hash
// Limit caps the maximum number of logs returned.
// When non-nil and non-zero, at most Limit matching logs are returned.
// Example: set Limit to 1 to receive a single matching event.
Limit *uint64
} }
// LogFilterer provides access to contract log events using a one-off query or continuous // LogFilterer provides access to contract log events using a one-off query or continuous