laconicd/ethereum/rpc/backend.go
Federico Kunze 2c722d03ce
rpc, evm: gRPC tests (#28)
* rpc, evm: grpc tests

* address validation

* update rpc

* test cleanup

* additional tests
2021-05-17 06:13:08 -04:00

337 lines
10 KiB
Go

package rpc
import (
"context"
"math/big"
"regexp"
"github.com/cosmos/ethermint/ethereum/rpc/types"
"github.com/pkg/errors"
tmtypes "github.com/tendermint/tendermint/types"
log "github.com/xlab/suplog"
"github.com/cosmos/cosmos-sdk/client"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"
ethermint "github.com/cosmos/ethermint/types"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
)
// Backend implements the functionality needed to filter changes.
// Implemented by EVMBackend.
type Backend interface {
// Used by block filter; also used for polling
BlockNumber() (hexutil.Uint64, error)
HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error)
HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error)
GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error)
GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error)
// returns the logs of a given block
GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error)
// Used by pending transaction filter
PendingTransactions() ([]*types.RPCTransaction, error)
// Used by log filter
GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error)
BloomStatus() (uint64, uint64)
}
var _ Backend = (*EVMBackend)(nil)
// EVMBackend implements the Backend interface
type EVMBackend struct {
ctx context.Context
clientCtx client.Context
queryClient *types.QueryClient // gRPC query client
logger log.Logger
}
// NewEVMBackend creates a new EVMBackend instance
func NewEVMBackend(clientCtx client.Context) *EVMBackend {
return &EVMBackend{
ctx: context.Background(),
clientCtx: clientCtx,
queryClient: types.NewQueryClient(clientCtx),
logger: log.WithField("module", "evm-backend"),
}
}
// BlockNumber returns the current block number.
func (e *EVMBackend) BlockNumber() (hexutil.Uint64, error) {
// NOTE: using 0 as min and max height returns the blockchain info up to the latest block.
info, err := e.clientCtx.Client.BlockchainInfo(e.ctx, 0, 0)
if err != nil {
return hexutil.Uint64(0), err
}
return hexutil.Uint64(info.LastHeight), nil
}
// GetBlockByNumber returns the block identified by number.
func (e *EVMBackend) GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) {
height := blockNum.Int64()
currentBlockNumber, _ := e.BlockNumber()
switch blockNum {
case types.EthLatestBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber - 1)
}
case types.EthPendingBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthEarliestBlockNumber:
height = 1
default:
if blockNum < 0 {
err := errors.Errorf("incorrect block height: %d", height)
return nil, err
} else if height > int64(currentBlockNumber) {
return nil, nil
}
}
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
if err != nil {
// e.logger.Debugf("GetBlockByNumber safely bumping down from %d to latest", height)
if resBlock, err = e.clientCtx.Client.Block(e.ctx, nil); err != nil {
e.logger.Debugln("GetBlockByNumber failed to get latest block")
return nil, nil
}
}
res, err := e.EthBlockFromTendermint(e.clientCtx, e.queryClient, resBlock.Block, fullTx)
if err != nil {
e.logger.WithError(err).Debugf("EthBlockFromTendermint failed with block %s", resBlock.Block.String())
}
return res, err
}
// GetBlockByHash returns the block identified by hash.
func (e *EVMBackend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) {
resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes())
if err != nil {
e.logger.Warningf("BlockByHash failed for %s", hash.Hex())
return nil, err
}
return e.EthBlockFromTendermint(e.clientCtx, e.queryClient, resBlock.Block, fullTx)
}
// EthBlockFromTendermint returns a JSON-RPC compatible Ethereum block from a given Tendermint block.
func (e *EVMBackend) EthBlockFromTendermint(
clientCtx client.Context,
queryClient evmtypes.QueryClient,
block *tmtypes.Block,
fullTx bool,
) (map[string]interface{}, error) {
req := &evmtypes.QueryTxReceiptsByBlockHeightRequest{}
txReceiptsResp, err := queryClient.TxReceiptsByBlockHeight(types.ContextWithHeight(block.Height), req)
if err != nil {
e.logger.Debugf("TxReceiptsByBlockHeight fail: %s", err.Error())
return nil, err
}
gasUsed := big.NewInt(0)
ethRPCTxs := make([]interface{}, 0, len(txReceiptsResp.Receipts))
for _, receipt := range txReceiptsResp.Receipts {
hash := common.HexToHash(receipt.Hash)
if fullTx {
// full txs from receipts
tx, err := types.NewTransactionFromData(
receipt.Data,
common.HexToAddress(receipt.From),
hash,
common.HexToHash(receipt.BlockHash),
receipt.BlockHeight,
receipt.Index,
)
if err != nil {
e.logger.WithError(err).Warningf("NewTransactionFromData for receipt %s failed", hash)
continue
}
ethRPCTxs = append(ethRPCTxs, tx)
gasUsed.Add(gasUsed, new(big.Int).SetUint64(receipt.Result.GasUsed))
} else {
// simply hashes
ethRPCTxs = append(ethRPCTxs, hash)
}
}
blockBloomResp, err := queryClient.BlockBloom(types.ContextWithHeight(block.Height), &evmtypes.QueryBlockBloomRequest{})
if err != nil {
e.logger.WithError(err).Debugln("failed to query BlockBloom at height", block.Height)
blockBloomResp.Bloom = ethtypes.Bloom{}.Bytes()
}
bloom := ethtypes.BytesToBloom(blockBloomResp.Bloom)
formattedBlock := types.FormatBlock(block.Header, block.Size(), ethermint.DefaultRPCGasLimit, gasUsed, ethRPCTxs, bloom)
e.logger.Infoln(formattedBlock)
return formattedBlock, nil
}
// HeaderByNumber returns the block header identified by height.
func (e *EVMBackend) HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error) {
height := blockNum.Int64()
currentBlockNumber, _ := e.BlockNumber()
switch blockNum {
case types.EthLatestBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber - 1)
}
case types.EthPendingBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthEarliestBlockNumber:
height = 1
default:
if blockNum < 0 {
err := errors.Errorf("incorrect block height: %d", height)
return nil, err
}
}
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
if err != nil {
e.logger.Warningf("HeaderByNumber failed")
return nil, err
}
req := &evmtypes.QueryBlockBloomRequest{}
res, err := e.queryClient.BlockBloom(types.ContextWithHeight(resBlock.Block.Height), req)
if err != nil {
e.logger.Warningf("HeaderByNumber BlockBloom fail %d", resBlock.Block.Height)
return nil, err
}
ethHeader := types.EthHeaderFromTendermint(resBlock.Block.Header)
ethHeader.Bloom = ethtypes.BytesToBloom(res.Bloom)
return ethHeader, nil
}
// HeaderByHash returns the block header identified by hash.
func (e *EVMBackend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) {
resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, blockHash.Bytes())
if err != nil {
e.logger.Warningf("HeaderByHash fail")
return nil, err
}
req := &evmtypes.QueryBlockBloomRequest{}
res, err := e.queryClient.BlockBloom(types.ContextWithHeight(resBlock.Block.Height), req)
if err != nil {
e.logger.Warningf("HeaderByHash BlockBloom fail %d", resBlock.Block.Height)
return nil, err
}
ethHeader := types.EthHeaderFromTendermint(resBlock.Block.Header)
ethHeader.Bloom = ethtypes.BytesToBloom(res.Bloom)
return ethHeader, nil
}
// GetTransactionLogs returns the logs given a transaction hash.
// It returns an error if there's an encoding error.
// If no logs are found for the tx hash, the error is nil.
func (e *EVMBackend) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) {
req := &evmtypes.QueryTxLogsRequest{
Hash: txHash.String(),
}
res, err := e.queryClient.TxLogs(e.ctx, req)
if err != nil {
e.logger.Warningf("TxLogs fail")
return nil, err
}
return evmtypes.LogsToEthereum(res.Logs), nil
}
// PendingTransactions returns the transactions that are in the transaction pool
// and have a from address that is one of the accounts this node manages.
func (e *EVMBackend) PendingTransactions() ([]*types.RPCTransaction, error) {
return []*types.RPCTransaction{}, nil
}
// GetLogs returns all the logs from all the ethereum transactions in a block.
func (e *EVMBackend) GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error) {
// NOTE: we query the state in case the tx result logs are not persisted after an upgrade.
req := &evmtypes.QueryBlockLogsRequest{
Hash: blockHash.String(),
}
res, err := e.queryClient.BlockLogs(e.ctx, req)
if err != nil {
e.logger.Warningf("BlockLogs fail")
return nil, err
}
var blockLogs = [][]*ethtypes.Log{}
for _, txLog := range res.TxLogs {
blockLogs = append(blockLogs, txLog.EthLogs())
}
return blockLogs, nil
}
// This is very brittle, see: https://github.com/tendermint/tendermint/issues/4740
var regexpMissingHeight = regexp.MustCompile(`height \d+ (must be less than or equal to|is not available)`)
func (e *EVMBackend) GetLogsByNumber(blockNum types.BlockNumber) ([][]*ethtypes.Log, error) {
height := blockNum.Int64()
currentBlockNumber, _ := e.BlockNumber()
switch blockNum {
case types.EthLatestBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber - 1)
}
case types.EthPendingBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthEarliestBlockNumber:
height = 1
default:
if blockNum < 0 {
err := errors.Errorf("incorrect block height: %d", height)
return nil, err
}
}
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
if err != nil {
if regexpMissingHeight.MatchString(err.Error()) {
return [][]*ethtypes.Log{}, nil
}
e.logger.Warningf("failed to query block at %d", height)
return nil, err
}
return e.GetLogs(common.BytesToHash(resBlock.BlockID.Hash))
}
// BloomStatus returns the BloomBitsBlocks and the number of processed sections maintained
// by the chain indexer.
func (e *EVMBackend) BloomStatus() (uint64, uint64) {
return 4096, 0
}