Merge pull request #16734 from reductionista/eip234

eth/filters, interfaces: EIP-234 Add blockHash option to eth_getLogs
pull/17236/head
Péter Szilágyi 6 years ago committed by GitHub
commit 21c059b67e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      accounts/abi/bind/backends/simulated.go
  2. 4
      eth/api_backend.go
  3. 76
      eth/filters/api.go
  4. 4
      eth/filters/bench_test.go
  5. 77
      eth/filters/filter.go
  6. 35
      eth/filters/filter_system_test.go
  7. 16
      eth/filters/filter_test.go
  8. 1
      interfaces.go
  9. 5
      les/api_backend.go

@ -324,18 +324,24 @@ func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transa
// //
// TODO(karalabe): Deprecate when the subscription one can return past data too. // TODO(karalabe): Deprecate when the subscription one can return past data too.
func (b *SimulatedBackend) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) { func (b *SimulatedBackend) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) {
// Initialize unset filter boundaried to run from genesis to chain head var filter *filters.Filter
from := int64(0) if query.BlockHash != nil {
if query.FromBlock != nil { // Block filter requested, construct a single-shot filter
from = query.FromBlock.Int64() filter = filters.NewBlockFilter(&filterBackend{b.database, b.blockchain}, *query.BlockHash, query.Addresses, query.Topics)
} } else {
to := int64(-1) // Initialize unset filter boundaried to run from genesis to chain head
if query.ToBlock != nil { from := int64(0)
to = query.ToBlock.Int64() if query.FromBlock != nil {
from = query.FromBlock.Int64()
}
to := int64(-1)
if query.ToBlock != nil {
to = query.ToBlock.Int64()
}
// Construct the range filter
filter = filters.NewRangeFilter(&filterBackend{b.database, b.blockchain}, from, to, query.Addresses, query.Topics)
} }
// Construct and execute the filter // Run the filter and return all the logs
filter := filters.New(&filterBackend{b.database, b.blockchain}, from, to, query.Addresses, query.Topics)
logs, err := filter.Logs(ctx) logs, err := filter.Logs(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -430,6 +436,10 @@ func (fb *filterBackend) HeaderByNumber(ctx context.Context, block rpc.BlockNumb
return fb.bc.GetHeaderByNumber(uint64(block.Int64())), nil return fb.bc.GetHeaderByNumber(uint64(block.Int64())), nil
} }
func (fb *filterBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) {
return fb.bc.GetHeaderByHash(hash), nil
}
func (fb *filterBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { func (fb *filterBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) {
number := rawdb.ReadHeaderNumber(fb.db, hash) number := rawdb.ReadHeaderNumber(fb.db, hash)
if number == nil { if number == nil {

@ -70,6 +70,10 @@ func (b *EthAPIBackend) HeaderByNumber(ctx context.Context, blockNr rpc.BlockNum
return b.eth.blockchain.GetHeaderByNumber(uint64(blockNr)), nil return b.eth.blockchain.GetHeaderByNumber(uint64(blockNr)), nil
} }
func (b *EthAPIBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) {
return b.eth.blockchain.GetHeaderByHash(hash), nil
}
func (b *EthAPIBackend) BlockByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Block, error) { func (b *EthAPIBackend) BlockByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Block, error) {
// Pending block is only known by the miner // Pending block is only known by the miner
if blockNr == rpc.PendingBlockNumber { if blockNr == rpc.PendingBlockNumber {

@ -324,16 +324,26 @@ func (api *PublicFilterAPI) NewFilter(crit FilterCriteria) (rpc.ID, error) {
// //
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs
func (api *PublicFilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*types.Log, error) { func (api *PublicFilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*types.Log, error) {
// Convert the RPC block numbers into internal representations var filter *Filter
if crit.FromBlock == nil { if crit.BlockHash != nil {
crit.FromBlock = big.NewInt(rpc.LatestBlockNumber.Int64()) // Block filter requested, construct a single-shot filter
} filter = NewBlockFilter(api.backend, *crit.BlockHash, crit.Addresses, crit.Topics)
if crit.ToBlock == nil { } else {
crit.ToBlock = big.NewInt(rpc.LatestBlockNumber.Int64()) // Convert the RPC block numbers into internal representations
var (
begin int64
end int64
)
if crit.FromBlock == nil {
begin = int64(rpc.LatestBlockNumber)
}
if crit.ToBlock == nil {
end = int64(rpc.LatestBlockNumber)
}
// Construct the range filter
filter = NewRangeFilter(api.backend, begin, end, crit.Addresses, crit.Topics)
} }
// Create and run the filter to get all the logs // Run the filter and return all the logs
filter := New(api.backend, crit.FromBlock.Int64(), crit.ToBlock.Int64(), crit.Addresses, crit.Topics)
logs, err := filter.Logs(ctx) logs, err := filter.Logs(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -371,17 +381,24 @@ func (api *PublicFilterAPI) GetFilterLogs(ctx context.Context, id rpc.ID) ([]*ty
return nil, fmt.Errorf("filter not found") return nil, fmt.Errorf("filter not found")
} }
begin := rpc.LatestBlockNumber.Int64() var filter *Filter
if f.crit.FromBlock != nil { if f.crit.BlockHash != nil {
begin = f.crit.FromBlock.Int64() // Block filter requested, construct a single-shot filter
} filter = NewBlockFilter(api.backend, *f.crit.BlockHash, f.crit.Addresses, f.crit.Topics)
end := rpc.LatestBlockNumber.Int64() } else {
if f.crit.ToBlock != nil { // Convert the RPC block numbers into internal representations
end = f.crit.ToBlock.Int64() begin := rpc.LatestBlockNumber.Int64()
if f.crit.FromBlock != nil {
begin = f.crit.FromBlock.Int64()
}
end := rpc.LatestBlockNumber.Int64()
if f.crit.ToBlock != nil {
end = f.crit.ToBlock.Int64()
}
// Construct the range filter
filter = NewRangeFilter(api.backend, begin, end, f.crit.Addresses, f.crit.Topics)
} }
// Create and run the filter to get all the logs // Run the filter and return all the logs
filter := New(api.backend, begin, end, f.crit.Addresses, f.crit.Topics)
logs, err := filter.Logs(ctx) logs, err := filter.Logs(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -444,7 +461,8 @@ func returnLogs(logs []*types.Log) []*types.Log {
// UnmarshalJSON sets *args fields with given data. // UnmarshalJSON sets *args fields with given data.
func (args *FilterCriteria) UnmarshalJSON(data []byte) error { func (args *FilterCriteria) UnmarshalJSON(data []byte) error {
type input struct { type input struct {
From *rpc.BlockNumber `json:"fromBlock"` BlockHash *common.Hash `json:"blockHash"`
FromBlock *rpc.BlockNumber `json:"fromBlock"`
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"`
@ -455,12 +473,20 @@ func (args *FilterCriteria) UnmarshalJSON(data []byte) error {
return err return err
} }
if raw.From != nil { if raw.BlockHash != nil {
args.FromBlock = big.NewInt(raw.From.Int64()) if raw.FromBlock != nil || raw.ToBlock != nil {
} // BlockHash is mutually exclusive with FromBlock/ToBlock criteria
return fmt.Errorf("cannot specify both BlockHash and FromBlock/ToBlock, choose one or the other")
}
args.BlockHash = raw.BlockHash
} else {
if raw.FromBlock != nil {
args.FromBlock = big.NewInt(raw.FromBlock.Int64())
}
if raw.ToBlock != nil { if raw.ToBlock != nil {
args.ToBlock = big.NewInt(raw.ToBlock.Int64()) args.ToBlock = big.NewInt(raw.ToBlock.Int64())
}
} }
args.Addresses = []common.Address{} args.Addresses = []common.Address{}

@ -135,7 +135,7 @@ func benchmarkBloomBits(b *testing.B, sectionSize uint64) {
var addr common.Address var addr common.Address
addr[0] = byte(i) addr[0] = byte(i)
addr[1] = byte(i / 256) addr[1] = byte(i / 256)
filter := New(backend, 0, int64(cnt*sectionSize-1), []common.Address{addr}, nil) filter := NewRangeFilter(backend, 0, int64(cnt*sectionSize-1), []common.Address{addr}, nil)
if _, err := filter.Logs(context.Background()); err != nil { if _, err := filter.Logs(context.Background()); err != nil {
b.Error("filter.Find error:", err) b.Error("filter.Find error:", err)
} }
@ -192,7 +192,7 @@ func BenchmarkNoBloomBits(b *testing.B) {
start := time.Now() start := time.Now()
mux := new(event.TypeMux) mux := new(event.TypeMux)
backend := &testBackend{mux, db, 0, new(event.Feed), new(event.Feed), new(event.Feed), new(event.Feed)} backend := &testBackend{mux, db, 0, new(event.Feed), new(event.Feed), new(event.Feed), new(event.Feed)}
filter := New(backend, 0, int64(*headNum), []common.Address{{}}, nil) filter := NewRangeFilter(backend, 0, int64(*headNum), []common.Address{{}}, nil)
filter.Logs(context.Background()) filter.Logs(context.Background())
d := time.Since(start) d := time.Since(start)
fmt.Println("Finished running filter benchmarks") fmt.Println("Finished running filter benchmarks")

@ -18,6 +18,7 @@ package filters
import ( import (
"context" "context"
"errors"
"math/big" "math/big"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -33,6 +34,7 @@ type Backend interface {
ChainDb() ethdb.Database ChainDb() ethdb.Database
EventMux() *event.TypeMux EventMux() *event.TypeMux
HeaderByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Header, error) HeaderByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Header, error)
HeaderByHash(ctx context.Context, blockHash common.Hash) (*types.Header, error)
GetReceipts(ctx context.Context, blockHash common.Hash) (types.Receipts, error) GetReceipts(ctx context.Context, blockHash common.Hash) (types.Receipts, error)
GetLogs(ctx context.Context, blockHash common.Hash) ([][]*types.Log, error) GetLogs(ctx context.Context, blockHash common.Hash) ([][]*types.Log, error)
@ -49,17 +51,19 @@ type Backend interface {
type Filter struct { type Filter struct {
backend Backend backend Backend
db ethdb.Database db ethdb.Database
begin, end int64 addresses []common.Address
addresses []common.Address topics [][]common.Hash
topics [][]common.Hash
block common.Hash // Block hash if filtering a single block
begin, end int64 // Range interval if filtering multiple blocks
matcher *bloombits.Matcher matcher *bloombits.Matcher
} }
// New creates a new filter which uses a bloom filter on blocks to figure out whether // NewRangeFilter creates a new filter which uses a bloom filter on blocks to
// a particular block is interesting or not. // figure out whether a particular block is interesting or not.
func New(backend Backend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter { func NewRangeFilter(backend Backend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter {
// Flatten the address and topic filter clauses into a single bloombits filter // Flatten the address and topic filter clauses into a single bloombits filter
// system. Since the bloombits are not positional, nil topics are permitted, // system. Since the bloombits are not positional, nil topics are permitted,
// which get flattened into a nil byte slice. // which get flattened into a nil byte slice.
@ -78,23 +82,52 @@ func New(backend Backend, begin, end int64, addresses []common.Address, topics [
} }
filters = append(filters, filter) filters = append(filters, filter)
} }
// Assemble and return the filter
size, _ := backend.BloomStatus() size, _ := backend.BloomStatus()
// Create a generic filter and convert it into a range filter
filter := newFilter(backend, addresses, topics)
filter.matcher = bloombits.NewMatcher(size, filters)
filter.begin = begin
filter.end = end
return filter
}
// 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 Backend, block common.Hash, addresses []common.Address, topics [][]common.Hash) *Filter {
// Create a generic filter and convert it into a block filter
filter := newFilter(backend, addresses, topics)
filter.block = block
return filter
}
// newFilter creates a generic filter that can either filter based on a block hash,
// or based on range queries. The search criteria needs to be explicitly set.
func newFilter(backend Backend, addresses []common.Address, topics [][]common.Hash) *Filter {
return &Filter{ return &Filter{
backend: backend, backend: backend,
begin: begin,
end: end,
addresses: addresses, addresses: addresses,
topics: topics, topics: topics,
db: backend.ChainDb(), db: backend.ChainDb(),
matcher: bloombits.NewMatcher(size, filters),
} }
} }
// Logs searches the blockchain for matching log entries, returning all from the // Logs searches the blockchain for matching log entries, returning all from the
// first block that contains matches, updating the start of the filter accordingly. // first block that contains matches, updating the start of the filter accordingly.
func (f *Filter) Logs(ctx context.Context) ([]*types.Log, error) { func (f *Filter) Logs(ctx context.Context) ([]*types.Log, error) {
// If we're doing singleton block filtering, execute and return
if f.block != (common.Hash{}) {
header, err := f.backend.HeaderByHash(ctx, f.block)
if err != nil {
return nil, err
}
if header == nil {
return nil, errors.New("unknown block")
}
return f.blockLogs(ctx, header)
}
// Figure out the limits of the filter range // Figure out the limits of the filter range
header, _ := f.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber) header, _ := f.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
if header == nil { if header == nil {
@ -187,13 +220,23 @@ func (f *Filter) unindexedLogs(ctx context.Context, end uint64) ([]*types.Log, e
if header == nil || err != nil { if header == nil || err != nil {
return logs, err return logs, err
} }
if bloomFilter(header.Bloom, f.addresses, f.topics) { found, err := f.blockLogs(ctx, header)
found, err := f.checkMatches(ctx, header) if err != nil {
if err != nil { return logs, err
return logs, err }
} logs = append(logs, found...)
logs = append(logs, found...) }
return logs, nil
}
// blockLogs returns the logs matching the filter criteria within a single block.
func (f *Filter) blockLogs(ctx context.Context, header *types.Header) (logs []*types.Log, err error) {
if bloomFilter(header.Bloom, f.addresses, f.topics) {
found, err := f.checkMatches(ctx, header)
if err != nil {
return logs, err
} }
logs = append(logs, found...)
} }
return logs, nil return logs, nil
} }

@ -75,6 +75,14 @@ func (b *testBackend) HeaderByNumber(ctx context.Context, blockNr rpc.BlockNumbe
return rawdb.ReadHeader(b.db, hash, num), nil return rawdb.ReadHeader(b.db, hash, num), nil
} }
func (b *testBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) {
number := rawdb.ReadHeaderNumber(b.db, hash)
if number == nil {
return nil, nil
}
return rawdb.ReadHeader(b.db, hash, *number), nil
}
func (b *testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { func (b *testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) {
if number := rawdb.ReadHeaderNumber(b.db, hash); number != nil { if number := rawdb.ReadHeaderNumber(b.db, hash); number != nil {
return rawdb.ReadReceipts(b.db, hash, *number), nil return rawdb.ReadReceipts(b.db, hash, *number), nil
@ -343,6 +351,33 @@ func TestInvalidLogFilterCreation(t *testing.T) {
} }
} }
func TestInvalidGetLogsRequest(t *testing.T) {
var (
mux = new(event.TypeMux)
db = ethdb.NewMemDatabase()
txFeed = new(event.Feed)
rmLogsFeed = new(event.Feed)
logsFeed = new(event.Feed)
chainFeed = new(event.Feed)
backend = &testBackend{mux, db, 0, txFeed, rmLogsFeed, logsFeed, chainFeed}
api = NewPublicFilterAPI(backend, false)
blockHash = common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111")
)
// Reason: Cannot specify both BlockHash and FromBlock/ToBlock)
testCases := []FilterCriteria{
0: {BlockHash: &blockHash, FromBlock: big.NewInt(100)},
1: {BlockHash: &blockHash, ToBlock: big.NewInt(500)},
2: {BlockHash: &blockHash, FromBlock: big.NewInt(rpc.LatestBlockNumber.Int64())},
}
for i, test := range testCases {
if _, err := api.GetLogs(context.Background(), test); err == nil {
t.Errorf("Expected Logs for case #%d to fail", i)
}
}
}
// TestLogFilter tests whether log filters match the correct logs that are posted to the event feed. // TestLogFilter tests whether log filters match the correct logs that are posted to the event feed.
func TestLogFilter(t *testing.T) { func TestLogFilter(t *testing.T) {
t.Parallel() t.Parallel()

@ -92,7 +92,7 @@ func BenchmarkFilters(b *testing.B) {
} }
b.ResetTimer() b.ResetTimer()
filter := New(backend, 0, -1, []common.Address{addr1, addr2, addr3, addr4}, nil) filter := NewRangeFilter(backend, 0, -1, []common.Address{addr1, addr2, addr3, addr4}, nil)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
logs, _ := filter.Logs(context.Background()) logs, _ := filter.Logs(context.Background())
@ -175,14 +175,14 @@ func TestFilters(t *testing.T) {
rawdb.WriteReceipts(db, block.Hash(), block.NumberU64(), receipts[i]) rawdb.WriteReceipts(db, block.Hash(), block.NumberU64(), receipts[i])
} }
filter := New(backend, 0, -1, []common.Address{addr}, [][]common.Hash{{hash1, hash2, hash3, hash4}}) filter := NewRangeFilter(backend, 0, -1, []common.Address{addr}, [][]common.Hash{{hash1, hash2, hash3, hash4}})
logs, _ := filter.Logs(context.Background()) logs, _ := filter.Logs(context.Background())
if len(logs) != 4 { if len(logs) != 4 {
t.Error("expected 4 log, got", len(logs)) t.Error("expected 4 log, got", len(logs))
} }
filter = New(backend, 900, 999, []common.Address{addr}, [][]common.Hash{{hash3}}) filter = NewRangeFilter(backend, 900, 999, []common.Address{addr}, [][]common.Hash{{hash3}})
logs, _ = filter.Logs(context.Background()) logs, _ = filter.Logs(context.Background())
if len(logs) != 1 { if len(logs) != 1 {
t.Error("expected 1 log, got", len(logs)) t.Error("expected 1 log, got", len(logs))
@ -191,7 +191,7 @@ func TestFilters(t *testing.T) {
t.Errorf("expected log[0].Topics[0] to be %x, got %x", hash3, logs[0].Topics[0]) t.Errorf("expected log[0].Topics[0] to be %x, got %x", hash3, logs[0].Topics[0])
} }
filter = New(backend, 990, -1, []common.Address{addr}, [][]common.Hash{{hash3}}) filter = NewRangeFilter(backend, 990, -1, []common.Address{addr}, [][]common.Hash{{hash3}})
logs, _ = filter.Logs(context.Background()) logs, _ = filter.Logs(context.Background())
if len(logs) != 1 { if len(logs) != 1 {
t.Error("expected 1 log, got", len(logs)) t.Error("expected 1 log, got", len(logs))
@ -200,7 +200,7 @@ func TestFilters(t *testing.T) {
t.Errorf("expected log[0].Topics[0] to be %x, got %x", hash3, logs[0].Topics[0]) t.Errorf("expected log[0].Topics[0] to be %x, got %x", hash3, logs[0].Topics[0])
} }
filter = New(backend, 1, 10, nil, [][]common.Hash{{hash1, hash2}}) filter = NewRangeFilter(backend, 1, 10, nil, [][]common.Hash{{hash1, hash2}})
logs, _ = filter.Logs(context.Background()) logs, _ = filter.Logs(context.Background())
if len(logs) != 2 { if len(logs) != 2 {
@ -208,7 +208,7 @@ func TestFilters(t *testing.T) {
} }
failHash := common.BytesToHash([]byte("fail")) failHash := common.BytesToHash([]byte("fail"))
filter = New(backend, 0, -1, nil, [][]common.Hash{{failHash}}) filter = NewRangeFilter(backend, 0, -1, nil, [][]common.Hash{{failHash}})
logs, _ = filter.Logs(context.Background()) logs, _ = filter.Logs(context.Background())
if len(logs) != 0 { if len(logs) != 0 {
@ -216,14 +216,14 @@ func TestFilters(t *testing.T) {
} }
failAddr := common.BytesToAddress([]byte("failmenow")) failAddr := common.BytesToAddress([]byte("failmenow"))
filter = New(backend, 0, -1, []common.Address{failAddr}, nil) filter = NewRangeFilter(backend, 0, -1, []common.Address{failAddr}, nil)
logs, _ = filter.Logs(context.Background()) logs, _ = filter.Logs(context.Background())
if len(logs) != 0 { if len(logs) != 0 {
t.Error("expected 0 log, got", len(logs)) t.Error("expected 0 log, got", len(logs))
} }
filter = New(backend, 0, -1, nil, [][]common.Hash{{failHash}, {hash1}}) filter = NewRangeFilter(backend, 0, -1, nil, [][]common.Hash{{failHash}, {hash1}})
logs, _ = filter.Logs(context.Background()) logs, _ = filter.Logs(context.Background())
if len(logs) != 0 { if len(logs) != 0 {

@ -131,6 +131,7 @@ type ContractCaller interface {
// FilterQuery contains options for contract log filtering. // FilterQuery contains options for contract log filtering.
type FilterQuery struct { type FilterQuery struct {
BlockHash *common.Hash // used by eth_getLogs, return logs only from block with this hash
FromBlock *big.Int // beginning of the queried range, nil means genesis block FromBlock *big.Int // beginning of the queried range, nil means genesis block
ToBlock *big.Int // end of the range, nil means latest block ToBlock *big.Int // end of the range, nil means latest block
Addresses []common.Address // restricts matches to events created by specific contracts Addresses []common.Address // restricts matches to events created by specific contracts

@ -60,10 +60,13 @@ func (b *LesApiBackend) HeaderByNumber(ctx context.Context, blockNr rpc.BlockNum
if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber {
return b.eth.blockchain.CurrentHeader(), nil return b.eth.blockchain.CurrentHeader(), nil
} }
return b.eth.blockchain.GetHeaderByNumberOdr(ctx, uint64(blockNr)) return b.eth.blockchain.GetHeaderByNumberOdr(ctx, uint64(blockNr))
} }
func (b *LesApiBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) {
return b.eth.blockchain.GetHeaderByHash(hash), nil
}
func (b *LesApiBackend) BlockByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Block, error) { func (b *LesApiBackend) BlockByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Block, error) {
header, err := b.HeaderByNumber(ctx, blockNr) header, err := b.HeaderByNumber(ctx, blockNr)
if header == nil || err != nil { if header == nil || err != nil {

Loading…
Cancel
Save