Description
Is there an existing issue for this?
- I have searched the existing issues
What happened?
Error handling on eth_getLogs rpc call
Description
We found in the rpc
package that some calls made to Tendermint RPC are not handled properly. In specific, the eth_getLogs
rpc call when providing a blockHash
.
TendermintBlockResultByNumber
Starting on the Logs
function in the filters
package:
// 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, logLimit int, blockLimit int64) ([]*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{}) {
resBlock, err := f.backend.TendermintBlockByHash(*f.criteria.BlockHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch header by hash %s: %w", f.criteria.BlockHash, err)
}
blockRes, err := f.backend.TendermintBlockResultByNumber(&resBlock.Block.Height)
if err != nil {
f.logger.Debug("failed to fetch block result from Tendermint", "height", resBlock.Block.Height, "error", err.Error())
return nil, nil
}
bloom, err := f.backend.BlockBloom(blockRes)
if err != nil {
return nil, err
}
return f.blockLogs(blockRes, bloom)
}
// ...
}
When fetching the block result, the TendermintBlockResultByNumber
function returns the result and an error. However, the error is not handled and the function returns nil, nil
, only debugging the error.
By not handling the error, the Logs
function will return nil, nil
, which will cause the GetLogs
to call the returnLogs
function with a nil
argument, returning an empty array.
// GetLogs returns logs matching the given argument that are stored within the state.
//
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs
func (api *PublicFilterAPI) GetLogs(ctx context.Context, crit filters.FilterCriteria) ([]*ethtypes.Log, error) {
var filter *Filter
if crit.BlockHash != nil {
// Block filter requested, construct a single-shot filter
filter = NewBlockFilter(api.logger, api.backend, crit)
} else {
// Convert the RPC block numbers into internal representations
begin := rpc.LatestBlockNumber.Int64()
if crit.FromBlock != nil {
begin = crit.FromBlock.Int64()
}
end := rpc.LatestBlockNumber.Int64()
if crit.ToBlock != nil {
end = crit.ToBlock.Int64()
}
// Construct the range filter
filter = NewRangeFilter(api.logger, api.backend, begin, end, crit.Addresses, crit.Topics)
}
// Run the filter and return all the logs
logs, err := filter.Logs(ctx, int(api.backend.RPCLogsCap()), int64(api.backend.RPCBlockRangeCap()))
if err != nil {
return nil, err
}
return returnLogs(logs), err
}
// returnLogs is a helper that will return an empty log array in case the given logs array is nil,
// otherwise the given logs array is returned.
func returnLogs(logs []*ethtypes.Log) []*ethtypes.Log {
if logs == nil {
return []*ethtypes.Log{}
}
return logs
}
Solution
The solution is to handle the returned error in the TendermintBlockResultByNumber
function.
// TendermintBlockResultByNumber returns a Tendermint-formatted block result
// by block number
func (b *Backend) TendermintBlockResultByNumber(height *int64) (*tmrpctypes.ResultBlockResults, error) {
res, err := b.rpcClient.BlockResults(b.ctx, height)
if err != nil {
return nil, fmt.Errorf("failed to fetch block result from Tendermint %d: %w", *height, err)
}
return res, nil
}
And properly handle the error in the Logs
function.
// 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, logLimit int, blockLimit int64) ([]*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{}) {
resBlock, err := f.backend.TendermintBlockByHash(*f.criteria.BlockHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch header by hash %s: %w", f.criteria.BlockHash, err)
}
blockRes, err := f.backend.TendermintBlockResultByNumber(&resBlock.Block.Height)
if err != nil {
f.logger.Debug("failed to fetch block result from Tendermint", "height", resBlock.Block.Height, "error", err.Error())
return nil, err
}
bloom, err := f.backend.BlockBloom(blockRes)
if err != nil {
return nil, err
}
return f.blockLogs(blockRes, bloom)
}
// ...
}
TendermintBlockByHash
Also, TendermintBlockByHash
returns a pair of nil, nil
when the resBlock
or resBlock.Block
is nil.
// TendermintBlockByHash returns a Tendermint-formatted block by block number
func (b *Backend) TendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) {
resBlock, err := b.rpcClient.BlockByHash(b.ctx, blockHash.Bytes())
if err != nil {
b.logger.Debug("tendermint client failed to get block", "blockHash", blockHash.Hex(), "error", err.Error())
return nil, err
}
if resBlock == nil || resBlock.Block == nil {
b.logger.Debug("TendermintBlockByHash block not found", "blockHash", blockHash.Hex())
return nil, nil
}
return resBlock, nil
}
This will cause the TendermintBlockResultByNumber
in the Logs
function to panic since it will receive a nil
resBlock
and will try to access the Block.Height
field.
// 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, logLimit int, blockLimit int64) ([]*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{}) {
resBlock, err := f.backend.TendermintBlockByHash(*f.criteria.BlockHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch header by hash %s: %w", f.criteria.BlockHash, err)
}
blockRes, err := f.backend.TendermintBlockResultByNumber(&resBlock.Block.Height)
if err != nil {
f.logger.Debug("failed to fetch block result from Tendermint", "height", resBlock.Block.Height, "error", err.Error())
return nil, nil
}
// ...
}
// ...
}
Solution
The solution is to return an error when checking that resBlock
or resBlock.Block
is nil.
// TendermintBlockByHash returns a Tendermint-formatted block by block number
func (b *Backend) TendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) {
resBlock, err := b.rpcClient.BlockByHash(b.ctx, blockHash.Bytes())
if err != nil {
b.logger.Debug("tendermint client failed to get block", "blockHash", blockHash.Hex(), "error", err.Error())
return nil, err
}
if resBlock == nil || resBlock.Block == nil {
b.logger.Error("tendermint block not found", "blockHash", blockHash.Hex())
return nil, fmt.Errorf("block not found for hash %s", blockHash.Hex())
}
return resBlock, nil
}
Affected functions in cosmos/evm
package
This issue affects the following functions in the cosmos/evm
package:
It is possible that other rpc calls are affected by this issue.
Cosmos EVM Version
main
How to reproduce?
No response