Fix poor eth_getLogs performance (fixes #3033) (#3040)

* Fix poor eth_getLogs performance (fixes #3033)

* don't recompute txhash in inner log loop (!)
* filter logs before computing hashes

* copyright
This commit is contained in:
Jacek Sieka 2025-01-30 20:38:24 +01:00 committed by GitHub
parent e03a9c3172
commit 8690a03af7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 88 additions and 88 deletions

View File

@ -20,7 +20,7 @@ import
../version ../version
from ../../nimbus/errors import ValidationError from ../../nimbus/errors import ValidationError
from ../../nimbus/rpc/filters import headerBloomFilter, deriveLogs, filterLogs from ../../nimbus/rpc/filters import headerBloomFilter, deriveLogs
from eth/common/eth_types_rlp import rlpHash from eth/common/eth_types_rlp import rlpHash
@ -269,10 +269,7 @@ proc installEthApiHandlers*(
receipts = (await hn.getReceipts(hash, header)).valueOr: receipts = (await hn.getReceipts(hash, header)).valueOr:
raise newException(ValueError, "Could not find receipts for requested hash") raise newException(ValueError, "Could not find receipts for requested hash")
logs = deriveLogs(header, body.transactions, receipts) return deriveLogs(header, body.transactions, receipts, filterOptions)
filteredLogs = filterLogs(logs, filterOptions.address, filterOptions.topics)
return filteredLogs
else: else:
# bloomfilter returned false, there are no logs matching the criteria # bloomfilter returned false, there are no logs matching the criteria
return @[] return @[]

View File

@ -1,12 +1,12 @@
# Nimbus # Nimbus
# Copyright (c) 2022-2024 Status Research & Development GmbH # Copyright (c) 2022-2025 Status Research & Development GmbH
# Licensed and distributed under either of # Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms. # at your option. This file may not be copied, modified, or distributed except according to those terms.
import import
std/options, std/sequtils,
eth/common/eth_types_rlp, eth/common/eth_types_rlp,
web3/eth_api_types, web3/eth_api_types,
eth/bloom as bFilter, eth/bloom as bFilter,
@ -17,7 +17,56 @@ export rpc_types
{.push raises: [].} {.push raises: [].}
proc deriveLogs*(header: Header, transactions: seq[Transaction], receipts: seq[Receipt]): seq[FilterLog] = proc matchTopics(
topics: openArray[receipts.Topic], filter: openArray[TopicOrList]
): bool =
for i, sub in filter:
if sub.kind == slkNull:
# null subtopic i.e it matches all possible move to nex
continue
var match = false
if sub.kind == slkSingle:
match = topics[i] == sub.single
else:
# treat empty as wildcard, although caller should rather use none kind of
# option to indicate that. If nim would have NonEmptySeq type that would be
# use case for it.
match = sub.list.len == 0
for topic in sub.list:
if topics[i] == topic:
match = true
break
if not match:
return false
return true
proc match*(
log: Log | FilterLog, addresses: AddressOrList, topics: openArray[TopicOrList]
): bool =
if addresses.kind == slkSingle and (addresses.single != log.address):
return false
if addresses.kind == slkList and addresses.list.len > 0 and
(not addresses.list.contains(log.address)):
return false
if len(topics) > len(log.topics):
return false
if not matchTopics(log.topics, topics):
return false
true
proc deriveLogs*(
header: Header,
transactions: openArray[Transaction],
receipts: openArray[Receipt],
filterOptions: FilterOptions,
): seq[FilterLog] =
## Derive log fields, does not deal with pending log, only the logs with ## Derive log fields, does not deal with pending log, only the logs with
## full data set ## full data set
doAssert(len(transactions) == len(receipts)) doAssert(len(transactions) == len(receipts))
@ -26,26 +75,30 @@ proc deriveLogs*(header: Header, transactions: seq[Transaction], receipts: seq[R
var logIndex = 0'u64 var logIndex = 0'u64
for i, receipt in receipts: for i, receipt in receipts:
for log in receipt.logs: let logs = receipt.logs.filterIt(it.match(filterOptions.address, filterOptions.topics))
let filterLog = FilterLog( if logs.len > 0:
# TODO investigate how to handle this field # TODO avoid recomputing entirely - we should have this cached somewhere
# - in nimbus info about log removel would need to be kept at synchronization let txHash = transactions[i].rlpHash
# level, to keep track about potential re-orgs for log in logs:
# - in fluffy there is no concept of re-org let filterLog = FilterLog(
removed: false, # TODO investigate how to handle this field
logIndex: Opt.some(Quantity(logIndex)), # - in nimbus info about log removel would need to be kept at synchronization
transactionIndex: Opt.some(Quantity(i)), # level, to keep track about potential re-orgs
transactionHash: Opt.some(transactions[i].rlpHash), # - in fluffy there is no concept of re-org
blockHash: Opt.some(header.blockHash), removed: false,
blockNumber: Opt.some(Quantity(header.number)), logIndex: Opt.some(Quantity(logIndex)),
address: log.address, transactionIndex: Opt.some(Quantity(i)),
data: log.data, transactionHash: Opt.some(txHash),
# TODO topics should probably be kept as Hash32 in receipts blockHash: Opt.some(header.blockHash),
topics: log.topics blockNumber: Opt.some(Quantity(header.number)),
) address: log.address,
data: log.data,
# TODO topics should probably be kept as Hash32 in receipts
topics: log.topics,
)
inc logIndex inc logIndex
resLogs.add(filterLog) resLogs.add(filterLog)
return resLogs return resLogs
@ -58,10 +111,8 @@ func participateInFilter(x: AddressOrList): bool =
true true
proc bloomFilter*( proc bloomFilter*(
bloom: Bloom, bloom: Bloom, addresses: AddressOrList, topics: seq[TopicOrList]
addresses: AddressOrList, ): bool =
topics: seq[TopicOrList]): bool =
let bloomFilter = bFilter.BloomFilter(value: bloom.to(StUint[2048])) let bloomFilter = bFilter.BloomFilter(value: bloom.to(StUint[2048]))
if addresses.participateInFilter(): if addresses.participateInFilter():
@ -101,58 +152,11 @@ proc bloomFilter*(
return true return true
proc headerBloomFilter*( proc headerBloomFilter*(
header: Header, header: Header, addresses: AddressOrList, topics: seq[TopicOrList]
addresses: AddressOrList, ): bool =
topics: seq[TopicOrList]): bool =
return bloomFilter(header.logsBloom, addresses, topics) return bloomFilter(header.logsBloom, addresses, topics)
proc matchTopics(log: FilterLog, topics: seq[TopicOrList]): bool =
for i, sub in topics:
if sub.kind == slkNull:
# null subtopic i.e it matches all possible move to nex
continue
var match = false
if sub.kind == slkSingle:
match = log.topics[i] == sub.single
else:
# treat empty as wildcard, although caller should rather use none kind of
# option to indicate that. If nim would have NonEmptySeq type that would be
# use case for it.
match = sub.list.len == 0
for topic in sub.list:
if log.topics[i] == topic:
match = true
break
if not match:
return false
return true
proc filterLogs*( proc filterLogs*(
logs: openArray[FilterLog], logs: openArray[FilterLog], addresses: AddressOrList, topics: seq[TopicOrList]
addresses: AddressOrList, ): seq[FilterLog] =
topics: seq[TopicOrList]): seq[FilterLog] = logs.filterIt(it.match(addresses, topics))
var filteredLogs: seq[FilterLog] = newSeq[FilterLog]()
for log in logs:
if addresses.kind == slkSingle and (addresses.single != log.address):
continue
if addresses.kind == slkList and
addresses.list.len > 0 and
(not addresses.list.contains(log.address)):
continue
if len(topics) > len(log.topics):
continue
if not matchTopics(log, topics):
continue
filteredLogs.add(log)
return filteredLogs

View File

@ -241,9 +241,8 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, ctx: EthContext) =
number = header.number, hash = header.blockHash.short, number = header.number, hash = header.blockHash.short,
txs = txs.len, receipts = receipts.len txs = txs.len, receipts = receipts.len
return Opt.none(seq[FilterLog]) return Opt.none(seq[FilterLog])
let logs = deriveLogs(header, txs, receipts) let logs = deriveLogs(header, txs, receipts, opts)
let filteredLogs = filterLogs(logs, opts.address, opts.topics) return Opt.some(logs)
return Opt.some(filteredLogs)
else: else:
return Opt.some(newSeq[FilterLog](0)) return Opt.some(newSeq[FilterLog](0))

View File

@ -1,4 +1,4 @@
# Copyright (c) 2022-2024 Status Research & Development GmbH # Copyright (c) 2022-2025 Status Research & Development GmbH
# Licensed under either of # Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT)) # * MIT license ([LICENSE-MIT](LICENSE-MIT))
@ -17,7 +17,7 @@ import
type Address = primitives.Address type Address = primitives.Address
let allLogs = deriveLogs(blockHeader4514995, blockBody4514995.transactions, receipts4514995) let allLogs = deriveLogs(blockHeader4514995, blockBody4514995.transactions, receipts4514995, FilterOptions())
proc filtersMain*() = proc filtersMain*() =
# All magic numbers and addresses in following tests are confirmed with geth eth_getLogs, # All magic numbers and addresses in following tests are confirmed with geth eth_getLogs,