laconicd/rpc/ethereum/backend/backend.go
Jongwhan Lee 392d1dd8cf
rpc: eth_feeHistory (#734)
* Problem: missing json rpc of eth_feeHistory #685

add oracle backend

space ready

structure ok

refactoring

return feehistory

data flow ok

basefee

set gas used ratio

computing reward

add testing

add gas used

prepare data

fill reward

increase coin

fixing api

add mac

add launch

gas used ratio ok

print element

reward workes

reward working

fix panic

value correct

remove debugging log

tidy up

tidy up

remove oracle

tidy up

fix handler crash

add unit test

tidy up

add limit check

reformat

fix lint

fix lint

fix lint

fix lint

Update rpc/ethereum/backend/feebackend.go

thanks

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

Update rpc/ethereum/backend/feebackend.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

Update rpc/ethereum/backend/feebackend.go

thanks

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

Update rpc/ethereum/backend/feebackend.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

fix compile error

split lines

remove temporary string conversion

return error if gaslimit is 0

move OneFeeHistory to types

add comment

only err check

Update rpc/ethereum/backend/feebackend.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

Update rpc/ethereum/backend/feebackend.go

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>

tidy up

add feehistory-cap

* Apply suggestions from code review

* changelog

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>
Co-authored-by: Federico Kunze Küllmer <federico.kunze94@gmail.com>
2021-11-17 11:58:52 +00:00

1066 lines
32 KiB
Go

package backend
import (
"bytes"
"context"
"encoding/json"
"fmt"
"math/big"
"strconv"
"time"
"github.com/cosmos/cosmos-sdk/client/flags"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/server"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
tmrpctypes "github.com/tendermint/tendermint/rpc/core/types"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"github.com/pkg/errors"
"github.com/tendermint/tendermint/libs/log"
tmtypes "github.com/tendermint/tendermint/types"
"github.com/cosmos/cosmos-sdk/client"
sdk "github.com/cosmos/cosmos-sdk/types"
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/tharsis/ethermint/rpc/ethereum/namespaces/eth/filters"
"github.com/tharsis/ethermint/rpc/ethereum/types"
"github.com/tharsis/ethermint/server/config"
ethermint "github.com/tharsis/ethermint/types"
evmtypes "github.com/tharsis/ethermint/x/evm/types"
feemarkettypes "github.com/tharsis/ethermint/x/feemarket/types"
)
// Backend implements the functionality shared within namespaces.
// Implemented by EVMBackend.
type Backend interface {
// Fee API
FeeHistory(blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*types.FeeHistoryResult, error)
// General Ethereum API
RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection
RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection
RPCTxFeeCap() float64 // RPCTxFeeCap is the global transaction fee(price * gaslimit) cap for send-transaction variants. The unit is ether.
RPCMinGasPrice() int64
SuggestGasTipCap() (*big.Int, error)
// Blockchain API
BlockNumber() (hexutil.Uint64, error)
GetTendermintBlockByNumber(blockNum types.BlockNumber) (*tmrpctypes.ResultBlock, error)
GetTendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error)
GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error)
GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error)
BlockByNumber(blockNum types.BlockNumber) (*ethtypes.Block, error)
BlockByHash(blockHash common.Hash) (*ethtypes.Block, error)
CurrentHeader() *ethtypes.Header
HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error)
HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error)
PendingTransactions() ([]*sdk.Tx, error)
GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error)
GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error)
SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error)
GetCoinbase() (sdk.AccAddress, error)
GetTransactionByHash(txHash common.Hash) (*types.RPCTransaction, error)
GetTxByEthHash(txHash common.Hash) (*tmrpctypes.ResultTx, error)
EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *types.BlockNumber) (hexutil.Uint64, error)
BaseFee(height int64) (*big.Int, error)
// Filter API
BloomStatus() (uint64, uint64)
GetLogs(hash common.Hash) ([][]*ethtypes.Log, error)
GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error)
GetFilteredBlocks(from int64, to int64, filter [][]filters.BloomIV, filterAddresses bool) ([]int64, error)
ChainConfig() *params.ChainConfig
SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error)
GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock) []*evmtypes.MsgEthereumTx
}
var _ Backend = (*EVMBackend)(nil)
var bAttributeKeyEthereumBloom = []byte(evmtypes.AttributeKeyEthereumBloom)
// EVMBackend implements the Backend interface
type EVMBackend struct {
ctx context.Context
clientCtx client.Context
queryClient *types.QueryClient // gRPC query client
logger log.Logger
chainID *big.Int
cfg config.Config
}
// NewEVMBackend creates a new EVMBackend instance
func NewEVMBackend(ctx *server.Context, logger log.Logger, clientCtx client.Context) *EVMBackend {
chainID, err := ethermint.ParseChainID(clientCtx.ChainID)
if err != nil {
panic(err)
}
appConf := config.GetConfig(ctx.Viper)
return &EVMBackend{
ctx: context.Background(),
clientCtx: clientCtx,
queryClient: types.NewQueryClient(clientCtx),
logger: logger.With("module", "evm-backend"),
chainID: chainID,
cfg: appConf,
}
}
// BlockNumber returns the current block number in abci app state.
// Because abci app state could lag behind from tendermint latest block, it's more stable
// for the client to use the latest block number in abci app state than tendermint rpc.
func (e *EVMBackend) BlockNumber() (hexutil.Uint64, error) {
// do any grpc query, ignore the response and use the returned block height
var header metadata.MD
_, err := e.queryClient.Params(e.ctx, &evmtypes.QueryParamsRequest{}, grpc.Header(&header))
if err != nil {
return hexutil.Uint64(0), err
}
blockHeightHeader := header.Get(grpctypes.GRPCBlockHeightHeader)
if headerLen := len(blockHeightHeader); headerLen != 1 {
return 0, fmt.Errorf("unexpected '%s' gRPC header length; got %d, expected: %d", grpctypes.GRPCBlockHeightHeader, headerLen, 1)
}
height, err := strconv.ParseUint(blockHeightHeader[0], 10, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse block height: %w", err)
}
return hexutil.Uint64(height), nil
}
// GetBlockByNumber returns the block identified by number.
func (e *EVMBackend) GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) {
resBlock, err := e.GetTendermintBlockByNumber(blockNum)
if err != nil {
return nil, err
}
// return if requested block height is greater than the current one
if resBlock == nil || resBlock.Block == nil {
return nil, nil
}
res, err := e.EthBlockFromTendermint(resBlock.Block, fullTx)
if err != nil {
e.logger.Debug("EthBlockFromTendermint failed", "height", blockNum, "error", err.Error())
return nil, err
}
return res, nil
}
// 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.Debug("BlockByHash block not found", "hash", hash.Hex(), "error", err.Error())
return nil, err
}
if resBlock == nil || resBlock.Block == nil {
e.logger.Debug("BlockByHash block not found", "hash", hash.Hex())
return nil, nil
}
return e.EthBlockFromTendermint(resBlock.Block, fullTx)
}
// BlockByNumber returns the block identified by number.
func (e *EVMBackend) BlockByNumber(blockNum types.BlockNumber) (*ethtypes.Block, error) {
height := blockNum.Int64()
switch blockNum {
case types.EthLatestBlockNumber:
currentBlockNumber, _ := e.BlockNumber()
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthPendingBlockNumber:
currentBlockNumber, _ := e.BlockNumber()
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthEarliestBlockNumber:
height = 1
default:
if blockNum < 0 {
return nil, errors.Errorf("incorrect block height: %d", height)
}
}
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
if err != nil {
e.logger.Debug("HeaderByNumber failed", "height", height)
return nil, err
}
if resBlock == nil || resBlock.Block == nil {
return nil, errors.Errorf("block not found for height %d", height)
}
return e.EthBlockFromTm(resBlock.Block)
}
// BlockByHash returns the block identified by hash.
func (e *EVMBackend) BlockByHash(hash common.Hash) (*ethtypes.Block, error) {
resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes())
if err != nil {
e.logger.Debug("HeaderByHash failed", "hash", hash.Hex())
return nil, err
}
if resBlock == nil || resBlock.Block == nil {
return nil, errors.Errorf("block not found for hash %s", hash)
}
return e.EthBlockFromTm(resBlock.Block)
}
func (e *EVMBackend) EthBlockFromTm(block *tmtypes.Block) (*ethtypes.Block, error) {
height := block.Height
bloom, err := e.BlockBloom(&height)
if err != nil {
e.logger.Debug("HeaderByNumber BlockBloom failed", "height", height)
}
baseFee, err := e.BaseFee(height)
if err != nil {
e.logger.Debug("HeaderByNumber BaseFee failed", "height", height, "error", err.Error())
return nil, err
}
ethHeader := types.EthHeaderFromTendermint(block.Header, bloom, baseFee)
var txs []*ethtypes.Transaction
for _, txBz := range block.Txs {
tx, err := e.clientCtx.TxConfig.TxDecoder()(txBz)
if err != nil {
e.logger.Debug("failed to decode transaction in block", "height", height, "error", err.Error())
continue
}
for _, msg := range tx.GetMsgs() {
ethMsg, ok := msg.(*evmtypes.MsgEthereumTx)
if !ok {
continue
}
tx := ethMsg.AsTransaction()
txs = append(txs, tx)
}
}
// TODO: add tx receipts
ethBlock := ethtypes.NewBlock(ethHeader, txs, nil, nil, nil)
return ethBlock, nil
}
// GetTendermintBlockByNumber returns a Tendermint format block by block number
func (e *EVMBackend) GetTendermintBlockByNumber(blockNum types.BlockNumber) (*tmrpctypes.ResultBlock, error) {
height := blockNum.Int64()
currentBlockNumber, _ := e.BlockNumber()
switch blockNum {
case types.EthLatestBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthPendingBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthEarliestBlockNumber:
height = 1
default:
if blockNum < 0 {
return nil, errors.Errorf("cannot fetch a negative block height: %d", height)
}
if height > int64(currentBlockNumber) {
return nil, nil
}
}
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
if err != nil {
if resBlock, err = e.clientCtx.Client.Block(e.ctx, nil); err != nil {
e.logger.Debug("tendermint client failed to get latest block", "height", height, "error", err.Error())
return nil, nil
}
}
if resBlock.Block == nil {
e.logger.Debug("GetBlockByNumber block not found", "height", height)
return nil, nil
}
return resBlock, nil
}
// GetTendermintBlockByHash returns a Tendermint format block by block number
func (e *EVMBackend) GetTendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) {
resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, blockHash.Bytes())
if err != nil {
e.logger.Debug("tendermint client failed to get block", "blockHash", blockHash.Hex(), "error", err.Error())
}
if resBlock == nil || resBlock.Block == nil {
e.logger.Debug("GetBlockByNumber block not found", "blockHash", blockHash.Hex())
return nil, nil
}
return resBlock, nil
}
// BlockBloom query block bloom filter from block results
func (e *EVMBackend) BlockBloom(height *int64) (ethtypes.Bloom, error) {
result, err := e.clientCtx.Client.BlockResults(e.ctx, height)
if err != nil {
return ethtypes.Bloom{}, err
}
for _, event := range result.EndBlockEvents {
if event.Type != evmtypes.EventTypeBlockBloom {
continue
}
for _, attr := range event.Attributes {
if bytes.Equal(attr.Key, bAttributeKeyEthereumBloom) {
return ethtypes.BytesToBloom(attr.Value), nil
}
}
}
return ethtypes.Bloom{}, errors.New("block bloom event is not found")
}
// EthBlockFromTendermint returns a JSON-RPC compatible Ethereum block from a given Tendermint block and its block result.
func (e *EVMBackend) EthBlockFromTendermint(
block *tmtypes.Block,
fullTx bool,
) (map[string]interface{}, error) {
ethRPCTxs := []interface{}{}
ctx := types.ContextWithHeight(block.Height)
baseFee, err := e.BaseFee(block.Height)
if err != nil {
return nil, err
}
for i, txBz := range block.Txs {
tx, err := e.clientCtx.TxConfig.TxDecoder()(txBz)
if err != nil {
e.logger.Debug("failed to decode transaction in block", "height", block.Height, "error", err.Error())
continue
}
for _, msg := range tx.GetMsgs() {
ethMsg, ok := msg.(*evmtypes.MsgEthereumTx)
if !ok {
continue
}
tx := ethMsg.AsTransaction()
// check tx exists on EVM and it has the correct block height
ethTx, err := e.GetTxByEthHash(tx.Hash())
if err != nil || ethTx.Height != block.Height {
e.logger.Debug("failed to query eth tx", "hash", tx.Hash().Hex())
continue
}
if !fullTx {
hash := tx.Hash()
ethRPCTxs = append(ethRPCTxs, hash)
continue
}
rpcTx, err := types.NewRPCTransaction(
tx,
common.BytesToHash(block.Hash()),
uint64(block.Height),
uint64(i),
baseFee,
)
if err != nil {
e.logger.Debug("NewTransactionFromData for receipt failed", "hash", tx.Hash().Hex(), "error", err.Error())
continue
}
ethRPCTxs = append(ethRPCTxs, rpcTx)
}
}
bloom, err := e.BlockBloom(&block.Height)
if err != nil {
e.logger.Debug("failed to query BlockBloom", "height", block.Height, "error", err.Error())
}
req := &evmtypes.QueryValidatorAccountRequest{
ConsAddress: sdk.ConsAddress(block.Header.ProposerAddress).String(),
}
res, err := e.queryClient.ValidatorAccount(ctx, req)
if err != nil {
e.logger.Debug(
"failed to query validator operator address",
"height", block.Height,
"cons-address", req.ConsAddress,
"error", err.Error(),
)
return nil, err
}
addr, err := sdk.AccAddressFromBech32(res.AccountAddress)
if err != nil {
return nil, err
}
validatorAddr := common.BytesToAddress(addr)
gasLimit, err := types.BlockMaxGasFromConsensusParams(ctx, e.clientCtx)
if err != nil {
e.logger.Error("failed to query consensus params", "error", err.Error())
}
resBlockResult, err := e.clientCtx.Client.BlockResults(e.ctx, &block.Height)
if err != nil {
e.logger.Debug("EthBlockFromTendermint block result not found", "height", block.Height, "error", err.Error())
return nil, err
}
gasUsed := uint64(0)
for _, txsResult := range resBlockResult.TxsResults {
gasUsed += uint64(txsResult.GetGasUsed())
}
formattedBlock := types.FormatBlock(
block.Header, block.Size(),
gasLimit, new(big.Int).SetUint64(gasUsed),
ethRPCTxs, bloom, validatorAddr, baseFee,
)
return formattedBlock, nil
}
// CurrentHeader returns the latest block header
func (e *EVMBackend) CurrentHeader() *ethtypes.Header {
header, _ := e.HeaderByNumber(types.EthLatestBlockNumber)
return header
}
// HeaderByNumber returns the block header identified by height.
func (e *EVMBackend) HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error) {
height := blockNum.Int64()
switch blockNum {
case types.EthLatestBlockNumber:
currentBlockNumber, _ := e.BlockNumber()
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthPendingBlockNumber:
currentBlockNumber, _ := e.BlockNumber()
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthEarliestBlockNumber:
height = 1
default:
if blockNum < 0 {
return nil, errors.Errorf("incorrect block height: %d", height)
}
}
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
if err != nil {
e.logger.Debug("HeaderByNumber failed")
return nil, err
}
bloom, err := e.BlockBloom(&resBlock.Block.Height)
if err != nil {
e.logger.Debug("HeaderByNumber BlockBloom failed", "height", resBlock.Block.Height)
}
baseFee, err := e.BaseFee(resBlock.Block.Height)
if err != nil {
e.logger.Debug("HeaderByNumber BaseFee failed", "height", resBlock.Block.Height, "error", err.Error())
return nil, err
}
ethHeader := types.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee)
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.Debug("HeaderByHash failed", "hash", blockHash.Hex())
return nil, err
}
if resBlock == nil || resBlock.Block == nil {
return nil, errors.Errorf("block not found for hash %s", blockHash.Hex())
}
bloom, err := e.BlockBloom(&resBlock.Block.Height)
if err != nil {
e.logger.Debug("HeaderByHash BlockBloom failed", "height", resBlock.Block.Height)
}
baseFee, err := e.BaseFee(resBlock.Block.Height)
if err != nil {
e.logger.Debug("HeaderByHash BaseFee failed", "height", resBlock.Block.Height, "error", err.Error())
return nil, err
}
ethHeader := types.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee)
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) {
tx, err := e.GetTxByEthHash(txHash)
if err != nil {
return nil, err
}
return TxLogsFromEvents(tx.TxResult.Events)
}
// 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() ([]*sdk.Tx, error) {
res, err := e.clientCtx.Client.UnconfirmedTxs(e.ctx, nil)
if err != nil {
return nil, err
}
result := make([]*sdk.Tx, 0, len(res.Txs))
for _, txBz := range res.Txs {
tx, err := e.clientCtx.TxConfig.TxDecoder()(txBz)
if err != nil {
return nil, err
}
result = append(result, &tx)
}
return result, nil
}
// GetLogsByHeight returns all the logs from all the ethereum transactions in a block.
func (e *EVMBackend) GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error) {
// NOTE: we query the state in case the tx result logs are not persisted after an upgrade.
blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, height)
if err != nil {
return nil, err
}
blockLogs := [][]*ethtypes.Log{}
for _, txResult := range blockRes.TxsResults {
logs, err := TxLogsFromEvents(txResult.Events)
if err != nil {
return nil, err
}
blockLogs = append(blockLogs, logs)
}
return blockLogs, nil
}
// GetLogs returns all the logs from all the ethereum transactions in a block.
func (e *EVMBackend) GetLogs(hash common.Hash) ([][]*ethtypes.Log, error) {
block, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes())
if err != nil {
return nil, err
}
return e.GetLogsByHeight(&block.Block.Header.Height)
}
func (e *EVMBackend) GetLogsByNumber(blockNum types.BlockNumber) ([][]*ethtypes.Log, error) {
height := blockNum.Int64()
switch blockNum {
case types.EthLatestBlockNumber:
currentBlockNumber, _ := e.BlockNumber()
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthPendingBlockNumber:
currentBlockNumber, _ := e.BlockNumber()
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthEarliestBlockNumber:
height = 1
default:
if blockNum < 0 {
return nil, errors.Errorf("incorrect block height: %d", height)
}
}
return e.GetLogsByHeight(&height)
}
// BloomStatus returns the BloomBitsBlocks and the number of processed sections maintained
// by the chain indexer.
func (e *EVMBackend) BloomStatus() (uint64, uint64) {
return 4096, 0
}
// GetCoinbase is the address that staking rewards will be send to (alias for Etherbase).
func (e *EVMBackend) GetCoinbase() (sdk.AccAddress, error) {
node, err := e.clientCtx.GetNode()
if err != nil {
return nil, err
}
status, err := node.Status(e.ctx)
if err != nil {
return nil, err
}
req := &evmtypes.QueryValidatorAccountRequest{
ConsAddress: sdk.ConsAddress(status.ValidatorInfo.Address).String(),
}
res, err := e.queryClient.ValidatorAccount(e.ctx, req)
if err != nil {
return nil, err
}
address, _ := sdk.AccAddressFromBech32(res.AccountAddress)
return address, nil
}
// GetTransactionByHash returns the Ethereum format transaction identified by Ethereum transaction hash
func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransaction, error) {
res, err := e.GetTxByEthHash(txHash)
hexTx := txHash.Hex()
if err != nil {
// try to find tx in mempool
txs, err := e.PendingTransactions()
if err != nil {
e.logger.Debug("tx not found", "hash", hexTx, "error", err.Error())
return nil, nil
}
for _, tx := range txs {
msg, err := evmtypes.UnwrapEthereumMsg(tx)
if err != nil {
// not ethereum tx
continue
}
if msg.Hash == hexTx {
rpctx, err := types.NewTransactionFromMsg(
msg,
common.Hash{},
uint64(0),
uint64(0),
e.chainID,
)
if err != nil {
return nil, err
}
return rpctx, nil
}
}
e.logger.Debug("tx not found", "hash", hexTx)
return nil, nil
}
resBlock, err := e.clientCtx.Client.Block(e.ctx, &res.Height)
if err != nil {
e.logger.Debug("block not found", "height", res.Height, "error", err.Error())
return nil, nil
}
var txIndex uint64
msgs := e.GetEthereumMsgsFromTendermintBlock(resBlock)
for i := range msgs {
if msgs[i].Hash == hexTx {
txIndex = uint64(i)
break
}
}
msg := msgs[txIndex]
return types.NewTransactionFromMsg(
msg,
common.BytesToHash(resBlock.Block.Hash()),
uint64(res.Height),
txIndex,
e.chainID,
)
}
// GetTxByEthHash uses `/tx_query` to find transaction by ethereum tx hash
// TODO: Don't need to convert once hashing is fixed on Tendermint
// https://github.com/tendermint/tendermint/issues/6539
func (e *EVMBackend) GetTxByEthHash(hash common.Hash) (*tmrpctypes.ResultTx, error) {
query := fmt.Sprintf("%s.%s='%s'", evmtypes.TypeMsgEthereumTx, evmtypes.AttributeKeyEthereumTxHash, hash.Hex())
resTxs, err := e.clientCtx.Client.TxSearch(e.ctx, query, false, nil, nil, "")
if err != nil {
return nil, err
}
if len(resTxs.Txs) == 0 {
return nil, errors.Errorf("ethereum tx not found for hash %s", hash.Hex())
}
return resTxs.Txs[0], nil
}
func (e *EVMBackend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) {
// Look up the wallet containing the requested signer
_, err := e.clientCtx.Keyring.KeyByAddress(sdk.AccAddress(args.From.Bytes()))
if err != nil {
e.logger.Error("failed to find key in keyring", "address", args.From, "error", err.Error())
return common.Hash{}, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error())
}
args, err = e.SetTxDefaults(args)
if err != nil {
return common.Hash{}, err
}
msg := args.ToTransaction()
if err := msg.ValidateBasic(); err != nil {
e.logger.Debug("tx failed basic validation", "error", err.Error())
return common.Hash{}, err
}
bn, err := e.BlockNumber()
if err != nil {
e.logger.Debug("failed to fetch latest block number", "error", err.Error())
return common.Hash{}, err
}
signer := ethtypes.MakeSigner(e.ChainConfig(), new(big.Int).SetUint64(uint64(bn)))
// Sign transaction
if err := msg.Sign(signer, e.clientCtx.Keyring); err != nil {
e.logger.Debug("failed to sign tx", "error", err.Error())
return common.Hash{}, err
}
// Assemble transaction from fields
builder, ok := e.clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder)
if !ok {
e.logger.Error("clientCtx.TxConfig.NewTxBuilder returns unsupported builder", "error", err.Error())
}
option, err := codectypes.NewAnyWithValue(&evmtypes.ExtensionOptionsEthereumTx{})
if err != nil {
e.logger.Error("codectypes.NewAnyWithValue failed to pack an obvious value", "error", err.Error())
return common.Hash{}, err
}
builder.SetExtensionOptions(option)
if err = builder.SetMsgs(msg); err != nil {
e.logger.Error("builder.SetMsgs failed", "error", err.Error())
}
// Query params to use the EVM denomination
res, err := e.queryClient.QueryClient.Params(e.ctx, &evmtypes.QueryParamsRequest{})
if err != nil {
e.logger.Error("failed to query evm params", "error", err.Error())
return common.Hash{}, err
}
txData, err := evmtypes.UnpackTxData(msg.Data)
if err != nil {
e.logger.Error("failed to unpack tx data", "error", err.Error())
return common.Hash{}, err
}
fees := sdk.Coins{sdk.NewCoin(res.Params.EvmDenom, sdk.NewIntFromBigInt(txData.Fee()))}
builder.SetFeeAmount(fees)
builder.SetGasLimit(msg.GetGas())
// Encode transaction by default Tx encoder
txEncoder := e.clientCtx.TxConfig.TxEncoder()
txBytes, err := txEncoder(builder.GetTx())
if err != nil {
e.logger.Error("failed to encode eth tx using default encoder", "error", err.Error())
return common.Hash{}, err
}
txHash := msg.AsTransaction().Hash()
// Broadcast transaction in sync mode (default)
// NOTE: If error is encountered on the node, the broadcast will not return an error
syncCtx := e.clientCtx.WithBroadcastMode(flags.BroadcastSync)
rsp, err := syncCtx.BroadcastTx(txBytes)
if err != nil || rsp.Code != 0 {
if err == nil {
err = errors.New(rsp.RawLog)
}
e.logger.Error("failed to broadcast tx", "error", err.Error())
return txHash, err
}
// Return transaction hash
return txHash, nil
}
// EstimateGas returns an estimate of gas usage for the given smart contract call.
func (e *EVMBackend) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *types.BlockNumber) (hexutil.Uint64, error) {
blockNr := types.EthPendingBlockNumber
if blockNrOptional != nil {
blockNr = *blockNrOptional
}
bz, err := json.Marshal(&args)
if err != nil {
return 0, err
}
req := evmtypes.EthCallRequest{
Args: bz,
GasCap: e.RPCGasCap(),
}
// From ContextWithHeight: if the provided height is 0,
// it will return an empty context and the gRPC query will use
// the latest block height for querying.
res, err := e.queryClient.EstimateGas(types.ContextWithHeight(blockNr.Int64()), &req)
if err != nil {
return 0, err
}
return hexutil.Uint64(res.Gas), nil
}
// GetTransactionCount returns the number of transactions at the given address up to the given block number.
func (e *EVMBackend) GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error) {
// Get nonce (sequence) from account
from := sdk.AccAddress(address.Bytes())
accRet := e.clientCtx.AccountRetriever
err := accRet.EnsureExists(e.clientCtx, from)
if err != nil {
// account doesn't exist yet, return 0
n := hexutil.Uint64(0)
return &n, nil
}
includePending := blockNum == types.EthPendingBlockNumber
nonce, err := e.getAccountNonce(address, includePending, blockNum.Int64(), e.logger)
if err != nil {
return nil, err
}
n := hexutil.Uint64(nonce)
return &n, nil
}
// RPCGasCap is the global gas cap for eth-call variants.
func (e *EVMBackend) RPCGasCap() uint64 {
return e.cfg.JSONRPC.GasCap
}
// RPCEVMTimeout is the global evm timeout for eth-call variants.
func (e *EVMBackend) RPCEVMTimeout() time.Duration {
return e.cfg.JSONRPC.EVMTimeout
}
// RPCGasCap is the global gas cap for eth-call variants.
func (e *EVMBackend) RPCTxFeeCap() float64 {
return e.cfg.JSONRPC.TxFeeCap
}
// RPCFilterCap is the limit for total number of filters that can be created
func (e *EVMBackend) RPCFilterCap() int32 {
return e.cfg.JSONRPC.FilterCap
}
// RPCFeeHistoryCap is the limit for total number of blocks that can be fetched
func (e *EVMBackend) RPCFeeHistoryCap() int32 {
return e.cfg.JSONRPC.FeeHistoryCap
}
// RPCMinGasPrice returns the minimum gas price for a transaction obtained from
// the node config. If set value is 0, it will default to 20.
func (e *EVMBackend) RPCMinGasPrice() int64 {
evmParams, err := e.queryClient.Params(e.ctx, &evmtypes.QueryParamsRequest{})
if err != nil {
return ethermint.DefaultGasPrice
}
minGasPrice := e.cfg.GetMinGasPrices()
amt := minGasPrice.AmountOf(evmParams.Params.EvmDenom).TruncateInt64()
if amt == 0 {
return ethermint.DefaultGasPrice
}
return amt
}
// ChainConfig return the ethereum chain configuration
func (e *EVMBackend) ChainConfig() *params.ChainConfig {
params, err := e.queryClient.Params(e.ctx, &evmtypes.QueryParamsRequest{})
if err != nil {
return nil
}
return params.Params.ChainConfig.EthereumConfig(e.chainID)
}
// SuggestGasTipCap returns the suggested tip cap
func (e *EVMBackend) SuggestGasTipCap() (*big.Int, error) {
out := new(big.Int).SetInt64(e.RPCMinGasPrice())
return out, nil
}
// BaseFee returns the base fee tracked by the Fee Market module. If the base fee is not enabled,
// it returns the initial base fee amount. Return nil if London is not activated.
func (e *EVMBackend) BaseFee(height int64) (*big.Int, error) {
cfg := e.ChainConfig()
if !cfg.IsLondon(new(big.Int).SetInt64(height)) {
return nil, nil
}
// Checks the feemarket param NoBaseFee settings, return 0 if it is enabled.
resParams, err := e.queryClient.FeeMarket.Params(e.ctx, &feemarkettypes.QueryParamsRequest{})
if err != nil {
return nil, err
}
if resParams.Params.NoBaseFee {
return big.NewInt(0), nil
}
blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, &height)
if err != nil {
return nil, err
}
baseFee := types.BaseFeeFromEvents(blockRes.EndBlockEvents)
if baseFee != nil {
return baseFee, nil
}
// If we cannot find in events, we tried to get it from the state.
// It will return feemarket.baseFee if london is activated but feemarket is not enable
res, err := e.queryClient.FeeMarket.BaseFee(types.ContextWithHeight(height), &feemarkettypes.QueryBaseFeeRequest{})
if err == nil && res.BaseFee != nil {
return res.BaseFee.BigInt(), nil
}
return nil, nil
}
// GetFilteredBlocks returns the block height list match the given bloom filters.
func (e *EVMBackend) GetFilteredBlocks(
from int64,
to int64,
filters [][]filters.BloomIV,
filterAddresses bool,
) ([]int64, error) {
matchedBlocks := make([]int64, 0)
BLOCKS:
for height := from; height <= to; height++ {
if err := e.ctx.Err(); err != nil {
e.logger.Error("EVMBackend context error", "err", err)
return nil, err
}
h := height
bloom, err := e.BlockBloom(&h)
if err != nil {
e.logger.Error("retrieve header failed", "blockHeight", height, "err", err)
return nil, err
}
for i, filter := range filters {
// filter the header bloom with the addresses
if filterAddresses && i == 0 {
if !checkMatches(bloom, filter) {
continue BLOCKS
}
// the filter doesn't have any topics
if len(filters) == 1 {
matchedBlocks = append(matchedBlocks, height)
continue BLOCKS
}
continue
}
// filter the bloom with topics
if len(filter) > 0 && !checkMatches(bloom, filter) {
continue BLOCKS
}
}
matchedBlocks = append(matchedBlocks, height)
}
return matchedBlocks, nil
}
// GetEthereumMsgsFromTendermintBlock returns all real MsgEthereumTxs from a Tendermint block.
// It also ensures consistency over the correct txs indexes across RPC endpoints
func (e *EVMBackend) GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock) []*evmtypes.MsgEthereumTx {
var result []*evmtypes.MsgEthereumTx
for _, tx := range block.Block.Txs {
tx, err := e.clientCtx.TxConfig.TxDecoder()(tx)
if err != nil {
e.logger.Debug("failed to decode transaction in block", "height", block.Block.Height, "error", err.Error())
continue
}
for _, msg := range tx.GetMsgs() {
ethMsg, ok := msg.(*evmtypes.MsgEthereumTx)
if !ok {
continue
}
hash := ethMsg.AsTransaction().Hash()
// check tx exists on EVM and has the correct block height
ethTx, err := e.GetTxByEthHash(hash)
if err != nil || ethTx.Height != block.Block.Height {
e.logger.Debug("failed to query eth tx hash", "hash", hash.Hex())
continue
}
result = append(result, ethMsg)
}
}
return result
}
// checkMatches revised the function from
// https://github.com/ethereum/go-ethereum/blob/401354976bb44f0ad4455ca1e0b5c0dc31d9a5f5/core/types/bloom9.go#L88
func checkMatches(bloom ethtypes.Bloom, filter []filters.BloomIV) bool {
for _, bloomIV := range filter {
if bloomIV.V[0] == bloomIV.V[0]&bloom[bloomIV.I[0]] &&
bloomIV.V[1] == bloomIV.V[1]&bloom[bloomIV.I[1]] &&
bloomIV.V[2] == bloomIV.V[2]&bloom[bloomIV.I[2]] {
return true
}
}
return false
}