Skip to content

[Bug]: eth_getLogs rpc error handling when providing a BlockHash #172

Open
@GuillemGarciaDev

Description

@GuillemGarciaDev

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions