303 lines
8.4 KiB
Go
303 lines
8.4 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/cosmos/ethermint/ethereum/rpc/types"
|
|
|
|
"github.com/pkg/errors"
|
|
log "github.com/xlab/suplog"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core/bloombits"
|
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/eth/filters"
|
|
)
|
|
|
|
// Filter can be used to retrieve and filter logs.
|
|
type Filter struct {
|
|
backend FiltersBackend
|
|
criteria filters.FilterCriteria
|
|
matcher *bloombits.Matcher
|
|
}
|
|
|
|
// NewBlockFilter creates a new filter which directly inspects the contents of
|
|
// a block to figure out whether it is interesting or not.
|
|
func NewBlockFilter(backend FiltersBackend, criteria filters.FilterCriteria) *Filter {
|
|
// Create a generic filter and convert it into a block filter
|
|
return newFilter(backend, criteria, nil)
|
|
}
|
|
|
|
// NewRangeFilter creates a new filter which uses a bloom filter on blocks to
|
|
// figure out whether a particular block is interesting or not.
|
|
func NewRangeFilter(backend FiltersBackend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter {
|
|
// Flatten the address and topic filter clauses into a single bloombits filter
|
|
// system. Since the bloombits are not positional, nil topics are permitted,
|
|
// which get flattened into a nil byte slice.
|
|
var filtersBz [][][]byte // nolint: prealloc
|
|
if len(addresses) > 0 {
|
|
filter := make([][]byte, len(addresses))
|
|
for i, address := range addresses {
|
|
filter[i] = address.Bytes()
|
|
}
|
|
filtersBz = append(filtersBz, filter)
|
|
}
|
|
|
|
for _, topicList := range topics {
|
|
filter := make([][]byte, len(topicList))
|
|
for i, topic := range topicList {
|
|
filter[i] = topic.Bytes()
|
|
}
|
|
filtersBz = append(filtersBz, filter)
|
|
}
|
|
|
|
size, _ := backend.BloomStatus()
|
|
|
|
// Create a generic filter and convert it into a range filter
|
|
criteria := filters.FilterCriteria{
|
|
FromBlock: big.NewInt(begin),
|
|
ToBlock: big.NewInt(end),
|
|
Addresses: addresses,
|
|
Topics: topics,
|
|
}
|
|
|
|
return newFilter(backend, criteria, bloombits.NewMatcher(size, filtersBz))
|
|
}
|
|
|
|
// newFilter returns a new Filter
|
|
func newFilter(backend FiltersBackend, criteria filters.FilterCriteria, matcher *bloombits.Matcher) *Filter {
|
|
return &Filter{
|
|
backend: backend,
|
|
criteria: criteria,
|
|
matcher: matcher,
|
|
}
|
|
}
|
|
|
|
const (
|
|
maxFilterBlocks = 100000
|
|
maxToOverhang = 600
|
|
)
|
|
|
|
// Logs searches the blockchain for matching log entries, returning all from the
|
|
// first block that contains matches, updating the start of the filter accordingly.
|
|
func (f *Filter) Logs(_ context.Context) ([]*ethtypes.Log, error) {
|
|
logs := []*ethtypes.Log{}
|
|
var err error
|
|
|
|
// If we're doing singleton block filtering, execute and return
|
|
if f.criteria.BlockHash != nil && *f.criteria.BlockHash != (common.Hash{}) {
|
|
header, err := f.backend.HeaderByHash(*f.criteria.BlockHash)
|
|
if err != nil {
|
|
err = errors.Wrap(err, "failed to fetch header by hash")
|
|
return nil, err
|
|
}
|
|
|
|
if header == nil {
|
|
err := errors.Errorf("unknown block header %s", f.criteria.BlockHash.String())
|
|
return nil, err
|
|
}
|
|
|
|
return f.blockLogs(header)
|
|
}
|
|
|
|
// Figure out the limits of the filter range
|
|
header, err := f.backend.HeaderByNumber(types.EthLatestBlockNumber)
|
|
if err != nil {
|
|
err = errors.Wrap(err, "failed to fetch header by number (latest)")
|
|
return nil, err
|
|
}
|
|
|
|
if header == nil || header.Number == nil {
|
|
log.Warningln("header not found or has no number")
|
|
return nil, nil
|
|
}
|
|
|
|
head := header.Number.Int64()
|
|
if f.criteria.FromBlock.Int64() == -1 {
|
|
f.criteria.FromBlock = big.NewInt(head)
|
|
}
|
|
if f.criteria.ToBlock.Int64() == -1 {
|
|
f.criteria.ToBlock = big.NewInt(head)
|
|
}
|
|
|
|
if f.criteria.ToBlock.Int64()-f.criteria.FromBlock.Int64() > maxFilterBlocks {
|
|
err := errors.Errorf("maximum [from, to] blocks distance: %d", maxFilterBlocks)
|
|
return nil, err
|
|
}
|
|
|
|
// check bounds
|
|
if f.criteria.FromBlock.Int64() > head {
|
|
return []*ethtypes.Log{}, nil
|
|
} else if f.criteria.ToBlock.Int64() > head+maxToOverhang {
|
|
f.criteria.ToBlock = big.NewInt(head + maxToOverhang)
|
|
}
|
|
|
|
for i := f.criteria.FromBlock.Int64(); i <= f.criteria.ToBlock.Int64(); i++ {
|
|
block, err := f.backend.GetBlockByNumber(types.BlockNumber(i), false)
|
|
if err != nil {
|
|
err = errors.Wrapf(err, "failed to fetch block by number %d", i)
|
|
return logs, err
|
|
} else if block["transactions"] == nil {
|
|
continue
|
|
}
|
|
|
|
var txHashes []common.Hash
|
|
|
|
txs, ok := block["transactions"].([]interface{})
|
|
if !ok {
|
|
txHashes, ok = block["transactions"].([]common.Hash)
|
|
if !ok {
|
|
log.WithField(
|
|
"transactions",
|
|
fmt.Sprintf("%T", block["transactions"]),
|
|
).Errorln("reading transactions from block data: bad field type")
|
|
continue
|
|
}
|
|
} else if len(txs) == 0 && len(txHashes) == 0 {
|
|
continue
|
|
}
|
|
|
|
for _, tx := range txs {
|
|
txHash, ok := tx.(common.Hash)
|
|
if !ok {
|
|
log.WithField(
|
|
"tx",
|
|
fmt.Sprintf("%T", tx),
|
|
).Errorln("transactions list contains non-hash element")
|
|
} else {
|
|
txHashes = append(txHashes, txHash)
|
|
}
|
|
}
|
|
|
|
logsMatched := f.checkMatches(txHashes)
|
|
logs = append(logs, logsMatched...)
|
|
}
|
|
|
|
return logs, nil
|
|
}
|
|
|
|
// blockLogs returns the logs matching the filter criteria within a single block.
|
|
func (f *Filter) blockLogs(header *ethtypes.Header) ([]*ethtypes.Log, error) {
|
|
if !bloomFilter(header.Bloom, f.criteria.Addresses, f.criteria.Topics) {
|
|
return []*ethtypes.Log{}, nil
|
|
}
|
|
|
|
// DANGER: do not call GetLogs(header.Hash())
|
|
// eth header's hash doesn't match tm block hash
|
|
logsList, err := f.backend.GetLogsByNumber(types.BlockNumber(header.Number.Int64()))
|
|
if err != nil {
|
|
err = errors.Wrapf(err, "failed to fetch logs block number %d", header.Number.Int64())
|
|
|
|
return []*ethtypes.Log{}, err
|
|
}
|
|
|
|
var unfiltered []*ethtypes.Log // nolint: prealloc
|
|
for _, logs := range logsList {
|
|
unfiltered = append(unfiltered, logs...)
|
|
}
|
|
|
|
logs := filterLogs(unfiltered, nil, nil, f.criteria.Addresses, f.criteria.Topics)
|
|
if len(logs) == 0 {
|
|
return []*ethtypes.Log{}, nil
|
|
}
|
|
|
|
return logs, nil
|
|
}
|
|
|
|
// checkMatches checks if the logs from the a list of transactions transaction
|
|
// contain any log events that match the filter criteria. This function is
|
|
// called when the bloom filter signals a potential match.
|
|
func (f *Filter) checkMatches(transactions []common.Hash) []*ethtypes.Log {
|
|
unfiltered := []*ethtypes.Log{}
|
|
for _, tx := range transactions {
|
|
logs, err := f.backend.GetTransactionLogs(tx)
|
|
if err != nil {
|
|
// ignore error if transaction didn't set any logs (eg: when tx type is not
|
|
// MsgEthereumTx or MsgEthermint)
|
|
continue
|
|
}
|
|
|
|
unfiltered = append(unfiltered, logs...)
|
|
}
|
|
|
|
return filterLogs(unfiltered, f.criteria.FromBlock, f.criteria.ToBlock, f.criteria.Addresses, f.criteria.Topics)
|
|
}
|
|
|
|
// filterLogs creates a slice of logs matching the given criteria.
|
|
// [] -> anything
|
|
// [A] -> A in first position of log topics, anything after
|
|
// [null, B] -> anything in first position, B in second position
|
|
// [A, B] -> A in first position and B in second position
|
|
// [[A, B], [A, B]] -> A or B in first position, A or B in second position
|
|
func filterLogs(logs []*ethtypes.Log, fromBlock, toBlock *big.Int, addresses []common.Address, topics [][]common.Hash) []*ethtypes.Log {
|
|
var ret []*ethtypes.Log
|
|
Logs:
|
|
for _, log := range logs {
|
|
if fromBlock != nil && fromBlock.Int64() >= 0 && fromBlock.Uint64() > log.BlockNumber {
|
|
continue
|
|
}
|
|
if toBlock != nil && toBlock.Int64() >= 0 && toBlock.Uint64() < log.BlockNumber {
|
|
continue
|
|
}
|
|
if len(addresses) > 0 && !includes(addresses, log.Address) {
|
|
continue
|
|
}
|
|
// If the to filtered topics is greater than the amount of topics in logs, skip.
|
|
if len(topics) > len(log.Topics) {
|
|
continue
|
|
}
|
|
for i, sub := range topics {
|
|
match := len(sub) == 0 // empty rule set == wildcard
|
|
for _, topic := range sub {
|
|
if log.Topics[i] == topic {
|
|
match = true
|
|
break
|
|
}
|
|
}
|
|
if !match {
|
|
continue Logs
|
|
}
|
|
}
|
|
ret = append(ret, log)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func includes(addresses []common.Address, a common.Address) bool {
|
|
for _, addr := range addresses {
|
|
if addr == a {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func bloomFilter(bloom ethtypes.Bloom, addresses []common.Address, topics [][]common.Hash) bool {
|
|
var included bool
|
|
if len(addresses) > 0 {
|
|
for _, addr := range addresses {
|
|
if ethtypes.BloomLookup(bloom, addr) {
|
|
included = true
|
|
break
|
|
}
|
|
}
|
|
if !included {
|
|
return false
|
|
}
|
|
}
|
|
|
|
for _, sub := range topics {
|
|
included = len(sub) == 0 // empty rule set == wildcard
|
|
for _, topic := range sub {
|
|
if ethtypes.BloomLookup(bloom, topic) {
|
|
included = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return included
|
|
}
|