Skip to content

Commit eb69ce1

Browse files
s1nashekhirin
authored andcommitted
eth/filters: avoid block body retrieval when no matching logs (ethereum#25199)
Logs stored on disk have minimal information. Contextual information such as block number, index of log in block, index of transaction in block are filled in upon request. We can fill in all these fields only having the block header and list of receipts. But determining the transaction hash of a log requires the block body. The goal of this PR is postponing this retrieval until we are sure we the transaction hash. It happens often that the header bloom filter signals there might be matches in a block, but after actually checking them reveals the logs do not match. We want to avoid fetching the body in this case. Note that this changes the semantics of Backend.GetLogs. Downstream callers of GetLogs now assume log context fields have not been derived, and need to call DeriveFields on the logs if necessary.
1 parent 0173547 commit eb69ce1

11 files changed

+283
-97
lines changed

accounts/abi/bind/backends/simulated.go

+7
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,13 @@ func (fb *filterBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*t
872872
return fb.bc.GetHeaderByHash(hash), nil
873873
}
874874

875+
func (fb *filterBackend) GetBody(ctx context.Context, hash common.Hash, number rpc.BlockNumber) (*types.Body, error) {
876+
if body := fb.bc.GetBody(hash); body != nil {
877+
return body, nil
878+
}
879+
return nil, errors.New("block body not found")
880+
}
881+
875882
func (fb *filterBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) {
876883
return fb.backend.pendingBlock, fb.backend.pendingReceipts
877884
}

core/rawdb/accessors_chain.go

+3-12
Original file line numberDiff line numberDiff line change
@@ -714,9 +714,9 @@ func deriveLogFields(receipts []*receiptLogs, hash common.Hash, number uint64, t
714714
return nil
715715
}
716716

717-
// ReadLogs retrieves the logs for all transactions in a block. The log fields
718-
// are populated with metadata. In case the receipts or the block body
719-
// are not found, a nil is returned.
717+
// ReadLogs retrieves the logs for all transactions in a block. In case
718+
// receipts is not found, a nil is returned.
719+
// Note: ReadLogs does not derive unstored log fields.
720720
func ReadLogs(db ethdb.Reader, hash common.Hash, number uint64, config *params.ChainConfig) [][]*types.Log {
721721
// Retrieve the flattened receipt slice
722722
data := ReadReceiptsRLP(db, hash, number)
@@ -729,15 +729,6 @@ func ReadLogs(db ethdb.Reader, hash common.Hash, number uint64, config *params.C
729729
return nil
730730
}
731731

732-
body := ReadBody(db, hash, number)
733-
if body == nil {
734-
log.Error("Missing body but have receipt", "hash", hash, "number", number)
735-
return nil
736-
}
737-
if err := deriveLogFields(receipts, hash, number, body.Transactions); err != nil {
738-
log.Error("Failed to derive block receipts fields", "hash", hash, "number", number, "err", err)
739-
return nil
740-
}
741732
logs := make([][]*types.Log, len(receipts))
742733
for i, receipt := range receipts {
743734
logs[i] = receipt.Logs

core/rawdb/accessors_chain_test.go

-4
Original file line numberDiff line numberDiff line change
@@ -750,10 +750,6 @@ func TestReadLogs(t *testing.T) {
750750
t.Fatalf("unexpected number of logs[1] returned, have %d want %d", have, want)
751751
}
752752

753-
// Fill in log fields so we can compare their rlp encoding
754-
if err := types.Receipts(receipts).DeriveFields(params.TestChainConfig, hash, 0, body.Transactions); err != nil {
755-
t.Fatal(err)
756-
}
757753
for i, pr := range receipts {
758754
for j, pl := range pr.Logs {
759755
rlpHave, err := rlp.EncodeToBytes(newFullLogRLP(logs[i][j]))

eth/api_backend.go

+11
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,17 @@ func (b *EthAPIBackend) BlockByHash(ctx context.Context, hash common.Hash) (*typ
135135
return b.eth.blockchain.GetBlockByHash(hash), nil
136136
}
137137

138+
// GetBody returns body of a block. It does not resolve special block numbers.
139+
func (b *EthAPIBackend) GetBody(ctx context.Context, hash common.Hash, number rpc.BlockNumber) (*types.Body, error) {
140+
if number < 0 || hash == (common.Hash{}) {
141+
return nil, errors.New("invalid arguments; expect hash and no special block numbers")
142+
}
143+
if body := b.eth.blockchain.GetBody(hash); body != nil {
144+
return body, nil
145+
}
146+
return nil, errors.New("block body not found")
147+
}
148+
138149
func (b *EthAPIBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) {
139150
if blockNr, ok := blockNrOrHash.Number(); ok {
140151
return b.BlockByNumber(ctx, blockNr)

eth/filters/filter.go

+30-38
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (f *Filter) Logs(ctx context.Context) ([]*types.Log, error) {
104104
if header == nil {
105105
return nil, errors.New("unknown block")
106106
}
107-
return f.blockLogs(ctx, header, false)
107+
return f.blockLogs(ctx, header)
108108
}
109109
// Short-cut if all we care about is pending logs
110110
if f.begin == rpc.PendingBlockNumber.Int64() {
@@ -216,7 +216,7 @@ func (f *Filter) indexedLogs(ctx context.Context, end uint64) ([]*types.Log, err
216216
if header == nil || err != nil {
217217
return logs, err
218218
}
219-
found, err := f.blockLogs(ctx, header, true)
219+
found, err := f.checkMatches(ctx, header)
220220
if err != nil {
221221
return logs, err
222222
}
@@ -241,7 +241,7 @@ func (f *Filter) unindexedLogs(ctx context.Context, end uint64) ([]*types.Log, e
241241
if header == nil || err != nil {
242242
return logs, err
243243
}
244-
found, err := f.blockLogs(ctx, header, false)
244+
found, err := f.blockLogs(ctx, header)
245245
if err != nil {
246246
return logs, err
247247
}
@@ -251,46 +251,46 @@ func (f *Filter) unindexedLogs(ctx context.Context, end uint64) ([]*types.Log, e
251251
}
252252

253253
// blockLogs returns the logs matching the filter criteria within a single block.
254-
func (f *Filter) blockLogs(ctx context.Context, header *types.Header, skipBloom bool) ([]*types.Log, error) {
255-
// Fast track: no filtering criteria
256-
if len(f.addresses) == 0 && len(f.topics) == 0 {
257-
list, err := f.sys.cachedGetLogs(ctx, header.Hash(), header.Number.Uint64())
258-
if err != nil {
259-
return nil, err
260-
}
261-
return flatten(list), nil
262-
} else if skipBloom || bloomFilter(header.Bloom, f.addresses, f.topics) {
254+
func (f *Filter) blockLogs(ctx context.Context, header *types.Header) ([]*types.Log, error) {
255+
if bloomFilter(header.Bloom, f.addresses, f.topics) {
263256
return f.checkMatches(ctx, header)
264257
}
265258
return nil, nil
266259
}
267260

268261
// checkMatches checks if the receipts belonging to the given header contain any log events that
269262
// match the filter criteria. This function is called when the bloom filter signals a potential match.
263+
// skipFilter signals all logs of the given block are requested.
270264
func (f *Filter) checkMatches(ctx context.Context, header *types.Header) ([]*types.Log, error) {
271-
logsList, err := f.sys.cachedGetLogs(ctx, header.Hash(), header.Number.Uint64())
265+
hash := header.Hash()
266+
// Logs in cache are partially filled with context data
267+
// such as tx index, block hash, etc.
268+
// Notably tx hash is NOT filled in because it needs
269+
// access to block body data.
270+
cached, err := f.sys.cachedLogElem(ctx, hash, header.Number.Uint64())
272271
if err != nil {
273272
return nil, err
274273
}
275-
276-
unfiltered := flatten(logsList)
277-
logs := filterLogs(unfiltered, nil, nil, f.addresses, f.topics)
278-
if len(logs) > 0 {
279-
// We have matching logs, check if we need to resolve full logs via the light client
280-
if logs[0].TxHash == (common.Hash{}) {
281-
receipts, err := f.sys.backend.GetReceipts(ctx, header.Hash())
282-
if err != nil {
283-
return nil, err
284-
}
285-
unfiltered = unfiltered[:0]
286-
for _, receipt := range receipts {
287-
unfiltered = append(unfiltered, receipt.Logs...)
288-
}
289-
logs = filterLogs(unfiltered, nil, nil, f.addresses, f.topics)
290-
}
274+
logs := filterLogs(cached.logs, nil, nil, f.addresses, f.topics)
275+
if len(logs) == 0 {
276+
return nil, nil
277+
}
278+
// Most backends will deliver un-derived logs, but check nevertheless.
279+
if len(logs) > 0 && logs[0].TxHash != (common.Hash{}) {
291280
return logs, nil
292281
}
293-
return nil, nil
282+
283+
body, err := f.sys.cachedGetBody(ctx, cached, hash, header.Number.Uint64())
284+
if err != nil {
285+
return nil, err
286+
}
287+
for i, log := range logs {
288+
// Copy log not to modify cache elements
289+
logcopy := *log
290+
logcopy.TxHash = body.Transactions[logcopy.TxIndex].Hash()
291+
logs[i] = &logcopy
292+
}
293+
return logs, nil
294294
}
295295

296296
// pendingLogs returns the logs matching the filter criteria within the pending block.
@@ -380,11 +380,3 @@ func bloomFilter(bloom types.Bloom, addresses []common.Address, topics [][]commo
380380
}
381381
return true
382382
}
383-
384-
func flatten(list [][]*types.Log) []*types.Log {
385-
var flat []*types.Log
386-
for _, logs := range list {
387-
flat = append(flat, logs...)
388-
}
389-
return flat
390-
}

eth/filters/filter_system.go

+77-40
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"context"
2323
"fmt"
2424
"sync"
25+
"sync/atomic"
2526
"time"
2627

2728
"github.com/ethereum/go-ethereum"
@@ -58,6 +59,7 @@ type Backend interface {
5859
ChainDb() ethdb.Database
5960
HeaderByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Header, error)
6061
HeaderByHash(ctx context.Context, blockHash common.Hash) (*types.Header, error)
62+
GetBody(ctx context.Context, hash common.Hash, number rpc.BlockNumber) (*types.Body, error)
6163
GetReceipts(ctx context.Context, blockHash common.Hash) (types.Receipts, error)
6264
GetLogs(ctx context.Context, blockHash common.Hash, number uint64) ([][]*types.Log, error)
6365
PendingBlockAndReceipts() (*types.Block, types.Receipts)
@@ -77,7 +79,7 @@ type Backend interface {
7779
// FilterSystem holds resources shared by all filters.
7880
type FilterSystem struct {
7981
backend Backend
80-
logsCache *lru.Cache[common.Hash, [][]*types.Log]
82+
logsCache *lru.Cache[common.Hash, *logCacheElem]
8183
cfg *Config
8284
}
8385

@@ -86,13 +88,18 @@ func NewFilterSystem(backend Backend, config Config) *FilterSystem {
8688
config = config.withDefaults()
8789
return &FilterSystem{
8890
backend: backend,
89-
logsCache: lru.NewCache[common.Hash, [][]*types.Log](config.LogCacheSize),
91+
logsCache: lru.NewCache[common.Hash, *logCacheElem](config.LogCacheSize),
9092
cfg: &config,
9193
}
9294
}
9395

94-
// cachedGetLogs loads block logs from the backend and caches the result.
95-
func (sys *FilterSystem) cachedGetLogs(ctx context.Context, blockHash common.Hash, number uint64) ([][]*types.Log, error) {
96+
type logCacheElem struct {
97+
logs []*types.Log
98+
body atomic.Pointer[types.Body]
99+
}
100+
101+
// cachedLogElem loads block logs from the backend and caches the result.
102+
func (sys *FilterSystem) cachedLogElem(ctx context.Context, blockHash common.Hash, number uint64) (*logCacheElem, error) {
96103
cached, ok := sys.logsCache.Get(blockHash)
97104
if ok {
98105
return cached, nil
@@ -105,8 +112,35 @@ func (sys *FilterSystem) cachedGetLogs(ctx context.Context, blockHash common.Has
105112
if logs == nil {
106113
return nil, fmt.Errorf("failed to get logs for block #%d (0x%s)", number, blockHash.TerminalString())
107114
}
108-
sys.logsCache.Add(blockHash, logs)
109-
return logs, nil
115+
// Database logs are un-derived.
116+
// Fill in whatever we can (txHash is inaccessible at this point).
117+
flattened := make([]*types.Log, 0)
118+
var logIdx uint
119+
for i, txLogs := range logs {
120+
for _, log := range txLogs {
121+
log.BlockHash = blockHash
122+
log.BlockNumber = number
123+
log.TxIndex = uint(i)
124+
log.Index = logIdx
125+
logIdx++
126+
flattened = append(flattened, log)
127+
}
128+
}
129+
elem := &logCacheElem{logs: flattened}
130+
sys.logsCache.Add(blockHash, elem)
131+
return elem, nil
132+
}
133+
134+
func (sys *FilterSystem) cachedGetBody(ctx context.Context, elem *logCacheElem, hash common.Hash, number uint64) (*types.Body, error) {
135+
if body := elem.body.Load(); body != nil {
136+
return body, nil
137+
}
138+
body, err := sys.backend.GetBody(ctx, hash, rpc.BlockNumber(number))
139+
if err != nil {
140+
return nil, err
141+
}
142+
elem.body.Store(body)
143+
return body, nil
110144
}
111145

112146
// Type determines the kind of filter and is used to put the filter in to
@@ -431,6 +465,12 @@ func (es *EventSystem) handleChainEvent(filters filterIndex, ev core.ChainEvent)
431465
if es.lightMode && len(filters[LogsSubscription]) > 0 {
432466
es.lightFilterNewHead(ev.Block.Header(), func(header *types.Header, remove bool) {
433467
for _, f := range filters[LogsSubscription] {
468+
if f.logsCrit.FromBlock != nil && header.Number.Cmp(f.logsCrit.FromBlock) < 0 {
469+
continue
470+
}
471+
if f.logsCrit.ToBlock != nil && header.Number.Cmp(f.logsCrit.ToBlock) > 0 {
472+
continue
473+
}
434474
if matchedLogs := es.lightFilterLogs(header, f.logsCrit.Addresses, f.logsCrit.Topics, remove); len(matchedLogs) > 0 {
435475
f.logs <- matchedLogs
436476
}
@@ -474,42 +514,39 @@ func (es *EventSystem) lightFilterNewHead(newHeader *types.Header, callBack func
474514

475515
// filter logs of a single header in light client mode
476516
func (es *EventSystem) lightFilterLogs(header *types.Header, addresses []common.Address, topics [][]common.Hash, remove bool) []*types.Log {
477-
if bloomFilter(header.Bloom, addresses, topics) {
478-
// Get the logs of the block
479-
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
480-
defer cancel()
481-
logsList, err := es.sys.cachedGetLogs(ctx, header.Hash(), header.Number.Uint64())
482-
if err != nil {
483-
return nil
484-
}
485-
var unfiltered []*types.Log
486-
for _, logs := range logsList {
487-
for _, log := range logs {
488-
logcopy := *log
489-
logcopy.Removed = remove
490-
unfiltered = append(unfiltered, &logcopy)
491-
}
492-
}
493-
logs := filterLogs(unfiltered, nil, nil, addresses, topics)
494-
if len(logs) > 0 && logs[0].TxHash == (common.Hash{}) {
495-
// We have matching but non-derived logs
496-
receipts, err := es.backend.GetReceipts(ctx, header.Hash())
497-
if err != nil {
498-
return nil
499-
}
500-
unfiltered = unfiltered[:0]
501-
for _, receipt := range receipts {
502-
for _, log := range receipt.Logs {
503-
logcopy := *log
504-
logcopy.Removed = remove
505-
unfiltered = append(unfiltered, &logcopy)
506-
}
507-
}
508-
logs = filterLogs(unfiltered, nil, nil, addresses, topics)
509-
}
517+
if !bloomFilter(header.Bloom, addresses, topics) {
518+
return nil
519+
}
520+
// Get the logs of the block
521+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
522+
defer cancel()
523+
cached, err := es.sys.cachedLogElem(ctx, header.Hash(), header.Number.Uint64())
524+
if err != nil {
525+
return nil
526+
}
527+
unfiltered := append([]*types.Log{}, cached.logs...)
528+
for i, log := range unfiltered {
529+
// Don't modify in-cache elements
530+
logcopy := *log
531+
logcopy.Removed = remove
532+
// Swap copy in-place
533+
unfiltered[i] = &logcopy
534+
}
535+
logs := filterLogs(unfiltered, nil, nil, addresses, topics)
536+
// Txhash is already resolved
537+
if len(logs) > 0 && logs[0].TxHash != (common.Hash{}) {
510538
return logs
511539
}
512-
return nil
540+
// Resolve txhash
541+
body, err := es.sys.cachedGetBody(ctx, cached, header.Hash(), header.Number.Uint64())
542+
if err != nil {
543+
return nil
544+
}
545+
for _, log := range logs {
546+
// logs are already copied, safe to modify
547+
log.TxHash = body.Transactions[log.TxIndex].Hash()
548+
}
549+
return logs
513550
}
514551

515552
// eventLoop (un)installs filters and processes mux events.

0 commit comments

Comments
 (0)