diff --git a/rpc/apis.go b/rpc/apis.go index d50a56aa..eb9f2d53 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -19,7 +19,6 @@ import ( "github.com/evmos/ethermint/rpc/namespaces/ethereum/personal" "github.com/evmos/ethermint/rpc/namespaces/ethereum/txpool" "github.com/evmos/ethermint/rpc/namespaces/ethereum/web3" - "github.com/evmos/ethermint/rpc/types" rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" ) @@ -57,13 +56,12 @@ var apiCreators map[string]APICreator func init() { apiCreators = map[string]APICreator{ EthNamespace: func(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, allowUnprotectedTxs bool) []rpc.API { - nonceLock := new(types.AddrLocker) evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) return []rpc.API{ { Namespace: EthNamespace, Version: apiVersion, - Service: eth.NewPublicAPI(ctx.Logger, clientCtx, evmBackend, nonceLock), + Service: eth.NewPublicAPI(ctx.Logger, evmBackend), Public: true, }, { @@ -100,7 +98,7 @@ func init() { { Namespace: PersonalNamespace, Version: apiVersion, - Service: personal.NewAPI(ctx.Logger, clientCtx, evmBackend), + Service: personal.NewAPI(ctx.Logger, evmBackend), Public: false, }, } @@ -121,7 +119,7 @@ func init() { { Namespace: DebugNamespace, Version: apiVersion, - Service: debug.NewAPI(ctx, evmBackend, clientCtx), + Service: debug.NewAPI(ctx, evmBackend), Public: true, }, } @@ -132,7 +130,7 @@ func init() { { Namespace: MinerNamespace, Version: apiVersion, - Service: miner.NewPrivateAPI(ctx, clientCtx, evmBackend), + Service: miner.NewPrivateAPI(ctx, evmBackend), Public: false, }, } diff --git a/rpc/backend/account_info.go b/rpc/backend/account_info.go new file mode 100644 index 00000000..b9a8916a --- /dev/null +++ b/rpc/backend/account_info.go @@ -0,0 +1,207 @@ +package backend + +import ( + "fmt" + "math" + "math/big" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + rpctypes "github.com/evmos/ethermint/rpc/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/pkg/errors" +) + +// GetCode returns the contract code at the given address and block number. +func (b *Backend) GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { + blockNum, err := b.GetBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } + + req := &evmtypes.QueryCodeRequest{ + Address: address.String(), + } + + res, err := b.queryClient.Code(rpctypes.ContextWithHeight(blockNum.Int64()), req) + if err != nil { + return nil, err + } + + return res.Code, nil +} + +// GetProof returns an account object with proof and any storage proofs +func (b *Backend) GetProof(address common.Address, storageKeys []string, blockNrOrHash rpctypes.BlockNumberOrHash) (*rpctypes.AccountResult, error) { + blockNum, err := b.GetBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } + + height := blockNum.Int64() + _, err = b.GetTendermintBlockByNumber(blockNum) + if err != nil { + // Get 'latest' proof if query is in the future + // this imitates geth behavior + height = 0 + } + ctx := rpctypes.ContextWithHeight(height) + + // if the height is equal to zero, meaning the query condition of the block is either "pending" or "latest" + if height == 0 { + bn, err := b.BlockNumber() + if err != nil { + return nil, err + } + + if bn > math.MaxInt64 { + return nil, fmt.Errorf("not able to query block number greater than MaxInt64") + } + + height = int64(bn) + } + + clientCtx := b.clientCtx.WithHeight(height) + + // query storage proofs + storageProofs := make([]rpctypes.StorageResult, len(storageKeys)) + + for i, key := range storageKeys { + hexKey := common.HexToHash(key) + valueBz, proof, err := b.queryClient.GetProof(clientCtx, evmtypes.StoreKey, evmtypes.StateKey(address, hexKey.Bytes())) + if err != nil { + return nil, err + } + + // check for proof + var proofStr string + if proof != nil { + proofStr = proof.String() + } + + storageProofs[i] = rpctypes.StorageResult{ + Key: key, + Value: (*hexutil.Big)(new(big.Int).SetBytes(valueBz)), + Proof: []string{proofStr}, + } + } + + // query EVM account + req := &evmtypes.QueryAccountRequest{ + Address: address.String(), + } + + res, err := b.queryClient.Account(ctx, req) + if err != nil { + return nil, err + } + + // query account proofs + accountKey := authtypes.AddressStoreKey(sdk.AccAddress(address.Bytes())) + _, proof, err := b.queryClient.GetProof(clientCtx, authtypes.StoreKey, accountKey) + if err != nil { + return nil, err + } + + // check for proof + var accProofStr string + if proof != nil { + accProofStr = proof.String() + } + + balance, ok := sdkmath.NewIntFromString(res.Balance) + if !ok { + return nil, errors.New("invalid balance") + } + + return &rpctypes.AccountResult{ + Address: address, + AccountProof: []string{accProofStr}, + Balance: (*hexutil.Big)(balance.BigInt()), + CodeHash: common.HexToHash(res.CodeHash), + Nonce: hexutil.Uint64(res.Nonce), + StorageHash: common.Hash{}, // NOTE: Ethermint doesn't have a storage hash. TODO: implement? + StorageProof: storageProofs, + }, nil +} + +// GetStorageAt returns the contract storage at the given address, block number, and key. +func (b *Backend) GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { + blockNum, err := b.GetBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } + + req := &evmtypes.QueryStorageRequest{ + Address: address.String(), + Key: key, + } + + res, err := b.queryClient.Storage(rpctypes.ContextWithHeight(blockNum.Int64()), req) + if err != nil { + return nil, err + } + + value := common.HexToHash(res.Value) + return value.Bytes(), nil +} + +// GetBalance returns the provided account's balance up to the provided block number. +func (b *Backend) GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) { + blockNum, err := b.GetBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } + + req := &evmtypes.QueryBalanceRequest{ + Address: address.String(), + } + + _, err = b.GetTendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + + res, err := b.queryClient.Balance(rpctypes.ContextWithHeight(blockNum.Int64()), req) + if err != nil { + return nil, err + } + + val, ok := sdkmath.NewIntFromString(res.Balance) + if !ok { + return nil, errors.New("invalid balance") + } + + // balance can only be negative in case of pruned node + if val.IsNegative() { + return nil, errors.New("couldn't fetch balance. Node state is pruned") + } + + return (*hexutil.Big)(val.BigInt()), nil +} + +// GetTransactionCount returns the number of transactions at the given address up to the given block number. +func (b *Backend) GetTransactionCount(address common.Address, blockNum rpctypes.BlockNumber) (*hexutil.Uint64, error) { + // Get nonce (sequence) from account + from := sdk.AccAddress(address.Bytes()) + accRet := b.clientCtx.AccountRetriever + + err := accRet.EnsureExists(b.clientCtx, from) + if err != nil { + // account doesn't exist yet, return 0 + n := hexutil.Uint64(0) + return &n, nil + } + + includePending := blockNum == rpctypes.EthPendingBlockNumber + nonce, err := b.getAccountNonce(address, includePending, blockNum.Int64(), b.logger) + if err != nil { + return nil, err + } + + n := hexutil.Uint64(nonce) + return &n, nil +} diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index a3371f30..d3c68246 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -6,6 +6,8 @@ import ( "time" "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/server" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" @@ -13,10 +15,13 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" - "github.com/evmos/ethermint/rpc/types" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/evmos/ethermint/crypto/hd" + rpctypes "github.com/evmos/ethermint/rpc/types" "github.com/evmos/ethermint/server/config" ethermint "github.com/evmos/ethermint/types" evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/spf13/viper" "github.com/tendermint/tendermint/libs/log" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" ) @@ -40,58 +45,97 @@ type CosmosBackend interface { // TODO: define // as defined by EIP-1474: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md // Implemented by Backend. type EVMBackend interface { - // General Ethereum API + // Node specific queries + Accounts() ([]common.Address, error) + Syncing() (interface{}, error) + SetEtherbase(etherbase common.Address) bool + ImportRawKey(privkey, password string) (common.Address, error) + ListAccounts() ([]common.Address, error) + NewMnemonic(uid string, language keyring.Language, hdPath, bip39Passphrase string, algo keyring.SignatureAlgo) (*keyring.Record, error) + UnprotectedAllowed() bool 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. - UnprotectedAllowed() bool - RPCMinGasPrice() int64 - SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) - // Blockchain API + // Sign Tx + Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) + SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) + SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) + + // Blocks Info BlockNumber() (hexutil.Uint64, error) - GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) - GetTendermintBlockByNumber(blockNum types.BlockNumber) (*tmrpctypes.ResultBlock, error) + GetBlockByNumber(blockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) + GetTendermintBlockByNumber(blockNum rpctypes.BlockNumber) (*tmrpctypes.ResultBlock, error) GetTendermintBlockResultByNumber(height *int64) (*tmrpctypes.ResultBlockResults, error) GetTendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) - BlockByNumber(blockNum types.BlockNumber) (*ethtypes.Block, error) + BlockByNumber(blockNum rpctypes.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) GetBlockNumberByHash(blockHash common.Hash) (*big.Int, error) + GetBlockNumber(blockNrOrHash rpctypes.BlockNumberOrHash) (rpctypes.BlockNumber, error) + GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint + GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint + BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (ethtypes.Bloom, error) + GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evmtypes.MsgEthereumTx + HeaderByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Header, error) + HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) + EthBlockFromTendermint(resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults, fullTx bool) (map[string]interface{}, error) + EthBlockFromTm(resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) (*ethtypes.Block, error) + + // Account Info + GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) + GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) + GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) + GetProof(address common.Address, storageKeys []string, blockNrOrHash rpctypes.BlockNumberOrHash) (*rpctypes.AccountResult, error) + GetTransactionCount(address common.Address, blockNum rpctypes.BlockNumber) (*hexutil.Uint64, error) + + // Chain Info + ChainID() (*hexutil.Big, error) + ChainConfig() *params.ChainConfig + GlobalMinGasPrice() (sdk.Dec, error) + BaseFee(blockRes *tmrpctypes.ResultBlockResults) (*big.Int, error) + CurrentHeader() *ethtypes.Header PendingTransactions() ([]*sdk.Tx, 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) + FeeHistory(blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*rpctypes.FeeHistoryResult, error) + SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) + + // Tx Info + GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransaction, error) GetTxByEthHash(txHash common.Hash) (*tmrpctypes.ResultTx, error) GetTxByTxIndex(height int64, txIndex uint) (*tmrpctypes.ResultTx, error) - EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *types.BlockNumber) (hexutil.Uint64, error) - BaseFee(blockRes *tmrpctypes.ResultBlockResults) (*big.Int, error) - GlobalMinGasPrice() (sdk.Dec, error) + GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) + GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) + GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) + GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) - // Fee API - FeeHistory(blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*types.FeeHistoryResult, error) + // Send Transaction + Resend(args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) + SendRawTransaction(data hexutil.Bytes) (common.Hash, error) + SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) + EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) + DoCall(args evmtypes.TransactionArgs, blockNr rpctypes.BlockNumber) (*evmtypes.MsgEthereumTxResponse, error) // Filter API - BloomStatus() (uint64, uint64) GetLogs(hash common.Hash) ([][]*ethtypes.Log, error) GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error) - ChainConfig() *params.ChainConfig - SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) - GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evmtypes.MsgEthereumTx + BloomStatus() (uint64, uint64) + + // Tracing + TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) (interface{}, error) + TraceBlock(height rpctypes.BlockNumber, config *evmtypes.TraceConfig, block *tmrpctypes.ResultBlock) ([]*evmtypes.TxTraceResult, error) } var _ BackendI = (*Backend)(nil) +var bAttributeKeyEthereumBloom = []byte(evmtypes.AttributeKeyEthereumBloom) + // Backend implements the BackendI interface type Backend struct { ctx context.Context clientCtx client.Context - queryClient *types.QueryClient // gRPC query client + queryClient *rpctypes.QueryClient // gRPC query client logger log.Logger chainID *big.Int cfg config.Config @@ -107,10 +151,27 @@ func NewBackend(ctx *server.Context, logger log.Logger, clientCtx client.Context appConf := config.GetConfig(ctx.Viper) + algos, _ := clientCtx.Keyring.SupportedAlgorithms() + if !algos.Contains(hd.EthSecp256k1) { + kr, err := keyring.New( + sdk.KeyringServiceName(), + viper.GetString(flags.FlagKeyringBackend), + clientCtx.KeyringDir, + clientCtx.Input, + clientCtx.Codec, + hd.EthSecp256k1Option(), + ) + if err != nil { + panic(err) + } + + clientCtx = clientCtx.WithKeyring(kr) + } + return &Backend{ ctx: context.Background(), clientCtx: clientCtx, - queryClient: types.NewQueryClient(clientCtx), + queryClient: rpctypes.NewQueryClient(clientCtx), logger: logger.With("module", "backend"), chainID: chainID, cfg: appConf, diff --git a/rpc/backend/backend_suite_test.go b/rpc/backend/backend_suite_test.go index 88186dfe..fa7efb0d 100644 --- a/rpc/backend/backend_suite_test.go +++ b/rpc/backend/backend_suite_test.go @@ -1,10 +1,15 @@ package backend import ( + "bufio" + "fmt" "math/big" + "os" + "path/filepath" "testing" "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/server" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" @@ -13,10 +18,10 @@ import ( tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/evmos/ethermint/app" + "github.com/evmos/ethermint/crypto/hd" "github.com/evmos/ethermint/encoding" "github.com/evmos/ethermint/rpc/backend/mocks" - ethrpc "github.com/evmos/ethermint/rpc/types" - rpc "github.com/evmos/ethermint/rpc/types" + rpctypes "github.com/evmos/ethermint/rpc/types" evmtypes "github.com/evmos/ethermint/x/evm/types" ) @@ -34,17 +39,27 @@ func (suite *BackendTestSuite) SetupTest() { ctx := server.NewDefaultContext() ctx.Viper.Set("telemetry.global-labels", []interface{}{}) + baseDir := suite.T().TempDir() + nodeDirName := fmt.Sprintf("node") + clientDir := filepath.Join(baseDir, nodeDirName, "evmoscli") + keyRing, err := suite.generateTestKeyring(clientDir) + if err != nil { + panic(err) + } + encodingConfig := encoding.MakeConfig(app.ModuleBasics) clientCtx := client.Context{}.WithChainID("ethermint_9000-1"). WithHeight(1). - WithTxConfig(encodingConfig.TxConfig) + WithTxConfig(encodingConfig.TxConfig). + WithKeyringDir(clientDir). + WithKeyring(keyRing) allowUnprotectedTxs := false suite.backend = NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) suite.backend.queryClient.QueryClient = mocks.NewQueryClient(suite.T()) suite.backend.clientCtx.Client = mocks.NewClient(suite.T()) - suite.backend.ctx = rpc.ContextWithHeight(1) + suite.backend.ctx = rpctypes.ContextWithHeight(1) } // buildEthereumTx returns an example legacy Ethereum transaction @@ -94,7 +109,7 @@ func (suite *BackendTestSuite) buildFormattedBlock( ethRPCTxs := []interface{}{} if tx != nil { if fullTx { - rpcTx, err := ethrpc.NewRPCTransaction( + rpcTx, err := rpctypes.NewRPCTransaction( tx.AsTransaction(), common.BytesToHash(header.Hash()), uint64(header.Height), @@ -108,7 +123,7 @@ func (suite *BackendTestSuite) buildFormattedBlock( } } - return ethrpc.FormatBlock( + return rpctypes.FormatBlock( header, resBlock.Block.Size(), gasLimit, @@ -119,3 +134,9 @@ func (suite *BackendTestSuite) buildFormattedBlock( baseFee, ) } + +func (suite *BackendTestSuite) generateTestKeyring(clientDir string) (keyring.Keyring, error) { + buf := bufio.NewReader(os.Stdin) + encCfg := encoding.MakeConfig(app.ModuleBasics) + return keyring.New(sdk.KeyringServiceName(), keyring.BackendTest, clientDir, buf, encCfg.Codec, []keyring.Option{hd.EthSecp256k1Option()}...) +} diff --git a/rpc/backend/blocks_info.go b/rpc/backend/blocks_info.go new file mode 100644 index 00000000..ca97153b --- /dev/null +++ b/rpc/backend/blocks_info.go @@ -0,0 +1,512 @@ +package backend + +import ( + "bytes" + "fmt" + "math/big" + "strconv" + + 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" + rpctypes "github.com/evmos/ethermint/rpc/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/pkg/errors" + tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// Getting Blocks +// +// Retrieves information from a particular block in the blockchain. +// BlockNumber() (hexutil.Uint64, error) +// GetBlockByNumber(ethBlockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) +// GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) + +// 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 (b *Backend) BlockNumber() (hexutil.Uint64, error) { + // do any grpc query, ignore the response and use the returned block height + var header metadata.MD + _, err := b.queryClient.Params(b.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 (b *Backend) GetBlockByNumber(blockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) { + resBlock, err := b.GetTendermintBlockByNumber(blockNum) + if err != nil { + return nil, nil + } + + // return if requested block height is greater than the current one + if resBlock == nil || resBlock.Block == nil { + return nil, nil + } + + blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + b.logger.Debug("failed to fetch block result from Tendermint", "height", blockNum, "error", err.Error()) + return nil, nil + } + + res, err := b.EthBlockFromTendermint(resBlock, blockRes, fullTx) + if err != nil { + b.logger.Debug("EthBlockFromTendermint failed", "height", blockNum, "error", err.Error()) + return nil, err + } + + return res, nil +} + +// GetBlockByHash returns the block identified by hash. +func (b *Backend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + resBlock, err := b.GetTendermintBlockByHash(hash) + if err != nil { + return nil, err + } + + if resBlock == nil { + // block not found + return nil, nil + } + + blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + b.logger.Debug("failed to fetch block result from Tendermint", "block-hash", hash.String(), "error", err.Error()) + return nil, nil + } + + res, err := b.EthBlockFromTendermint(resBlock, blockRes, fullTx) + if err != nil { + b.logger.Debug("EthBlockFromTendermint failed", "hash", hash, "error", err.Error()) + return nil, err + } + + return res, nil +} + +// GetTendermintBlockByNumber returns a Tendermint formatted block for a given +// block number +func (b *Backend) GetTendermintBlockByNumber(blockNum rpctypes.BlockNumber) (*tmrpctypes.ResultBlock, error) { + height := blockNum.Int64() + if height <= 0 { + // fetch the latest block number from the app state, more accurate than the tendermint block store state. + n, err := b.BlockNumber() + if err != nil { + return nil, err + } + height = int64(n) + } + resBlock, err := b.clientCtx.Client.Block(b.ctx, &height) + if err != nil { + b.logger.Debug("tendermint client failed to get block", "height", height, "error", err.Error()) + return nil, err + } + + if resBlock.Block == nil { + b.logger.Debug("GetTendermintBlockByNumber block not found", "height", height) + return nil, nil + } + + return resBlock, nil +} + +// BlockBloom query block bloom filter from block results +func (b *Backend) BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (ethtypes.Bloom, error) { + for _, event := range blockRes.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") +} + +// GetTendermintBlockResultByNumber returns a Tendermint-formatted block result by block number +func (b *Backend) GetTendermintBlockResultByNumber(height *int64) (*tmrpctypes.ResultBlockResults, error) { + return b.clientCtx.Client.BlockResults(b.ctx, height) +} + +// GetTendermintBlockByHash returns a Tendermint format block by block number +func (b *Backend) GetTendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) { + resBlock, err := b.clientCtx.Client.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("GetTendermintBlockByHash block not found", "blockHash", blockHash.Hex()) + return nil, nil + } + + return resBlock, nil +} + +// BlockByNumber returns the block identified by number. +func (b *Backend) BlockByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Block, error) { + resBlock, err := b.GetTendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + if resBlock == nil { + // block not found + return nil, fmt.Errorf("block not found for height %d", blockNum) + } + + blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + return nil, fmt.Errorf("block result not found for height %d", resBlock.Block.Height) + } + + return b.EthBlockFromTm(resBlock, blockRes) +} + +// BlockByHash returns the block identified by hash. +func (b *Backend) BlockByHash(hash common.Hash) (*ethtypes.Block, error) { + resBlock, err := b.GetTendermintBlockByHash(hash) + if err != nil { + return nil, err + } + + if resBlock == nil || resBlock.Block == nil { + return nil, fmt.Errorf("block not found for hash %s", hash) + } + + blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + return nil, fmt.Errorf("block result not found for hash %s", hash) + } + + return b.EthBlockFromTm(resBlock, blockRes) +} + +// GetBlockNumberByHash returns the block height of given block hash +func (b *Backend) GetBlockNumberByHash(blockHash common.Hash) (*big.Int, error) { + resBlock, err := b.GetTendermintBlockByHash(blockHash) + if err != nil { + return nil, err + } + if resBlock == nil { + return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) + } + return big.NewInt(resBlock.Block.Height), nil +} + +// getBlockNumber returns the BlockNumber from BlockNumberOrHash +func (b *Backend) GetBlockNumber(blockNrOrHash rpctypes.BlockNumberOrHash) (rpctypes.BlockNumber, error) { + switch { + case blockNrOrHash.BlockHash == nil && blockNrOrHash.BlockNumber == nil: + return rpctypes.EthEarliestBlockNumber, fmt.Errorf("types BlockHash and BlockNumber cannot be both nil") + case blockNrOrHash.BlockHash != nil: + blockNumber, err := b.GetBlockNumberByHash(*blockNrOrHash.BlockHash) + if err != nil { + return rpctypes.EthEarliestBlockNumber, err + } + return rpctypes.NewBlockNumber(blockNumber), nil + case blockNrOrHash.BlockNumber != nil: + return *blockNrOrHash.BlockNumber, nil + default: + return rpctypes.EthEarliestBlockNumber, nil + } +} + +// GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. +func (b *Backend) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { + block, err := b.clientCtx.Client.BlockByHash(b.ctx, hash.Bytes()) + if err != nil { + b.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error()) + return nil + } + + if block.Block == nil { + b.logger.Debug("block not found", "hash", hash.Hex()) + return nil + } + + blockRes, err := b.GetTendermintBlockResultByNumber(&block.Block.Height) + if err != nil { + return nil + } + + ethMsgs := b.GetEthereumMsgsFromTendermintBlock(block, blockRes) + n := hexutil.Uint(len(ethMsgs)) + return &n +} + +// GetBlockTransactionCountByNumber returns the number of transactions in the block identified by number. +func (b *Backend) GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint { + block, err := b.GetTendermintBlockByNumber(blockNum) + if err != nil { + b.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) + return nil + } + + if block.Block == nil { + b.logger.Debug("block not found", "height", blockNum.Int64()) + return nil + } + + blockRes, err := b.GetTendermintBlockResultByNumber(&block.Block.Height) + if err != nil { + return nil + } + + ethMsgs := b.GetEthereumMsgsFromTendermintBlock(block, blockRes) + n := hexutil.Uint(len(ethMsgs)) + return &n +} + +// GetEthereumMsgsFromTendermintBlock returns all real MsgEthereumTxs from a +// Tendermint block. It also ensures consistency over the correct txs indexes +// across RPC endpoints +func (b *Backend) GetEthereumMsgsFromTendermintBlock( + resBlock *tmrpctypes.ResultBlock, + blockRes *tmrpctypes.ResultBlockResults, +) []*evmtypes.MsgEthereumTx { + var result []*evmtypes.MsgEthereumTx + block := resBlock.Block + + txResults := blockRes.TxsResults + + for i, tx := range block.Txs { + // Check if tx exists on EVM by cross checking with blockResults: + // - Include unsuccessful tx that exceeds block gas limit + // - Exclude unsuccessful tx with any other error but ExceedBlockGasLimit + if !TxSuccessOrExceedsBlockGasLimit(txResults[i]) { + b.logger.Debug("invalid tx result code", "cosmos-hash", hexutil.Encode(tx.Hash())) + continue + } + + tx, err := b.clientCtx.TxConfig.TxDecoder()(tx) + if err != nil { + b.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 + } + + ethMsg.Hash = ethMsg.AsTransaction().Hash().Hex() + result = append(result, ethMsg) + } + } + + return result +} + +// HeaderByNumber returns the block header identified by height. +func (b *Backend) HeaderByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Header, error) { + resBlock, err := b.GetTendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + + if resBlock == nil { + return nil, errors.Errorf("block not found for height %d", blockNum) + } + + blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + return nil, fmt.Errorf("block result not found for height %d", resBlock.Block.Height) + } + + bloom, err := b.BlockBloom(blockRes) + if err != nil { + b.logger.Debug("HeaderByNumber BlockBloom failed", "height", resBlock.Block.Height) + } + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", resBlock.Block.Height, "error", err) + } + + ethHeader := rpctypes.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) + return ethHeader, nil +} + +// HeaderByHash returns the block header identified by hash. +func (b *Backend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) { + resBlock, err := b.GetTendermintBlockByHash(blockHash) + if err != nil { + return nil, err + } + if resBlock == nil { + return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) + } + + blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) + if err != nil { + return nil, errors.Errorf("block result not found for height %d", resBlock.Block.Height) + } + + bloom, err := b.BlockBloom(blockRes) + if err != nil { + b.logger.Debug("HeaderByHash BlockBloom failed", "height", resBlock.Block.Height) + } + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", resBlock.Block.Height, "error", err) + } + + ethHeader := rpctypes.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) + return ethHeader, nil +} + +// EthBlockFromTendermint returns a JSON-RPC compatible Ethereum block from a +// given Tendermint block and its block result. +func (b *Backend) EthBlockFromTendermint( + resBlock *tmrpctypes.ResultBlock, + blockRes *tmrpctypes.ResultBlockResults, + fullTx bool, +) (map[string]interface{}, error) { + ethRPCTxs := []interface{}{} + block := resBlock.Block + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", block.Height, "error", err) + } + + msgs := b.GetEthereumMsgsFromTendermintBlock(resBlock, blockRes) + for txIndex, ethMsg := range msgs { + if !fullTx { + hash := common.HexToHash(ethMsg.Hash) + ethRPCTxs = append(ethRPCTxs, hash) + continue + } + + tx := ethMsg.AsTransaction() + rpcTx, err := rpctypes.NewRPCTransaction( + tx, + common.BytesToHash(block.Hash()), + uint64(block.Height), + uint64(txIndex), + baseFee, + ) + if err != nil { + b.logger.Debug("NewTransactionFromData for receipt failed", "hash", tx.Hash().Hex(), "error", err.Error()) + continue + } + ethRPCTxs = append(ethRPCTxs, rpcTx) + } + + bloom, err := b.BlockBloom(blockRes) + if err != nil { + b.logger.Debug("failed to query BlockBloom", "height", block.Height, "error", err.Error()) + } + + req := &evmtypes.QueryValidatorAccountRequest{ + ConsAddress: sdk.ConsAddress(block.Header.ProposerAddress).String(), + } + + var validatorAccAddr sdk.AccAddress + + ctx := rpctypes.ContextWithHeight(block.Height) + res, err := b.queryClient.ValidatorAccount(ctx, req) + if err != nil { + b.logger.Debug( + "failed to query validator operator address", + "height", block.Height, + "cons-address", req.ConsAddress, + "error", err.Error(), + ) + // use zero address as the validator operator address + validatorAccAddr = sdk.AccAddress(common.Address{}.Bytes()) + } else { + validatorAccAddr, err = sdk.AccAddressFromBech32(res.AccountAddress) + if err != nil { + return nil, err + } + } + + validatorAddr := common.BytesToAddress(validatorAccAddr) + + gasLimit, err := rpctypes.BlockMaxGasFromConsensusParams(ctx, b.clientCtx, block.Height) + if err != nil { + b.logger.Error("failed to query consensus params", "error", err.Error()) + } + + gasUsed := uint64(0) + + for _, txsResult := range blockRes.TxsResults { + // workaround for cosmos-sdk bug. https://github.com/cosmos/cosmos-sdk/issues/10832 + if ShouldIgnoreGasUsed(txsResult) { + // block gas limit has exceeded, other txs must have failed with same reason. + break + } + gasUsed += uint64(txsResult.GetGasUsed()) + } + + formattedBlock := rpctypes.FormatBlock( + block.Header, block.Size(), + gasLimit, new(big.Int).SetUint64(gasUsed), + ethRPCTxs, bloom, validatorAddr, baseFee, + ) + return formattedBlock, nil +} + +// Returns and Ethereum Block type from Tendermint block +func (b *Backend) EthBlockFromTm(resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) (*ethtypes.Block, error) { + block := resBlock.Block + height := block.Height + bloom, err := b.BlockBloom(blockRes) + if err != nil { + b.logger.Debug("HeaderByNumber BlockBloom failed", "height", height) + } + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle error for pruned node and log + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", height, "error", err) + } + + ethHeader := rpctypes.EthHeaderFromTendermint(block.Header, bloom, baseFee) + + resBlockResult, err := b.GetTendermintBlockResultByNumber(&block.Height) + if err != nil { + return nil, err + } + + msgs := b.GetEthereumMsgsFromTendermintBlock(resBlock, resBlockResult) + + txs := make([]*ethtypes.Transaction, len(msgs)) + for i, ethMsg := range msgs { + txs[i] = ethMsg.AsTransaction() + } + + // TODO: add tx receipts + ethBlock := ethtypes.NewBlock(ethHeader, txs, nil, nil, nil) + return ethBlock, nil +} diff --git a/rpc/backend/call_tx.go b/rpc/backend/call_tx.go new file mode 100644 index 00000000..b572f5e5 --- /dev/null +++ b/rpc/backend/call_tx.go @@ -0,0 +1,364 @@ +package backend + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "math/big" + + "github.com/cosmos/cosmos-sdk/client/flags" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + rpctypes "github.com/evmos/ethermint/rpc/types" + ethermint "github.com/evmos/ethermint/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Resend accepts an existing transaction and a new gas price and limit. It will remove +// the given transaction from the pool and reinsert it with the new gas price and limit. +func (b *Backend) Resend(args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) { + if args.Nonce == nil { + return common.Hash{}, fmt.Errorf("missing transaction nonce in transaction spec") + } + + args, err := b.SetTxDefaults(args) + if err != nil { + return common.Hash{}, err + } + + // The signer used should always be the 'latest' known one because we expect + // signers to be backwards-compatible with old transactions. + eip155ChainID, err := ethermint.ParseChainID(b.clientCtx.ChainID) + if err != nil { + return common.Hash{}, err + } + + cfg := b.ChainConfig() + if cfg == nil { + cfg = evmtypes.DefaultChainConfig().EthereumConfig(eip155ChainID) + } + + signer := ethtypes.LatestSigner(cfg) + + matchTx := args.ToTransaction().AsTransaction() + + // Before replacing the old transaction, ensure the _new_ transaction fee is reasonable. + price := matchTx.GasPrice() + if gasPrice != nil { + price = gasPrice.ToInt() + } + gas := matchTx.Gas() + if gasLimit != nil { + gas = uint64(*gasLimit) + } + if err := rpctypes.CheckTxFee(price, gas, b.RPCTxFeeCap()); err != nil { + return common.Hash{}, err + } + + pending, err := b.PendingTransactions() + if err != nil { + return common.Hash{}, err + } + + for _, tx := range pending { + // FIXME does Resend api possible at all? https://github.com/evmos/ethermint/issues/905 + p, err := evmtypes.UnwrapEthereumMsg(tx, common.Hash{}) + if err != nil { + // not valid ethereum tx + continue + } + + pTx := p.AsTransaction() + + wantSigHash := signer.Hash(matchTx) + pFrom, err := ethtypes.Sender(signer, pTx) + if err != nil { + continue + } + + if pFrom == *args.From && signer.Hash(pTx) == wantSigHash { + // Match. Re-sign and send the transaction. + if gasPrice != nil && (*big.Int)(gasPrice).Sign() != 0 { + args.GasPrice = gasPrice + } + if gasLimit != nil && *gasLimit != 0 { + args.Gas = gasLimit + } + + return b.SendTransaction(args) // TODO: this calls SetTxDefaults again, refactor to avoid calling it twice + } + } + + return common.Hash{}, fmt.Errorf("transaction %#x not found", matchTx.Hash()) +} + +// SendRawTransaction send a raw Ethereum transaction. +func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { + // RLP decode raw transaction bytes + tx := ðtypes.Transaction{} + if err := tx.UnmarshalBinary(data); err != nil { + b.logger.Error("transaction decoding failed", "error", err.Error()) + return common.Hash{}, err + } + + // check the local node config in case unprotected txs are disabled + if !b.UnprotectedAllowed() && !tx.Protected() { + // Ensure only eip155 signed transactions are submitted if EIP155Required is set. + return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") + } + + ethereumTx := &evmtypes.MsgEthereumTx{} + if err := ethereumTx.FromEthereumTx(tx); err != nil { + b.logger.Error("transaction converting failed", "error", err.Error()) + return common.Hash{}, err + } + + if err := ethereumTx.ValidateBasic(); err != nil { + b.logger.Debug("tx failed basic validation", "error", err.Error()) + return common.Hash{}, err + } + + // Query params to use the EVM denomination + res, err := b.queryClient.QueryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) + if err != nil { + b.logger.Error("failed to query evm params", "error", err.Error()) + return common.Hash{}, err + } + + cosmosTx, err := ethereumTx.BuildTx(b.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom) + if err != nil { + b.logger.Error("failed to build cosmos tx", "error", err.Error()) + return common.Hash{}, err + } + + // Encode transaction by default Tx encoder + txBytes, err := b.clientCtx.TxConfig.TxEncoder()(cosmosTx) + if err != nil { + b.logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) + return common.Hash{}, err + } + + txHash := ethereumTx.AsTransaction().Hash() + + syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) + rsp, err := syncCtx.BroadcastTx(txBytes) + if rsp != nil && rsp.Code != 0 { + err = sdkerrors.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) + } + if err != nil { + b.logger.Error("failed to broadcast tx", "error", err.Error()) + return txHash, err + } + + return txHash, nil +} + +// SetTxDefaults populates tx message with default values in case they are not +// provided on the args +func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) { + if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { + return args, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") + } + + head := b.CurrentHeader() + if head == nil { + return args, errors.New("latest header is nil") + } + + // If user specifies both maxPriorityfee and maxFee, then we do not + // need to consult the chain for defaults. It's definitely a London tx. + if args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil { + // In this clause, user left some fields unspecified. + if head.BaseFee != nil && args.GasPrice == nil { + if args.MaxPriorityFeePerGas == nil { + tip, err := b.SuggestGasTipCap(head.BaseFee) + if err != nil { + return args, err + } + args.MaxPriorityFeePerGas = (*hexutil.Big)(tip) + } + + if args.MaxFeePerGas == nil { + gasFeeCap := new(big.Int).Add( + (*big.Int)(args.MaxPriorityFeePerGas), + new(big.Int).Mul(head.BaseFee, big.NewInt(2)), + ) + args.MaxFeePerGas = (*hexutil.Big)(gasFeeCap) + } + + if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 { + return args, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas) + } + + } else { + if args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil { + return args, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but london is not active yet") + } + + if args.GasPrice == nil { + price, err := b.SuggestGasTipCap(head.BaseFee) + if err != nil { + return args, err + } + if head.BaseFee != nil { + // The legacy tx gas price suggestion should not add 2x base fee + // because all fees are consumed, so it would result in a spiral + // upwards. + price.Add(price, head.BaseFee) + } + args.GasPrice = (*hexutil.Big)(price) + } + } + } else { + // Both maxPriorityfee and maxFee set by caller. Sanity-check their internal relation + if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 { + return args, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas) + } + } + + if args.Value == nil { + args.Value = new(hexutil.Big) + } + if args.Nonce == nil { + // get the nonce from the account retriever + // ignore error in case tge account doesn't exist yet + nonce, _ := b.getAccountNonce(*args.From, true, 0, b.logger) + args.Nonce = (*hexutil.Uint64)(&nonce) + } + + if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) { + return args, errors.New("both 'data' and 'input' are set and not equal. Please use 'input' to pass transaction call data") + } + + if args.To == nil { + // Contract creation + var input []byte + if args.Data != nil { + input = *args.Data + } else if args.Input != nil { + input = *args.Input + } + + if len(input) == 0 { + return args, errors.New("contract creation without any data provided") + } + } + + if args.Gas == nil { + // For backwards-compatibility reason, we try both input and data + // but input is preferred. + input := args.Input + if input == nil { + input = args.Data + } + + callArgs := evmtypes.TransactionArgs{ + From: args.From, + To: args.To, + Gas: args.Gas, + GasPrice: args.GasPrice, + MaxFeePerGas: args.MaxFeePerGas, + MaxPriorityFeePerGas: args.MaxPriorityFeePerGas, + Value: args.Value, + Data: input, + AccessList: args.AccessList, + } + + blockNr := rpctypes.NewBlockNumber(big.NewInt(0)) + estimated, err := b.EstimateGas(callArgs, &blockNr) + if err != nil { + return args, err + } + args.Gas = &estimated + b.logger.Debug("estimate gas usage automatically", "gas", args.Gas) + } + + if args.ChainID == nil { + args.ChainID = (*hexutil.Big)(b.chainID) + } + + return args, nil +} + +// EstimateGas returns an estimate of gas usage for the given smart contract call. +func (b *Backend) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) { + blockNr := rpctypes.EthPendingBlockNumber + if blockNrOptional != nil { + blockNr = *blockNrOptional + } + + bz, err := json.Marshal(&args) + if err != nil { + return 0, err + } + + req := evmtypes.EthCallRequest{ + Args: bz, + GasCap: b.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 := b.queryClient.EstimateGas(rpctypes.ContextWithHeight(blockNr.Int64()), &req) + if err != nil { + return 0, err + } + return hexutil.Uint64(res.Gas), nil +} + +// DoCall performs a simulated call operation through the evmtypes. It returns the +// estimated gas used on the operation or an error if fails. +func (b *Backend) DoCall( + args evmtypes.TransactionArgs, blockNr rpctypes.BlockNumber, +) (*evmtypes.MsgEthereumTxResponse, error) { + bz, err := json.Marshal(&args) + if err != nil { + return nil, err + } + + req := evmtypes.EthCallRequest{ + Args: bz, + GasCap: b.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. + ctx := rpctypes.ContextWithHeight(blockNr.Int64()) + timeout := b.RPCEVMTimeout() + + // Setup context so it may be canceled the call has completed + // or, in case of unmetered gas, setup a context with a timeout. + var cancel context.CancelFunc + if timeout > 0 { + ctx, cancel = context.WithTimeout(ctx, timeout) + } else { + ctx, cancel = context.WithCancel(ctx) + } + + // Make sure the context is canceled when the call has completed + // this makes sure resources are cleaned up. + defer cancel() + + res, err := b.queryClient.EthCall(ctx, &req) + if err != nil { + return nil, err + } + + if res.Failed() { + if res.VmError != vm.ErrExecutionReverted.Error() { + return nil, status.Error(codes.Internal, res.VmError) + } + return nil, evmtypes.NewExecErrorWithReason(res.Ret) + } + + return res, nil +} diff --git a/rpc/backend/chain_info.go b/rpc/backend/chain_info.go new file mode 100644 index 00000000..8672b2c6 --- /dev/null +++ b/rpc/backend/chain_info.go @@ -0,0 +1,265 @@ +package backend + +import ( + "fmt" + "math/big" + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + rpctypes "github.com/evmos/ethermint/rpc/types" + ethermint "github.com/evmos/ethermint/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" + tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +// ChainID is the EIP-155 replay-protection chain id for the current ethereum chain config. +func (b *Backend) ChainID() (*hexutil.Big, error) { + eip155ChainID, err := ethermint.ParseChainID(b.clientCtx.ChainID) + if err != nil { + panic(err) + } + // if current block is at or past the EIP-155 replay-protection fork block, return chainID from config + bn, err := b.BlockNumber() + if err != nil { + b.logger.Debug("failed to fetch latest block number", "error", err.Error()) + return (*hexutil.Big)(eip155ChainID), nil + } + + if config := b.ChainConfig(); config.IsEIP155(new(big.Int).SetUint64(uint64(bn))) { + return (*hexutil.Big)(config.ChainID), nil + } + + return nil, fmt.Errorf("chain not synced beyond EIP-155 replay-protection fork block") +} + +// ChainConfig returns the latest ethereum chain configuration +func (b *Backend) ChainConfig() *params.ChainConfig { + params, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) + if err != nil { + return nil + } + + return params.Params.ChainConfig.EthereumConfig(b.chainID) +} + +// GlobalMinGasPrice returns MinGasPrice param from FeeMarket +func (b *Backend) GlobalMinGasPrice() (sdk.Dec, error) { + res, err := b.queryClient.FeeMarket.Params(b.ctx, &feemarkettypes.QueryParamsRequest{}) + if err != nil { + return sdk.ZeroDec(), err + } + return res.Params.MinGasPrice, nil +} + +// BaseFee returns the base fee tracked by the Fee Market module. +// If the base fee is not enabled globally, the query returns nil. +// If the London hard fork is not activated at the current height, the query will +// return nil. +func (b *Backend) BaseFee(blockRes *tmrpctypes.ResultBlockResults) (*big.Int, error) { + // return BaseFee if London hard fork is activated and feemarket is enabled + res, err := b.queryClient.BaseFee(rpctypes.ContextWithHeight(blockRes.Height), &evmtypes.QueryBaseFeeRequest{}) + if err != nil || res.BaseFee == nil { + // we can't tell if it's london HF not enabled or the state is pruned, + // in either case, we'll fallback to parsing from begin blocker event, + // faster to iterate reversely + for i := len(blockRes.BeginBlockEvents) - 1; i >= 0; i-- { + evt := blockRes.BeginBlockEvents[i] + if evt.Type == feemarkettypes.EventTypeFeeMarket && len(evt.Attributes) > 0 { + baseFee, err := strconv.ParseInt(string(evt.Attributes[0].Value), 10, 64) + if err == nil { + return big.NewInt(baseFee), nil + } + break + } + } + return nil, err + } + + if res.BaseFee == nil { + return nil, nil + } + + return res.BaseFee.BigInt(), nil +} + +// CurrentHeader returns the latest block header +func (b *Backend) CurrentHeader() *ethtypes.Header { + header, _ := b.HeaderByNumber(rpctypes.EthLatestBlockNumber) + return header +} + +// 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 (b *Backend) PendingTransactions() ([]*sdk.Tx, error) { + res, err := b.clientCtx.Client.UnconfirmedTxs(b.ctx, nil) + if err != nil { + return nil, err + } + + result := make([]*sdk.Tx, 0, len(res.Txs)) + for _, txBz := range res.Txs { + tx, err := b.clientCtx.TxConfig.TxDecoder()(txBz) + if err != nil { + return nil, err + } + result = append(result, &tx) + } + + return result, nil +} + +// GetCoinbase is the address that staking rewards will be send to (alias for Etherbase). +func (b *Backend) GetCoinbase() (sdk.AccAddress, error) { + node, err := b.clientCtx.GetNode() + if err != nil { + return nil, err + } + + status, err := node.Status(b.ctx) + if err != nil { + return nil, err + } + + req := &evmtypes.QueryValidatorAccountRequest{ + ConsAddress: sdk.ConsAddress(status.ValidatorInfo.Address).String(), + } + + res, err := b.queryClient.ValidatorAccount(b.ctx, req) + if err != nil { + return nil, err + } + + address, _ := sdk.AccAddressFromBech32(res.AccountAddress) + return address, nil +} + +// FeeHistory returns data relevant for fee estimation based on the specified range of blocks. +func (b *Backend) FeeHistory( + userBlockCount rpc.DecimalOrHex, // number blocks to fetch, maximum is 100 + lastBlock rpc.BlockNumber, // the block to start search , to oldest + rewardPercentiles []float64, // percentiles to fetch reward +) (*rpctypes.FeeHistoryResult, error) { + blockEnd := int64(lastBlock) + + if blockEnd <= 0 { + blockNumber, err := b.BlockNumber() + if err != nil { + return nil, err + } + blockEnd = int64(blockNumber) + } + userBlockCountInt := int64(userBlockCount) + maxBlockCount := int64(b.cfg.JSONRPC.FeeHistoryCap) + if userBlockCountInt > maxBlockCount { + return nil, fmt.Errorf("FeeHistory user block count %d higher than %d", userBlockCountInt, maxBlockCount) + } + blockStart := blockEnd - userBlockCountInt + if blockStart < 0 { + blockStart = 0 + } + + blockCount := blockEnd - blockStart + + oldestBlock := (*hexutil.Big)(big.NewInt(blockStart)) + + // prepare space + reward := make([][]*hexutil.Big, blockCount) + rewardCount := len(rewardPercentiles) + for i := 0; i < int(blockCount); i++ { + reward[i] = make([]*hexutil.Big, rewardCount) + } + thisBaseFee := make([]*hexutil.Big, blockCount) + thisGasUsedRatio := make([]float64, blockCount) + + // rewards should only be calculated if reward percentiles were included + calculateRewards := rewardCount != 0 + + // fetch block + for blockID := blockStart; blockID < blockEnd; blockID++ { + index := int32(blockID - blockStart) + // tendermint block + tendermintblock, err := b.GetTendermintBlockByNumber(rpctypes.BlockNumber(blockID)) + if tendermintblock == nil { + return nil, err + } + + // eth block + ethBlock, err := b.GetBlockByNumber(rpctypes.BlockNumber(blockID), true) + if ethBlock == nil { + return nil, err + } + + // tendermint block result + tendermintBlockResult, err := b.GetTendermintBlockResultByNumber(&tendermintblock.Block.Height) + if tendermintBlockResult == nil { + b.logger.Debug("block result not found", "height", tendermintblock.Block.Height, "error", err.Error()) + return nil, err + } + + oneFeeHistory := rpctypes.OneFeeHistory{} + err = b.processBlock(tendermintblock, ðBlock, rewardPercentiles, tendermintBlockResult, &oneFeeHistory) + if err != nil { + return nil, err + } + + // copy + thisBaseFee[index] = (*hexutil.Big)(oneFeeHistory.BaseFee) + thisGasUsedRatio[index] = oneFeeHistory.GasUsedRatio + if calculateRewards { + for j := 0; j < rewardCount; j++ { + reward[index][j] = (*hexutil.Big)(oneFeeHistory.Reward[j]) + if reward[index][j] == nil { + reward[index][j] = (*hexutil.Big)(big.NewInt(0)) + } + } + } + } + + feeHistory := rpctypes.FeeHistoryResult{ + OldestBlock: oldestBlock, + BaseFee: thisBaseFee, + GasUsedRatio: thisGasUsedRatio, + } + + if calculateRewards { + feeHistory.Reward = reward + } + + return &feeHistory, nil +} + +// SuggestGasTipCap returns the suggested tip cap +// Although we don't support tx prioritization yet, but we return a positive value to help client to +// mitigate the base fee changes. +func (b *Backend) SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) { + if baseFee == nil { + // london hardfork not enabled or feemarket not enabled + return big.NewInt(0), nil + } + + params, err := b.queryClient.FeeMarket.Params(b.ctx, &feemarkettypes.QueryParamsRequest{}) + if err != nil { + return nil, err + } + // calculate the maximum base fee delta in current block, assuming all block gas limit is consumed + // ``` + // GasTarget = GasLimit / ElasticityMultiplier + // Delta = BaseFee * (GasUsed - GasTarget) / GasTarget / Denominator + // ``` + // The delta is at maximum when `GasUsed` is equal to `GasLimit`, which is: + // ``` + // MaxDelta = BaseFee * (GasLimit - GasLimit / ElasticityMultiplier) / (GasLimit / ElasticityMultiplier) / Denominator + // = BaseFee * (ElasticityMultiplier - 1) / Denominator + // ``` + maxDelta := baseFee.Int64() * (int64(params.Params.ElasticityMultiplier) - 1) / int64(params.Params.BaseFeeChangeDenominator) + if maxDelta < 0 { + // impossible if the parameter validation passed. + maxDelta = 0 + } + return big.NewInt(maxDelta), nil +} diff --git a/rpc/backend/evm_backend.go b/rpc/backend/evm_backend.go deleted file mode 100644 index 6b678f5c..00000000 --- a/rpc/backend/evm_backend.go +++ /dev/null @@ -1,1038 +0,0 @@ -package backend - -import ( - "bytes" - "encoding/json" - "fmt" - "math/big" - "strconv" - "time" - - "github.com/pkg/errors" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rpc" - - tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" - - "github.com/cosmos/cosmos-sdk/client/flags" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" - - "github.com/evmos/ethermint/rpc/types" - ethermint "github.com/evmos/ethermint/types" - evmtypes "github.com/evmos/ethermint/x/evm/types" - feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" -) - -var bAttributeKeyEthereumBloom = []byte(evmtypes.AttributeKeyEthereumBloom) - -// 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 (b *Backend) BlockNumber() (hexutil.Uint64, error) { - // do any grpc query, ignore the response and use the returned block height - var header metadata.MD - _, err := b.queryClient.Params(b.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 (b *Backend) GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) { - resBlock, err := b.GetTendermintBlockByNumber(blockNum) - if err != nil { - return nil, nil - } - - // return if requested block height is greater than the current one - if resBlock == nil || resBlock.Block == nil { - return nil, nil - } - - blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) - if err != nil { - b.logger.Debug("failed to fetch block result from Tendermint", "height", blockNum, "error", err.Error()) - return nil, nil - } - - res, err := b.EthBlockFromTendermint(resBlock, blockRes, fullTx) - if err != nil { - b.logger.Debug("EthBlockFromTendermint failed", "height", blockNum, "error", err.Error()) - return nil, err - } - - return res, nil -} - -// GetBlockByHash returns the block identified by hash. -func (b *Backend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { - resBlock, err := b.GetTendermintBlockByHash(hash) - if err != nil { - return nil, err - } - - if resBlock == nil { - // block not found - return nil, nil - } - - blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) - if err != nil { - b.logger.Debug("failed to fetch block result from Tendermint", "block-hash", hash.String(), "error", err.Error()) - return nil, nil - } - - res, err := b.EthBlockFromTendermint(resBlock, blockRes, fullTx) - if err != nil { - b.logger.Debug("EthBlockFromTendermint failed", "hash", hash, "error", err.Error()) - return nil, err - } - - return res, nil -} - -// BlockByNumber returns the block identified by number. -func (b *Backend) BlockByNumber(blockNum types.BlockNumber) (*ethtypes.Block, error) { - resBlock, err := b.GetTendermintBlockByNumber(blockNum) - if err != nil { - return nil, err - } - if resBlock == nil { - // block not found - return nil, fmt.Errorf("block not found for height %d", blockNum) - } - - blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) - if err != nil { - return nil, fmt.Errorf("block result not found for height %d", resBlock.Block.Height) - } - - return b.EthBlockFromTm(resBlock, blockRes) -} - -// BlockByHash returns the block identified by hash. -func (b *Backend) BlockByHash(hash common.Hash) (*ethtypes.Block, error) { - resBlock, err := b.GetTendermintBlockByHash(hash) - if err != nil { - return nil, err - } - - if resBlock == nil || resBlock.Block == nil { - return nil, fmt.Errorf("block not found for hash %s", hash) - } - - blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) - if err != nil { - return nil, fmt.Errorf("block result not found for hash %s", hash) - } - - return b.EthBlockFromTm(resBlock, blockRes) -} - -func (b *Backend) EthBlockFromTm(resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) (*ethtypes.Block, error) { - block := resBlock.Block - height := block.Height - bloom, err := b.BlockBloom(blockRes) - if err != nil { - b.logger.Debug("HeaderByNumber BlockBloom failed", "height", height) - } - - baseFee, err := b.BaseFee(blockRes) - if err != nil { - // handle error for pruned node and log - b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", height, "error", err) - } - - ethHeader := types.EthHeaderFromTendermint(block.Header, bloom, baseFee) - - resBlockResult, err := b.GetTendermintBlockResultByNumber(&block.Height) - if err != nil { - return nil, err - } - - msgs := b.GetEthereumMsgsFromTendermintBlock(resBlock, resBlockResult) - - txs := make([]*ethtypes.Transaction, len(msgs)) - for i, ethMsg := range msgs { - txs[i] = ethMsg.AsTransaction() - } - - // TODO: add tx receipts - ethBlock := ethtypes.NewBlock(ethHeader, txs, nil, nil, nil) - return ethBlock, nil -} - -// GetTendermintBlockByNumber returns a Tendermint formatted block for a given -// block number -func (b *Backend) GetTendermintBlockByNumber(blockNum types.BlockNumber) (*tmrpctypes.ResultBlock, error) { - height := blockNum.Int64() - if height <= 0 { - // fetch the latest block number from the app state, more accurate than the tendermint block store state. - n, err := b.BlockNumber() - if err != nil { - return nil, err - } - height = int64(n) - } - resBlock, err := b.clientCtx.Client.Block(b.ctx, &height) - if err != nil { - b.logger.Debug("tendermint client failed to get block", "height", height, "error", err.Error()) - return nil, err - } - - if resBlock.Block == nil { - b.logger.Debug("GetTendermintBlockByNumber block not found", "height", height) - return nil, nil - } - - return resBlock, nil -} - -// GetTendermintBlockResultByNumber returns a Tendermint-formatted block result by block number -func (b *Backend) GetTendermintBlockResultByNumber(height *int64) (*tmrpctypes.ResultBlockResults, error) { - return b.clientCtx.Client.BlockResults(b.ctx, height) -} - -// GetTendermintBlockByHash returns a Tendermint format block by block number -func (b *Backend) GetTendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) { - resBlock, err := b.clientCtx.Client.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("GetTendermintBlockByHash block not found", "blockHash", blockHash.Hex()) - return nil, nil - } - - return resBlock, nil -} - -// BlockBloom query block bloom filter from block results -func (b *Backend) BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (ethtypes.Bloom, error) { - for _, event := range blockRes.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 (b *Backend) EthBlockFromTendermint( - resBlock *tmrpctypes.ResultBlock, - blockRes *tmrpctypes.ResultBlockResults, - fullTx bool, -) (map[string]interface{}, error) { - ethRPCTxs := []interface{}{} - block := resBlock.Block - - baseFee, err := b.BaseFee(blockRes) - if err != nil { - // handle the error for pruned node. - b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", block.Height, "error", err) - } - - msgs := b.GetEthereumMsgsFromTendermintBlock(resBlock, blockRes) - for txIndex, ethMsg := range msgs { - if !fullTx { - hash := common.HexToHash(ethMsg.Hash) - ethRPCTxs = append(ethRPCTxs, hash) - continue - } - - tx := ethMsg.AsTransaction() - rpcTx, err := types.NewRPCTransaction( - tx, - common.BytesToHash(block.Hash()), - uint64(block.Height), - uint64(txIndex), - baseFee, - ) - if err != nil { - b.logger.Debug("NewTransactionFromData for receipt failed", "hash", tx.Hash().Hex(), "error", err.Error()) - continue - } - ethRPCTxs = append(ethRPCTxs, rpcTx) - } - - bloom, err := b.BlockBloom(blockRes) - if err != nil { - b.logger.Debug("failed to query BlockBloom", "height", block.Height, "error", err.Error()) - } - - req := &evmtypes.QueryValidatorAccountRequest{ - ConsAddress: sdk.ConsAddress(block.Header.ProposerAddress).String(), - } - - var validatorAccAddr sdk.AccAddress - - ctx := types.ContextWithHeight(block.Height) - res, err := b.queryClient.ValidatorAccount(ctx, req) - if err != nil { - b.logger.Debug( - "failed to query validator operator address", - "height", block.Height, - "cons-address", req.ConsAddress, - "error", err.Error(), - ) - // use zero address as the validator operator address - validatorAccAddr = sdk.AccAddress(common.Address{}.Bytes()) - } else { - validatorAccAddr, err = sdk.AccAddressFromBech32(res.AccountAddress) - if err != nil { - return nil, err - } - } - - validatorAddr := common.BytesToAddress(validatorAccAddr) - - gasLimit, err := types.BlockMaxGasFromConsensusParams(ctx, b.clientCtx, block.Height) - if err != nil { - b.logger.Error("failed to query consensus params", "error", err.Error()) - } - - gasUsed := uint64(0) - - for _, txsResult := range blockRes.TxsResults { - // workaround for cosmos-sdk bug. https://github.com/cosmos/cosmos-sdk/issues/10832 - if ShouldIgnoreGasUsed(txsResult) { - // block gas limit has exceeded, other txs must have failed with same reason. - break - } - 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 (b *Backend) CurrentHeader() *ethtypes.Header { - header, _ := b.HeaderByNumber(types.EthLatestBlockNumber) - return header -} - -// HeaderByNumber returns the block header identified by height. -func (b *Backend) HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error) { - resBlock, err := b.GetTendermintBlockByNumber(blockNum) - if err != nil { - return nil, err - } - - if resBlock == nil { - return nil, errors.Errorf("block not found for height %d", blockNum) - } - - blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) - if err != nil { - return nil, fmt.Errorf("block result not found for height %d", resBlock.Block.Height) - } - - bloom, err := b.BlockBloom(blockRes) - if err != nil { - b.logger.Debug("HeaderByNumber BlockBloom failed", "height", resBlock.Block.Height) - } - - baseFee, err := b.BaseFee(blockRes) - if err != nil { - // handle the error for pruned node. - b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", resBlock.Block.Height, "error", err) - } - - ethHeader := types.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) - return ethHeader, nil -} - -// GetBlockNumberByHash returns the block height of given block hash -func (b *Backend) GetBlockNumberByHash(blockHash common.Hash) (*big.Int, error) { - resBlock, err := b.GetTendermintBlockByHash(blockHash) - if err != nil { - return nil, err - } - if resBlock == nil { - return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) - } - return big.NewInt(resBlock.Block.Height), nil -} - -// HeaderByHash returns the block header identified by hash. -func (b *Backend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) { - resBlock, err := b.GetTendermintBlockByHash(blockHash) - if err != nil { - return nil, err - } - if resBlock == nil { - return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) - } - - blockRes, err := b.GetTendermintBlockResultByNumber(&resBlock.Block.Height) - if err != nil { - return nil, errors.Errorf("block result not found for height %d", resBlock.Block.Height) - } - - bloom, err := b.BlockBloom(blockRes) - if err != nil { - b.logger.Debug("HeaderByHash BlockBloom failed", "height", resBlock.Block.Height) - } - - baseFee, err := b.BaseFee(blockRes) - if err != nil { - // handle the error for pruned node. - b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", resBlock.Block.Height, "error", err) - } - - ethHeader := types.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) - return ethHeader, 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 (b *Backend) PendingTransactions() ([]*sdk.Tx, error) { - res, err := b.clientCtx.Client.UnconfirmedTxs(b.ctx, nil) - if err != nil { - return nil, err - } - - result := make([]*sdk.Tx, 0, len(res.Txs)) - for _, txBz := range res.Txs { - tx, err := b.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 (b *Backend) 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 := b.GetTendermintBlockResultByNumber(height) - if err != nil { - return nil, err - } - - return GetLogsFromBlockResults(blockRes) -} - -// GetLogs returns all the logs from all the ethereum transactions in a block. -func (b *Backend) GetLogs(hash common.Hash) ([][]*ethtypes.Log, error) { - resBlock, err := b.GetTendermintBlockByHash(hash) - if err != nil { - return nil, err - } - if resBlock == nil { - return nil, errors.Errorf("block not found for hash %s", hash) - } - - return b.GetLogsByHeight(&resBlock.Block.Header.Height) -} - -// BloomStatus returns the BloomBitsBlocks and the number of processed sections maintained -// by the chain indexer. -func (b *Backend) BloomStatus() (uint64, uint64) { - return 4096, 0 -} - -// GetCoinbase is the address that staking rewards will be send to (alias for Etherbase). -func (b *Backend) GetCoinbase() (sdk.AccAddress, error) { - node, err := b.clientCtx.GetNode() - if err != nil { - return nil, err - } - - status, err := node.Status(b.ctx) - if err != nil { - return nil, err - } - - req := &evmtypes.QueryValidatorAccountRequest{ - ConsAddress: sdk.ConsAddress(status.ValidatorInfo.Address).String(), - } - - res, err := b.queryClient.ValidatorAccount(b.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 (b *Backend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransaction, error) { - res, err := b.GetTxByEthHash(txHash) - hexTx := txHash.Hex() - - if err != nil { - // try to find tx in mempool - txs, err := b.PendingTransactions() - if err != nil { - b.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) - return nil, nil - } - - for _, tx := range txs { - msg, err := evmtypes.UnwrapEthereumMsg(tx, txHash) - if err != nil { - // not ethereum tx - continue - } - - if msg.Hash == hexTx { - rpctx, err := types.NewTransactionFromMsg( - msg, - common.Hash{}, - uint64(0), - uint64(0), - nil, - ) - if err != nil { - return nil, err - } - return rpctx, nil - } - } - - b.logger.Debug("tx not found", "hash", hexTx) - return nil, nil - } - - if !TxSuccessOrExceedsBlockGasLimit(&res.TxResult) { - return nil, errors.New("invalid ethereum tx") - } - - parsedTxs, err := types.ParseTxResult(&res.TxResult) - if err != nil { - return nil, fmt.Errorf("failed to parse tx events: %s", hexTx) - } - - parsedTx := parsedTxs.GetTxByHash(txHash) - if parsedTx == nil { - return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) - } - - tx, err := b.clientCtx.TxConfig.TxDecoder()(res.Tx) - if err != nil { - return nil, err - } - - // the `msgIndex` is inferred from tx events, should be within the bound. - msg, ok := tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) - if !ok { - return nil, errors.New("invalid ethereum tx") - } - - block, err := b.clientCtx.Client.Block(b.ctx, &res.Height) - if err != nil { - b.logger.Debug("block not found", "height", res.Height, "error", err.Error()) - return nil, err - } - - blockRes, err := b.GetTendermintBlockResultByNumber(&block.Block.Height) - if err != nil { - b.logger.Debug("block result not found", "height", block.Block.Height, "error", err.Error()) - return nil, nil - } - - if parsedTx.EthTxIndex == -1 { - // Fallback to find tx index by iterating all valid eth transactions - msgs := b.GetEthereumMsgsFromTendermintBlock(block, blockRes) - for i := range msgs { - if msgs[i].Hash == hexTx { - parsedTx.EthTxIndex = int64(i) - break - } - } - } - if parsedTx.EthTxIndex == -1 { - return nil, errors.New("can't find index of ethereum tx") - } - - baseFee, err := b.BaseFee(blockRes) - if err != nil { - // handle the error for pruned node. - b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", blockRes.Height, "error", err) - } - - return types.NewTransactionFromMsg( - msg, - common.BytesToHash(block.BlockID.Hash.Bytes()), - uint64(res.Height), - uint64(parsedTx.EthTxIndex), - baseFee, - ) -} - -// 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 (b *Backend) GetTxByEthHash(hash common.Hash) (*tmrpctypes.ResultTx, error) { - query := fmt.Sprintf("%s.%s='%s'", evmtypes.TypeMsgEthereumTx, evmtypes.AttributeKeyEthereumTxHash, hash.Hex()) - resTxs, err := b.clientCtx.Client.TxSearch(b.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 -} - -// GetTxByTxIndex uses `/tx_query` to find transaction by tx index of valid ethereum txs -func (b *Backend) GetTxByTxIndex(height int64, index uint) (*tmrpctypes.ResultTx, error) { - query := fmt.Sprintf("tx.height=%d AND %s.%s=%d", - height, evmtypes.TypeMsgEthereumTx, - evmtypes.AttributeKeyTxIndex, index, - ) - resTxs, err := b.clientCtx.Client.TxSearch(b.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 block %d index %d", height, index) - } - return resTxs.Txs[0], nil -} - -func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) { - // Look up the wallet containing the requested signer - _, err := b.clientCtx.Keyring.KeyByAddress(sdk.AccAddress(args.From.Bytes())) - if err != nil { - b.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 = b.SetTxDefaults(args) - if err != nil { - return common.Hash{}, err - } - - msg := args.ToTransaction() - if err := msg.ValidateBasic(); err != nil { - b.logger.Debug("tx failed basic validation", "error", err.Error()) - return common.Hash{}, err - } - - bn, err := b.BlockNumber() - if err != nil { - b.logger.Debug("failed to fetch latest block number", "error", err.Error()) - return common.Hash{}, err - } - - signer := ethtypes.MakeSigner(b.ChainConfig(), new(big.Int).SetUint64(uint64(bn))) - - // Sign transaction - if err := msg.Sign(signer, b.clientCtx.Keyring); err != nil { - b.logger.Debug("failed to sign tx", "error", err.Error()) - return common.Hash{}, err - } - - // Query params to use the EVM denomination - res, err := b.queryClient.QueryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) - if err != nil { - b.logger.Error("failed to query evm params", "error", err.Error()) - return common.Hash{}, err - } - - // Assemble transaction from fields - tx, err := msg.BuildTx(b.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom) - if err != nil { - b.logger.Error("build cosmos tx failed", "error", err.Error()) - return common.Hash{}, err - } - - // Encode transaction by default Tx encoder - txEncoder := b.clientCtx.TxConfig.TxEncoder() - txBytes, err := txEncoder(tx) - if err != nil { - b.logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) - return common.Hash{}, err - } - - ethTx := msg.AsTransaction() - - // check the local node config in case unprotected txs are disabled - if !b.UnprotectedAllowed() && !ethTx.Protected() { - // Ensure only eip155 signed transactions are submitted if EIP155Required is set. - return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") - } - - txHash := ethTx.Hash() - - // Broadcast transaction in sync mode (default) - // NOTE: If error is encountered on the node, the broadcast will not return an error - syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) - rsp, err := syncCtx.BroadcastTx(txBytes) - if rsp != nil && rsp.Code != 0 { - err = sdkerrors.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) - } - if err != nil { - b.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 (b *Backend) 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: b.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 := b.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 (b *Backend) GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error) { - // Get nonce (sequence) from account - from := sdk.AccAddress(address.Bytes()) - accRet := b.clientCtx.AccountRetriever - - err := accRet.EnsureExists(b.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 := b.getAccountNonce(address, includePending, blockNum.Int64(), b.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 (b *Backend) RPCGasCap() uint64 { - return b.cfg.JSONRPC.GasCap -} - -// RPCEVMTimeout is the global evm timeout for eth-call variants. -func (b *Backend) RPCEVMTimeout() time.Duration { - return b.cfg.JSONRPC.EVMTimeout -} - -// RPCGasCap is the global gas cap for eth-call variants. -func (b *Backend) RPCTxFeeCap() float64 { - return b.cfg.JSONRPC.TxFeeCap -} - -// RPCFilterCap is the limit for total number of filters that can be created -func (b *Backend) RPCFilterCap() int32 { - return b.cfg.JSONRPC.FilterCap -} - -// RPCFeeHistoryCap is the limit for total number of blocks that can be fetched -func (b *Backend) RPCFeeHistoryCap() int32 { - return b.cfg.JSONRPC.FeeHistoryCap -} - -// RPCLogsCap defines the max number of results can be returned from single `eth_getLogs` query. -func (b *Backend) RPCLogsCap() int32 { - return b.cfg.JSONRPC.LogsCap -} - -// RPCBlockRangeCap defines the max block range allowed for `eth_getLogs` query. -func (b *Backend) RPCBlockRangeCap() int32 { - return b.cfg.JSONRPC.BlockRangeCap -} - -// 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 (b *Backend) RPCMinGasPrice() int64 { - evmParams, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) - if err != nil { - return ethermint.DefaultGasPrice - } - - minGasPrice := b.cfg.GetMinGasPrices() - amt := minGasPrice.AmountOf(evmParams.Params.EvmDenom).TruncateInt64() - if amt == 0 { - return ethermint.DefaultGasPrice - } - - return amt -} - -// ChainConfig returns the latest ethereum chain configuration -func (b *Backend) ChainConfig() *params.ChainConfig { - params, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) - if err != nil { - return nil - } - - return params.Params.ChainConfig.EthereumConfig(b.chainID) -} - -// SuggestGasTipCap returns the suggested tip cap -// Although we don't support tx prioritization yet, but we return a positive value to help client to -// mitigate the base fee changes. -func (b *Backend) SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) { - if baseFee == nil { - // london hardfork not enabled or feemarket not enabled - return big.NewInt(0), nil - } - - params, err := b.queryClient.FeeMarket.Params(b.ctx, &feemarkettypes.QueryParamsRequest{}) - if err != nil { - return nil, err - } - // calculate the maximum base fee delta in current block, assuming all block gas limit is consumed - // ``` - // GasTarget = GasLimit / ElasticityMultiplier - // Delta = BaseFee * (GasUsed - GasTarget) / GasTarget / Denominator - // ``` - // The delta is at maximum when `GasUsed` is equal to `GasLimit`, which is: - // ``` - // MaxDelta = BaseFee * (GasLimit - GasLimit / ElasticityMultiplier) / (GasLimit / ElasticityMultiplier) / Denominator - // = BaseFee * (ElasticityMultiplier - 1) / Denominator - // ``` - maxDelta := baseFee.Int64() * (int64(params.Params.ElasticityMultiplier) - 1) / int64(params.Params.BaseFeeChangeDenominator) - if maxDelta < 0 { - // impossible if the parameter validation passed. - maxDelta = 0 - } - return big.NewInt(maxDelta), nil -} - -// BaseFee returns the base fee tracked by the Fee Market module. -// If the base fee is not enabled globally, the query returns nil. -// If the London hard fork is not activated at the current height, the query will -// return nil. -func (b *Backend) BaseFee(blockRes *tmrpctypes.ResultBlockResults) (*big.Int, error) { - // return BaseFee if London hard fork is activated and feemarket is enabled - res, err := b.queryClient.BaseFee(types.ContextWithHeight(blockRes.Height), &evmtypes.QueryBaseFeeRequest{}) - if err != nil || res.BaseFee == nil { - // we can't tell if it's london HF not enabled or the state is pruned, - // in either case, we'll fallback to parsing from begin blocker event, - // faster to iterate reversely - for i := len(blockRes.BeginBlockEvents) - 1; i >= 0; i-- { - evt := blockRes.BeginBlockEvents[i] - if evt.Type == feemarkettypes.EventTypeFeeMarket && len(evt.Attributes) > 0 { - baseFee, err := strconv.ParseInt(string(evt.Attributes[0].Value), 10, 64) - if err == nil { - return big.NewInt(baseFee), nil - } - break - } - } - return nil, err - } - - if res.BaseFee == nil { - return nil, nil - } - - return res.BaseFee.BigInt(), nil -} - -// GlobalMinGasPrice returns MinGasPrice param from FeeMarket -func (b *Backend) GlobalMinGasPrice() (sdk.Dec, error) { - res, err := b.queryClient.FeeMarket.Params(b.ctx, &feemarkettypes.QueryParamsRequest{}) - if err != nil { - return sdk.ZeroDec(), err - } - return res.Params.MinGasPrice, nil -} - -// FeeHistory returns data relevant for fee estimation based on the specified range of blocks. -func (b *Backend) FeeHistory( - userBlockCount rpc.DecimalOrHex, // number blocks to fetch, maximum is 100 - lastBlock rpc.BlockNumber, // the block to start search , to oldest - rewardPercentiles []float64, // percentiles to fetch reward -) (*types.FeeHistoryResult, error) { - blockEnd := int64(lastBlock) - - if blockEnd <= 0 { - blockNumber, err := b.BlockNumber() - if err != nil { - return nil, err - } - blockEnd = int64(blockNumber) - } - userBlockCountInt := int64(userBlockCount) - maxBlockCount := int64(b.cfg.JSONRPC.FeeHistoryCap) - if userBlockCountInt > maxBlockCount { - return nil, fmt.Errorf("FeeHistory user block count %d higher than %d", userBlockCountInt, maxBlockCount) - } - blockStart := blockEnd - userBlockCountInt - if blockStart < 0 { - blockStart = 0 - } - - blockCount := blockEnd - blockStart - - oldestBlock := (*hexutil.Big)(big.NewInt(blockStart)) - - // prepare space - reward := make([][]*hexutil.Big, blockCount) - rewardCount := len(rewardPercentiles) - for i := 0; i < int(blockCount); i++ { - reward[i] = make([]*hexutil.Big, rewardCount) - } - thisBaseFee := make([]*hexutil.Big, blockCount) - thisGasUsedRatio := make([]float64, blockCount) - - // rewards should only be calculated if reward percentiles were included - calculateRewards := rewardCount != 0 - - // fetch block - for blockID := blockStart; blockID < blockEnd; blockID++ { - index := int32(blockID - blockStart) - // tendermint block - tendermintblock, err := b.GetTendermintBlockByNumber(types.BlockNumber(blockID)) - if tendermintblock == nil { - return nil, err - } - - // eth block - ethBlock, err := b.GetBlockByNumber(types.BlockNumber(blockID), true) - if ethBlock == nil { - return nil, err - } - - // tendermint block result - tendermintBlockResult, err := b.GetTendermintBlockResultByNumber(&tendermintblock.Block.Height) - if tendermintBlockResult == nil { - b.logger.Debug("block result not found", "height", tendermintblock.Block.Height, "error", err.Error()) - return nil, err - } - - oneFeeHistory := types.OneFeeHistory{} - err = b.processBlock(tendermintblock, ðBlock, rewardPercentiles, tendermintBlockResult, &oneFeeHistory) - if err != nil { - return nil, err - } - - // copy - thisBaseFee[index] = (*hexutil.Big)(oneFeeHistory.BaseFee) - thisGasUsedRatio[index] = oneFeeHistory.GasUsedRatio - if calculateRewards { - for j := 0; j < rewardCount; j++ { - reward[index][j] = (*hexutil.Big)(oneFeeHistory.Reward[j]) - if reward[index][j] == nil { - reward[index][j] = (*hexutil.Big)(big.NewInt(0)) - } - } - } - } - - feeHistory := types.FeeHistoryResult{ - OldestBlock: oldestBlock, - BaseFee: thisBaseFee, - GasUsedRatio: thisGasUsedRatio, - } - - if calculateRewards { - feeHistory.Reward = reward - } - - return &feeHistory, nil -} - -// GetEthereumMsgsFromTendermintBlock returns all real MsgEthereumTxs from a -// Tendermint block. It also ensures consistency over the correct txs indexes -// across RPC endpoints -func (b *Backend) GetEthereumMsgsFromTendermintBlock( - resBlock *tmrpctypes.ResultBlock, - blockRes *tmrpctypes.ResultBlockResults, -) []*evmtypes.MsgEthereumTx { - var result []*evmtypes.MsgEthereumTx - block := resBlock.Block - - txResults := blockRes.TxsResults - - for i, tx := range block.Txs { - // Check if tx exists on EVM by cross checking with blockResults: - // - Include unsuccessful tx that exceeds block gas limit - // - Exclude unsuccessful tx with any other error but ExceedBlockGasLimit - if !TxSuccessOrExceedsBlockGasLimit(txResults[i]) { - b.logger.Debug("invalid tx result code", "cosmos-hash", hexutil.Encode(tx.Hash())) - continue - } - - tx, err := b.clientCtx.TxConfig.TxDecoder()(tx) - if err != nil { - b.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 - } - - ethMsg.Hash = ethMsg.AsTransaction().Hash().Hex() - result = append(result, ethMsg) - } - } - - return result -} - -// UnprotectedAllowed returns the node configuration value for allowing -// unprotected transactions (i.e not replay-protected) -func (b Backend) UnprotectedAllowed() bool { - return b.allowUnprotectedTxs -} diff --git a/rpc/backend/filters.go b/rpc/backend/filters.go new file mode 100644 index 00000000..5af09044 --- /dev/null +++ b/rpc/backend/filters.go @@ -0,0 +1,37 @@ +package backend + +import ( + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/pkg/errors" +) + +// GetLogs returns all the logs from all the ethereum transactions in a block. +func (b *Backend) GetLogs(hash common.Hash) ([][]*ethtypes.Log, error) { + resBlock, err := b.GetTendermintBlockByHash(hash) + if err != nil { + return nil, err + } + if resBlock == nil { + return nil, errors.Errorf("block not found for hash %s", hash) + } + + return b.GetLogsByHeight(&resBlock.Block.Header.Height) +} + +// GetLogsByHeight returns all the logs from all the ethereum transactions in a block. +func (b *Backend) 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 := b.GetTendermintBlockResultByNumber(height) + if err != nil { + return nil, err + } + + return GetLogsFromBlockResults(blockRes) +} + +// BloomStatus returns the BloomBitsBlocks and the number of processed sections maintained +// by the chain indexer. +func (b *Backend) BloomStatus() (uint64, uint64) { + return 4096, 0 +} diff --git a/rpc/backend/node_info.go b/rpc/backend/node_info.go new file mode 100644 index 00000000..0cc42d85 --- /dev/null +++ b/rpc/backend/node_info.go @@ -0,0 +1,299 @@ +package backend + +import ( + "fmt" + "math/big" + "time" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + sdkcrypto "github.com/cosmos/cosmos-sdk/crypto" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/evmos/ethermint/crypto/ethsecp256k1" + rpctypes "github.com/evmos/ethermint/rpc/types" + ethermint "github.com/evmos/ethermint/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + tmtypes "github.com/tendermint/tendermint/types" +) + +// Accounts returns the list of accounts available to this node. +func (b *Backend) Accounts() ([]common.Address, error) { + addresses := make([]common.Address, 0) // return [] instead of nil if empty + + infos, err := b.clientCtx.Keyring.List() + if err != nil { + return addresses, err + } + + for _, info := range infos { + pubKey, err := info.GetPubKey() + if err != nil { + return nil, err + } + addressBytes := pubKey.Address().Bytes() + addresses = append(addresses, common.BytesToAddress(addressBytes)) + } + + return addresses, nil +} + +// Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not +// yet received the latest block headers from its pears. In case it is synchronizing: +// - startingBlock: block number this node started to synchronize from +// - currentBlock: block number this node is currently importing +// - highestBlock: block number of the highest block header this node has received from peers +// - pulledStates: number of state entries processed until now +// - knownStates: number of known state entries that still need to be pulled +func (b *Backend) Syncing() (interface{}, error) { + status, err := b.clientCtx.Client.Status(b.ctx) + if err != nil { + return false, err + } + + if !status.SyncInfo.CatchingUp { + return false, nil + } + + return map[string]interface{}{ + "startingBlock": hexutil.Uint64(status.SyncInfo.EarliestBlockHeight), + "currentBlock": hexutil.Uint64(status.SyncInfo.LatestBlockHeight), + // "highestBlock": nil, // NA + // "pulledStates": nil, // NA + // "knownStates": nil, // NA + }, nil +} + +// SetEtherbase sets the etherbase of the miner +func (b *Backend) SetEtherbase(etherbase common.Address) bool { + delAddr, err := b.GetCoinbase() + if err != nil { + b.logger.Debug("failed to get coinbase address", "error", err.Error()) + return false + } + + withdrawAddr := sdk.AccAddress(etherbase.Bytes()) + msg := distributiontypes.NewMsgSetWithdrawAddress(delAddr, withdrawAddr) + + if err := msg.ValidateBasic(); err != nil { + b.logger.Debug("tx failed basic validation", "error", err.Error()) + return false + } + + // Assemble transaction from fields + builder, ok := b.clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + if !ok { + b.logger.Debug("clientCtx.TxConfig.NewTxBuilder returns unsupported builder", "error", err.Error()) + return false + } + + err = builder.SetMsgs(msg) + if err != nil { + b.logger.Error("builder.SetMsgs failed", "error", err.Error()) + return false + } + + // Fetch minimun gas price to calculate fees using the configuration. + minGasPrices := b.cfg.GetMinGasPrices() + if len(minGasPrices) == 0 || minGasPrices.Empty() { + b.logger.Debug("the minimun fee is not set") + return false + } + minGasPriceValue := minGasPrices[0].Amount + denom := minGasPrices[0].Denom + + delCommonAddr := common.BytesToAddress(delAddr.Bytes()) + nonce, err := b.GetTransactionCount(delCommonAddr, rpctypes.EthPendingBlockNumber) + if err != nil { + b.logger.Debug("failed to get nonce", "error", err.Error()) + return false + } + + txFactory := tx.Factory{} + txFactory = txFactory. + WithChainID(b.clientCtx.ChainID). + WithKeybase(b.clientCtx.Keyring). + WithTxConfig(b.clientCtx.TxConfig). + WithSequence(uint64(*nonce)). + WithGasAdjustment(1.25) + + _, gas, err := tx.CalculateGas(b.clientCtx, txFactory, msg) + if err != nil { + b.logger.Debug("failed to calculate gas", "error", err.Error()) + return false + } + + txFactory = txFactory.WithGas(gas) + + value := new(big.Int).SetUint64(gas * minGasPriceValue.Ceil().TruncateInt().Uint64()) + fees := sdk.Coins{sdk.NewCoin(denom, sdkmath.NewIntFromBigInt(value))} + builder.SetFeeAmount(fees) + builder.SetGasLimit(gas) + + keyInfo, err := b.clientCtx.Keyring.KeyByAddress(delAddr) + if err != nil { + b.logger.Debug("failed to get the wallet address using the keyring", "error", err.Error()) + return false + } + + if err := tx.Sign(txFactory, keyInfo.Name, builder, false); err != nil { + b.logger.Debug("failed to sign tx", "error", err.Error()) + return false + } + + // Encode transaction by default Tx encoder + txEncoder := b.clientCtx.TxConfig.TxEncoder() + txBytes, err := txEncoder(builder.GetTx()) + if err != nil { + b.logger.Debug("failed to encode eth tx using default encoder", "error", err.Error()) + return false + } + + tmHash := common.BytesToHash(tmtypes.Tx(txBytes).Hash()) + + // Broadcast transaction in sync mode (default) + // NOTE: If error is encountered on the node, the broadcast will not return an error + syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) + rsp, err := syncCtx.BroadcastTx(txBytes) + if rsp != nil && rsp.Code != 0 { + err = sdkerrors.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) + } + if err != nil { + b.logger.Debug("failed to broadcast tx", "error", err.Error()) + return false + } + + b.logger.Debug("broadcasted tx to set miner withdraw address (etherbase)", "hash", tmHash.String()) + return true +} + +// ImportRawKey armors and encrypts a given raw hex encoded ECDSA key and stores it into the key directory. +// The name of the key will have the format "personal_", where is the total number of +// keys stored on the keyring. +// +// NOTE: The key will be both armored and encrypted using the same passphrase. +func (b *Backend) ImportRawKey(privkey, password string) (common.Address, error) { + priv, err := crypto.HexToECDSA(privkey) + if err != nil { + return common.Address{}, err + } + + privKey := ðsecp256k1.PrivKey{Key: crypto.FromECDSA(priv)} + + addr := sdk.AccAddress(privKey.PubKey().Address().Bytes()) + ethereumAddr := common.BytesToAddress(addr) + + // return if the key has already been imported + if _, err := b.clientCtx.Keyring.KeyByAddress(addr); err == nil { + return ethereumAddr, nil + } + + // ignore error as we only care about the length of the list + list, _ := b.clientCtx.Keyring.List() + privKeyName := fmt.Sprintf("personal_%d", len(list)) + + armor := sdkcrypto.EncryptArmorPrivKey(privKey, password, ethsecp256k1.KeyType) + + if err := b.clientCtx.Keyring.ImportPrivKey(privKeyName, armor, password); err != nil { + return common.Address{}, err + } + + b.logger.Info("key successfully imported", "name", privKeyName, "address", ethereumAddr.String()) + + return ethereumAddr, nil +} + +// ListAccounts will return a list of addresses for accounts this node manages. +func (b *Backend) ListAccounts() ([]common.Address, error) { + addrs := []common.Address{} + + list, err := b.clientCtx.Keyring.List() + if err != nil { + return nil, err + } + + for _, info := range list { + pubKey, err := info.GetPubKey() + if err != nil { + return nil, err + } + addrs = append(addrs, common.BytesToAddress(pubKey.Address())) + } + + return addrs, nil +} + +// NewAccount will create a new account and returns the address for the new account. +func (b *Backend) NewMnemonic(uid string, language keyring.Language, hdPath, bip39Passphrase string, algo keyring.SignatureAlgo) (*keyring.Record, error) { + info, _, err := b.clientCtx.Keyring.NewMnemonic(uid, keyring.English, bip39Passphrase, bip39Passphrase, algo) + if err != nil { + return nil, err + } + return info, err +} + +// UnprotectedAllowed returns the node configuration value for allowing +// unprotected transactions (i.e not replay-protected) +func (b Backend) UnprotectedAllowed() bool { + return b.allowUnprotectedTxs +} + +// RPCGasCap is the global gas cap for eth-call variants. +func (b *Backend) RPCGasCap() uint64 { + return b.cfg.JSONRPC.GasCap +} + +// RPCEVMTimeout is the global evm timeout for eth-call variants. +func (b *Backend) RPCEVMTimeout() time.Duration { + return b.cfg.JSONRPC.EVMTimeout +} + +// RPCGasCap is the global gas cap for eth-call variants. +func (b *Backend) RPCTxFeeCap() float64 { + return b.cfg.JSONRPC.TxFeeCap +} + +// RPCFilterCap is the limit for total number of filters that can be created +func (b *Backend) RPCFilterCap() int32 { + return b.cfg.JSONRPC.FilterCap +} + +// RPCFeeHistoryCap is the limit for total number of blocks that can be fetched +func (b *Backend) RPCFeeHistoryCap() int32 { + return b.cfg.JSONRPC.FeeHistoryCap +} + +// RPCLogsCap defines the max number of results can be returned from single `eth_getLogs` query. +func (b *Backend) RPCLogsCap() int32 { + return b.cfg.JSONRPC.LogsCap +} + +// RPCBlockRangeCap defines the max block range allowed for `eth_getLogs` query. +func (b *Backend) RPCBlockRangeCap() int32 { + return b.cfg.JSONRPC.BlockRangeCap +} + +// 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 (b *Backend) RPCMinGasPrice() int64 { + evmParams, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) + if err != nil { + return ethermint.DefaultGasPrice + } + + minGasPrice := b.cfg.GetMinGasPrices() + amt := minGasPrice.AmountOf(evmParams.Params.EvmDenom).TruncateInt64() + if amt == 0 { + return ethermint.DefaultGasPrice + } + + return amt +} diff --git a/rpc/backend/sign_tx.go b/rpc/backend/sign_tx.go new file mode 100644 index 00000000..ae656215 --- /dev/null +++ b/rpc/backend/sign_tx.go @@ -0,0 +1,148 @@ +package backend + +import ( + "fmt" + "math/big" + + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/evmos/ethermint/ethereum/eip712" + evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/pkg/errors" +) + +// SendTransaction sends transaction based on received args using Node's key to sign it +func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) { + // Look up the wallet containing the requested signer + _, err := b.clientCtx.Keyring.KeyByAddress(sdk.AccAddress(args.From.Bytes())) + if err != nil { + b.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 = b.SetTxDefaults(args) + if err != nil { + return common.Hash{}, err + } + + msg := args.ToTransaction() + if err := msg.ValidateBasic(); err != nil { + b.logger.Debug("tx failed basic validation", "error", err.Error()) + return common.Hash{}, err + } + + bn, err := b.BlockNumber() + if err != nil { + b.logger.Debug("failed to fetch latest block number", "error", err.Error()) + return common.Hash{}, err + } + + signer := ethtypes.MakeSigner(b.ChainConfig(), new(big.Int).SetUint64(uint64(bn))) + + // Sign transaction + if err := msg.Sign(signer, b.clientCtx.Keyring); err != nil { + b.logger.Debug("failed to sign tx", "error", err.Error()) + return common.Hash{}, err + } + + // Query params to use the EVM denomination + res, err := b.queryClient.QueryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) + if err != nil { + b.logger.Error("failed to query evm params", "error", err.Error()) + return common.Hash{}, err + } + + // Assemble transaction from fields + tx, err := msg.BuildTx(b.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom) + if err != nil { + b.logger.Error("build cosmos tx failed", "error", err.Error()) + return common.Hash{}, err + } + + // Encode transaction by default Tx encoder + txEncoder := b.clientCtx.TxConfig.TxEncoder() + txBytes, err := txEncoder(tx) + if err != nil { + b.logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) + return common.Hash{}, err + } + + ethTx := msg.AsTransaction() + + // check the local node config in case unprotected txs are disabled + if !b.UnprotectedAllowed() && !ethTx.Protected() { + // Ensure only eip155 signed transactions are submitted if EIP155Required is set. + return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") + } + + txHash := ethTx.Hash() + + // Broadcast transaction in sync mode (default) + // NOTE: If error is encountered on the node, the broadcast will not return an error + syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) + rsp, err := syncCtx.BroadcastTx(txBytes) + if rsp != nil && rsp.Code != 0 { + err = sdkerrors.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) + } + if err != nil { + b.logger.Error("failed to broadcast tx", "error", err.Error()) + return txHash, err + } + + // Return transaction hash + return txHash, nil +} + +// Sign signs the provided data using the private key of address via Geth's signature standard. +func (b *Backend) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { + from := sdk.AccAddress(address.Bytes()) + + _, err := b.clientCtx.Keyring.KeyByAddress(from) + if err != nil { + b.logger.Error("failed to find key in keyring", "address", address.String()) + return nil, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error()) + } + + // Sign the requested hash with the wallet + signature, _, err := b.clientCtx.Keyring.SignByAddress(from, data) + if err != nil { + b.logger.Error("keyring.SignByAddress failed", "address", address.Hex()) + return nil, err + } + + signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper + return signature, nil +} + +// SignTypedData signs EIP-712 conformant typed data +func (b *Backend) SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) { + from := sdk.AccAddress(address.Bytes()) + + _, err := b.clientCtx.Keyring.KeyByAddress(from) + if err != nil { + b.logger.Error("failed to find key in keyring", "address", address.String()) + return nil, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error()) + } + + sigHash, err := eip712.ComputeTypedDataHash(typedData) + if err != nil { + return nil, err + } + + // Sign the requested hash with the wallet + signature, _, err := b.clientCtx.Keyring.SignByAddress(from, sigHash) + if err != nil { + b.logger.Error("keyring.SignByAddress failed", "address", address.Hex()) + return nil, err + } + + signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper + return signature, nil +} diff --git a/rpc/backend/tracing.go b/rpc/backend/tracing.go new file mode 100644 index 00000000..6c54d512 --- /dev/null +++ b/rpc/backend/tracing.go @@ -0,0 +1,181 @@ +package backend + +import ( + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/common" + rpctypes "github.com/evmos/ethermint/rpc/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/pkg/errors" + tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +// TraceTransaction returns the structured logs created during the execution of EVM +// and returns them as a JSON object. +func (b *Backend) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) (interface{}, error) { + // Get transaction by hash + transaction, err := b.GetTxByEthHash(hash) + if err != nil { + b.logger.Debug("tx not found", "hash", hash) + return nil, err + } + + // check if block number is 0 + if transaction.Height == 0 { + return nil, errors.New("genesis is not traceable") + } + + blk, err := b.GetTendermintBlockByNumber(rpctypes.BlockNumber(transaction.Height)) + if err != nil { + b.logger.Debug("block not found", "height", transaction.Height) + return nil, err + } + + parsedTxs, err := rpctypes.ParseTxResult(&transaction.TxResult) + if err != nil { + return nil, fmt.Errorf("failed to parse tx events: %s", hash.Hex()) + } + parsedTx := parsedTxs.GetTxByHash(hash) + if parsedTx == nil { + return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hash.Hex()) + } + + // check tx index is not out of bound + if uint32(len(blk.Block.Txs)) < transaction.Index { + b.logger.Debug("tx index out of bounds", "index", transaction.Index, "hash", hash.String(), "height", blk.Block.Height) + return nil, fmt.Errorf("transaction not included in block %v", blk.Block.Height) + } + + var predecessors []*evmtypes.MsgEthereumTx + for _, txBz := range blk.Block.Txs[:transaction.Index] { + tx, err := b.clientCtx.TxConfig.TxDecoder()(txBz) + if err != nil { + b.logger.Debug("failed to decode transaction in block", "height", blk.Block.Height, "error", err.Error()) + continue + } + for _, msg := range tx.GetMsgs() { + ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + continue + } + + predecessors = append(predecessors, ethMsg) + } + } + + tx, err := b.clientCtx.TxConfig.TxDecoder()(transaction.Tx) + if err != nil { + b.logger.Debug("tx not found", "hash", hash) + return nil, err + } + + // add predecessor messages in current cosmos tx + for i := 0; i < parsedTx.MsgIndex; i++ { + ethMsg, ok := tx.GetMsgs()[i].(*evmtypes.MsgEthereumTx) + if !ok { + continue + } + predecessors = append(predecessors, ethMsg) + } + + ethMessage, ok := tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) + if !ok { + b.logger.Debug("invalid transaction type", "type", fmt.Sprintf("%T", tx)) + return nil, fmt.Errorf("invalid transaction type %T", tx) + } + + traceTxRequest := evmtypes.QueryTraceTxRequest{ + Msg: ethMessage, + Predecessors: predecessors, + BlockNumber: blk.Block.Height, + BlockTime: blk.Block.Time, + BlockHash: common.Bytes2Hex(blk.BlockID.Hash), + } + + if config != nil { + traceTxRequest.TraceConfig = config + } + + // minus one to get the context of block beginning + contextHeight := transaction.Height - 1 + if contextHeight < 1 { + // 0 is a special value in `ContextWithHeight` + contextHeight = 1 + } + traceResult, err := b.queryClient.TraceTx(rpctypes.ContextWithHeight(contextHeight), &traceTxRequest) + if err != nil { + return nil, err + } + + // Response format is unknown due to custom tracer config param + // More information can be found here https://geth.ethereum.org/docs/dapp/tracing-filtered + var decodedResult interface{} + err = json.Unmarshal(traceResult.Data, &decodedResult) + if err != nil { + return nil, err + } + + return decodedResult, nil +} + +// traceBlock configures a new tracer according to the provided configuration, and +// executes all the transactions contained within. The return value will be one item +// per transaction, dependent on the requested tracer. +func (b *Backend) TraceBlock(height rpctypes.BlockNumber, config *evmtypes.TraceConfig, block *tmrpctypes.ResultBlock) ([]*evmtypes.TxTraceResult, error) { + txs := block.Block.Txs + txsLength := len(txs) + + if txsLength == 0 { + // If there are no transactions return empty array + return []*evmtypes.TxTraceResult{}, nil + } + + txDecoder := b.clientCtx.TxConfig.TxDecoder() + + var txsMessages []*evmtypes.MsgEthereumTx + for i, tx := range txs { + decodedTx, err := txDecoder(tx) + if err != nil { + b.logger.Error("failed to decode transaction", "hash", txs[i].Hash(), "error", err.Error()) + continue + } + + for _, msg := range decodedTx.GetMsgs() { + ethMessage, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + // Just considers Ethereum transactions + continue + } + txsMessages = append(txsMessages, ethMessage) + } + } + + // minus one to get the context at the beginning of the block + contextHeight := height - 1 + if contextHeight < 1 { + // 0 is a special value for `ContextWithHeight`. + contextHeight = 1 + } + ctxWithHeight := rpctypes.ContextWithHeight(int64(contextHeight)) + + traceBlockRequest := &evmtypes.QueryTraceBlockRequest{ + Txs: txsMessages, + TraceConfig: config, + BlockNumber: block.Block.Height, + BlockTime: block.Block.Time, + BlockHash: common.Bytes2Hex(block.BlockID.Hash), + } + + res, err := b.queryClient.TraceBlock(ctxWithHeight, traceBlockRequest) + if err != nil { + return nil, err + } + + decodedResults := make([]*evmtypes.TxTraceResult, txsLength) + if err := json.Unmarshal(res.Data, &decodedResults); err != nil { + return nil, err + } + + return decodedResults, nil +} diff --git a/rpc/backend/tx_info.go b/rpc/backend/tx_info.go new file mode 100644 index 00000000..4de59661 --- /dev/null +++ b/rpc/backend/tx_info.go @@ -0,0 +1,393 @@ +package backend + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + rpctypes "github.com/evmos/ethermint/rpc/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/pkg/errors" + tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +// GetTransactionByHash returns the Ethereum format transaction identified by Ethereum transaction hash +func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransaction, error) { + res, err := b.GetTxByEthHash(txHash) + hexTx := txHash.Hex() + + if err != nil { + // try to find tx in mempool + txs, err := b.PendingTransactions() + if err != nil { + b.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) + return nil, nil + } + + for _, tx := range txs { + msg, err := evmtypes.UnwrapEthereumMsg(tx, txHash) + if err != nil { + // not ethereum tx + continue + } + + if msg.Hash == hexTx { + rpctx, err := rpctypes.NewTransactionFromMsg( + msg, + common.Hash{}, + uint64(0), + uint64(0), + nil, + ) + if err != nil { + return nil, err + } + return rpctx, nil + } + } + + b.logger.Debug("tx not found", "hash", hexTx) + return nil, nil + } + + if !TxSuccessOrExceedsBlockGasLimit(&res.TxResult) { + return nil, errors.New("invalid ethereum tx") + } + + parsedTxs, err := rpctypes.ParseTxResult(&res.TxResult) + if err != nil { + return nil, fmt.Errorf("failed to parse tx events: %s", hexTx) + } + + parsedTx := parsedTxs.GetTxByHash(txHash) + if parsedTx == nil { + return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) + } + + tx, err := b.clientCtx.TxConfig.TxDecoder()(res.Tx) + if err != nil { + return nil, err + } + + // the `msgIndex` is inferred from tx events, should be within the bound. + msg, ok := tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) + if !ok { + return nil, errors.New("invalid ethereum tx") + } + + block, err := b.clientCtx.Client.Block(b.ctx, &res.Height) + if err != nil { + b.logger.Debug("block not found", "height", res.Height, "error", err.Error()) + return nil, err + } + + blockRes, err := b.GetTendermintBlockResultByNumber(&block.Block.Height) + if err != nil { + b.logger.Debug("block result not found", "height", block.Block.Height, "error", err.Error()) + return nil, nil + } + + if parsedTx.EthTxIndex == -1 { + // Fallback to find tx index by iterating all valid eth transactions + msgs := b.GetEthereumMsgsFromTendermintBlock(block, blockRes) + for i := range msgs { + if msgs[i].Hash == hexTx { + parsedTx.EthTxIndex = int64(i) + break + } + } + } + if parsedTx.EthTxIndex == -1 { + return nil, errors.New("can't find index of ethereum tx") + } + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", blockRes.Height, "error", err) + } + + return rpctypes.NewTransactionFromMsg( + msg, + common.BytesToHash(block.BlockID.Hash.Bytes()), + uint64(res.Height), + uint64(parsedTx.EthTxIndex), + baseFee, + ) +} + +// GetTransactionReceipt returns the transaction receipt identified by hash. +func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { + hexTx := hash.Hex() + b.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) + + res, err := b.GetTxByEthHash(hash) + if err != nil { + b.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) + return nil, nil + } + + // don't ignore the txs which exceed block gas limit. + if !TxSuccessOrExceedsBlockGasLimit(&res.TxResult) { + return nil, nil + } + + parsedTxs, err := rpctypes.ParseTxResult(&res.TxResult) + if err != nil { + return nil, fmt.Errorf("failed to parse tx events: %s, %v", hexTx, err) + } + + parsedTx := parsedTxs.GetTxByHash(hash) + if parsedTx == nil { + return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) + } + + resBlock, err := b.clientCtx.Client.Block(b.ctx, &res.Height) + if err != nil { + b.logger.Debug("block not found", "height", res.Height, "error", err.Error()) + return nil, nil + } + + tx, err := b.clientCtx.TxConfig.TxDecoder()(res.Tx) + if err != nil { + b.logger.Debug("decoding failed", "error", err.Error()) + return nil, fmt.Errorf("failed to decode tx: %w", err) + } + + if res.TxResult.Code != 0 { + // tx failed, we should return gas limit as gas used, because that's how the fee get deducted. + for i := 0; i <= parsedTx.MsgIndex; i++ { + gasLimit := tx.GetMsgs()[i].(*evmtypes.MsgEthereumTx).GetGas() + parsedTxs.Txs[i].GasUsed = gasLimit + } + } + + // the `msgIndex` is inferred from tx events, should be within the bound, + // and the tx is found by eth tx hash, so the msg type must be correct. + ethMsg := tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) + + txData, err := evmtypes.UnpackTxData(ethMsg.Data) + if err != nil { + b.logger.Error("failed to unpack tx data", "error", err.Error()) + return nil, err + } + + cumulativeGasUsed := uint64(0) + blockRes, err := b.GetTendermintBlockResultByNumber(&res.Height) + if err != nil { + b.logger.Debug("failed to retrieve block results", "height", res.Height, "error", err.Error()) + return nil, nil + } + for i := 0; i < int(res.Index) && i < len(blockRes.TxsResults); i++ { + cumulativeGasUsed += uint64(blockRes.TxsResults[i].GasUsed) + } + cumulativeGasUsed += parsedTxs.AccumulativeGasUsed(parsedTx.MsgIndex) + + // Get the transaction result from the log + var status hexutil.Uint + if res.TxResult.Code != 0 || parsedTx.Failed { + status = hexutil.Uint(ethtypes.ReceiptStatusFailed) + } else { + status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful) + } + + from, err := ethMsg.GetSender(b.chainID) + if err != nil { + return nil, err + } + + // parse tx logs from events + logs, err := parsedTx.ParseTxLogs() + if err != nil { + b.logger.Debug("failed to parse logs", "hash", hexTx, "error", err.Error()) + } + + if parsedTx.EthTxIndex == -1 { + // Fallback to find tx index by iterating all valid eth transactions + msgs := b.GetEthereumMsgsFromTendermintBlock(resBlock, blockRes) + for i := range msgs { + if msgs[i].Hash == hexTx { + parsedTx.EthTxIndex = int64(i) + break + } + } + } + + if parsedTx.EthTxIndex == -1 { + return nil, errors.New("can't find index of ethereum tx") + } + + receipt := map[string]interface{}{ + // Consensus fields: These fields are defined by the Yellow Paper + "status": status, + "cumulativeGasUsed": hexutil.Uint64(cumulativeGasUsed), + "logsBloom": ethtypes.BytesToBloom(ethtypes.LogsBloom(logs)), + "logs": logs, + + // Implementation fields: These fields are added by geth when processing a transaction. + // They are stored in the chain database. + "transactionHash": hash, + "contractAddress": nil, + "gasUsed": hexutil.Uint64(parsedTx.GasUsed), + + // Inclusion information: These fields provide information about the inclusion of the + // transaction corresponding to this receipt. + "blockHash": common.BytesToHash(resBlock.Block.Header.Hash()).Hex(), + "blockNumber": hexutil.Uint64(res.Height), + "transactionIndex": hexutil.Uint64(parsedTx.EthTxIndex), + + // sender and receiver (contract or EOA) addreses + "from": from, + "to": txData.GetTo(), + } + + if logs == nil { + receipt["logs"] = [][]*ethtypes.Log{} + } + + // If the ContractAddress is 20 0x0 bytes, assume it is not a contract creation + if txData.GetTo() == nil { + receipt["contractAddress"] = crypto.CreateAddress(from, txData.GetNonce()) + } + + if dynamicTx, ok := txData.(*evmtypes.DynamicFeeTx); ok { + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // tolerate the error for pruned node. + b.logger.Error("fetch basefee failed, node is pruned?", "height", res.Height, "error", err) + } else { + receipt["effectiveGasPrice"] = hexutil.Big(*dynamicTx.EffectiveGasPrice(baseFee)) + } + } + + return receipt, nil +} + +// GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. +func (b *Backend) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + b.logger.Debug("eth_getTransactionByBlockHashAndIndex", "hash", hash.Hex(), "index", idx) + + block, err := b.clientCtx.Client.BlockByHash(b.ctx, hash.Bytes()) + if err != nil { + b.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error()) + return nil, nil + } + + if block.Block == nil { + b.logger.Debug("block not found", "hash", hash.Hex()) + return nil, nil + } + + return b.GetTransactionByBlockAndIndex(block, idx) +} + +// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. +func (b *Backend) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + b.logger.Debug("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx) + + block, err := b.GetTendermintBlockByNumber(blockNum) + if err != nil { + b.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) + return nil, nil + } + + if block.Block == nil { + b.logger.Debug("block not found", "height", blockNum.Int64()) + return nil, nil + } + + return b.GetTransactionByBlockAndIndex(block, idx) +} + +// 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 (b *Backend) GetTxByEthHash(hash common.Hash) (*tmrpctypes.ResultTx, error) { + query := fmt.Sprintf("%s.%s='%s'", evmtypes.TypeMsgEthereumTx, evmtypes.AttributeKeyEthereumTxHash, hash.Hex()) + resTxs, err := b.clientCtx.Client.TxSearch(b.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 +} + +// GetTxByTxIndex uses `/tx_query` to find transaction by tx index of valid ethereum txs +func (b *Backend) GetTxByTxIndex(height int64, index uint) (*tmrpctypes.ResultTx, error) { + query := fmt.Sprintf("tx.height=%d AND %s.%s=%d", + height, evmtypes.TypeMsgEthereumTx, + evmtypes.AttributeKeyTxIndex, index, + ) + resTxs, err := b.clientCtx.Client.TxSearch(b.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 block %d index %d", height, index) + } + return resTxs.Txs[0], nil +} + +// getTransactionByBlockAndIndex is the common code shared by `GetTransactionByBlockNumberAndIndex` and `GetTransactionByBlockHashAndIndex`. +func (b *Backend) GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + blockRes, err := b.GetTendermintBlockResultByNumber(&block.Block.Height) + if err != nil { + return nil, nil + } + + var msg *evmtypes.MsgEthereumTx + // try /tx_search first + res, err := b.GetTxByTxIndex(block.Block.Height, uint(idx)) + if err == nil { + tx, err := b.clientCtx.TxConfig.TxDecoder()(res.Tx) + if err != nil { + b.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) + return nil, nil + } + + parsedTxs, err := rpctypes.ParseTxResult(&res.TxResult) + if err != nil { + return nil, fmt.Errorf("failed to parse tx events: %d, %v", idx, err) + } + + parsedTx := parsedTxs.GetTxByTxIndex(int(idx)) + if parsedTx == nil { + return nil, fmt.Errorf("ethereum tx not found in msgs: %d", idx) + } + + var ok bool + // msgIndex is inferred from tx events, should be within bound. + msg, ok = tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) + if !ok { + b.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) + return nil, nil + } + } else { + i := int(idx) + ethMsgs := b.GetEthereumMsgsFromTendermintBlock(block, blockRes) + if i >= len(ethMsgs) { + b.logger.Debug("block txs index out of bound", "index", i) + return nil, nil + } + + msg = ethMsgs[i] + } + + baseFee, err := b.BaseFee(blockRes) + if err != nil { + // handle the error for pruned node. + b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", block.Block.Height, "error", err) + } + + return rpctypes.NewTransactionFromMsg( + msg, + common.BytesToHash(block.Block.Hash()), + uint64(block.Block.Height), + uint64(idx), + baseFee, + ) +} diff --git a/rpc/backend/utils.go b/rpc/backend/utils.go index 1f4bafa8..737df49a 100644 --- a/rpc/backend/utils.go +++ b/rpc/backend/utils.go @@ -3,7 +3,6 @@ package backend import ( "bytes" "encoding/json" - "errors" "fmt" "math/big" "sort" @@ -44,133 +43,6 @@ func (s sortGasAndReward) Less(i, j int) bool { return s[i].reward.Cmp(s[j].reward) < 0 } -// SetTxDefaults populates tx message with default values in case they are not -// provided on the args -func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) { - if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { - return args, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") - } - - head := b.CurrentHeader() - if head == nil { - return args, errors.New("latest header is nil") - } - - // If user specifies both maxPriorityfee and maxFee, then we do not - // need to consult the chain for defaults. It's definitely a London tx. - if args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil { - // In this clause, user left some fields unspecified. - if head.BaseFee != nil && args.GasPrice == nil { - if args.MaxPriorityFeePerGas == nil { - tip, err := b.SuggestGasTipCap(head.BaseFee) - if err != nil { - return args, err - } - args.MaxPriorityFeePerGas = (*hexutil.Big)(tip) - } - - if args.MaxFeePerGas == nil { - gasFeeCap := new(big.Int).Add( - (*big.Int)(args.MaxPriorityFeePerGas), - new(big.Int).Mul(head.BaseFee, big.NewInt(2)), - ) - args.MaxFeePerGas = (*hexutil.Big)(gasFeeCap) - } - - if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 { - return args, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas) - } - - } else { - if args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil { - return args, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but london is not active yet") - } - - if args.GasPrice == nil { - price, err := b.SuggestGasTipCap(head.BaseFee) - if err != nil { - return args, err - } - if head.BaseFee != nil { - // The legacy tx gas price suggestion should not add 2x base fee - // because all fees are consumed, so it would result in a spiral - // upwards. - price.Add(price, head.BaseFee) - } - args.GasPrice = (*hexutil.Big)(price) - } - } - } else { - // Both maxPriorityfee and maxFee set by caller. Sanity-check their internal relation - if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 { - return args, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas) - } - } - - if args.Value == nil { - args.Value = new(hexutil.Big) - } - if args.Nonce == nil { - // get the nonce from the account retriever - // ignore error in case tge account doesn't exist yet - nonce, _ := b.getAccountNonce(*args.From, true, 0, b.logger) - args.Nonce = (*hexutil.Uint64)(&nonce) - } - - if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) { - return args, errors.New("both 'data' and 'input' are set and not equal. Please use 'input' to pass transaction call data") - } - - if args.To == nil { - // Contract creation - var input []byte - if args.Data != nil { - input = *args.Data - } else if args.Input != nil { - input = *args.Input - } - - if len(input) == 0 { - return args, errors.New("contract creation without any data provided") - } - } - - if args.Gas == nil { - // For backwards-compatibility reason, we try both input and data - // but input is preferred. - input := args.Input - if input == nil { - input = args.Data - } - - callArgs := evmtypes.TransactionArgs{ - From: args.From, - To: args.To, - Gas: args.Gas, - GasPrice: args.GasPrice, - MaxFeePerGas: args.MaxFeePerGas, - MaxPriorityFeePerGas: args.MaxPriorityFeePerGas, - Value: args.Value, - Data: input, - AccessList: args.AccessList, - } - - blockNr := types.NewBlockNumber(big.NewInt(0)) - estimated, err := b.EstimateGas(callArgs, &blockNr) - if err != nil { - return args, err - } - args.Gas = &estimated - b.logger.Debug("estimate gas usage automatically", "gas", args.Gas) - } - - if args.ChainID == nil { - args.ChainID = (*hexutil.Big)(b.chainID) - } - - return args, nil -} - // getAccountNonce returns the account nonce for the given account address. // If the pending value is true, it will iterate over the mempool (pending) // txs in order to compute and return the pending tx sequence. diff --git a/rpc/namespaces/ethereum/debug/api.go b/rpc/namespaces/ethereum/debug/api.go index c46493f6..2ae1bdb7 100644 --- a/rpc/namespaces/ethereum/debug/api.go +++ b/rpc/namespaces/ethereum/debug/api.go @@ -2,7 +2,6 @@ package debug import ( "bytes" - "encoding/json" "errors" "fmt" "io" @@ -14,11 +13,9 @@ import ( "time" "github.com/davecgh/go-spew/spew" - tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" evmtypes "github.com/evmos/ethermint/x/evm/types" - "github.com/cosmos/cosmos-sdk/client" stderrors "github.com/pkg/errors" "github.com/cosmos/cosmos-sdk/server" @@ -43,27 +40,22 @@ type HandlerT struct { // API is the collection of tracing APIs exposed over the private debugging endpoint. type API struct { - ctx *server.Context - logger log.Logger - backend backend.EVMBackend - clientCtx client.Context - queryClient *rpctypes.QueryClient - handler *HandlerT + ctx *server.Context + logger log.Logger + backend backend.EVMBackend + handler *HandlerT } // NewAPI creates a new API definition for the tracing methods of the Ethereum service. func NewAPI( ctx *server.Context, backend backend.EVMBackend, - clientCtx client.Context, ) *API { return &API{ - ctx: ctx, - logger: ctx.Logger.With("module", "debug"), - backend: backend, - clientCtx: clientCtx, - queryClient: rpctypes.NewQueryClient(clientCtx), - handler: new(HandlerT), + ctx: ctx, + logger: ctx.Logger.With("module", "debug"), + backend: backend, + handler: new(HandlerT), } } @@ -71,109 +63,7 @@ func NewAPI( // and returns them as a JSON object. func (a *API) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) (interface{}, error) { a.logger.Debug("debug_traceTransaction", "hash", hash) - // Get transaction by hash - transaction, err := a.backend.GetTxByEthHash(hash) - if err != nil { - a.logger.Debug("tx not found", "hash", hash) - return nil, err - } - - // check if block number is 0 - if transaction.Height == 0 { - return nil, errors.New("genesis is not traceable") - } - - blk, err := a.backend.GetTendermintBlockByNumber(rpctypes.BlockNumber(transaction.Height)) - if err != nil { - a.logger.Debug("block not found", "height", transaction.Height) - return nil, err - } - - parsedTxs, err := rpctypes.ParseTxResult(&transaction.TxResult) - if err != nil { - return nil, fmt.Errorf("failed to parse tx events: %s", hash.Hex()) - } - parsedTx := parsedTxs.GetTxByHash(hash) - if parsedTx == nil { - return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hash.Hex()) - } - - // check tx index is not out of bound - if uint32(len(blk.Block.Txs)) < transaction.Index { - a.logger.Debug("tx index out of bounds", "index", transaction.Index, "hash", hash.String(), "height", blk.Block.Height) - return nil, fmt.Errorf("transaction not included in block %v", blk.Block.Height) - } - - var predecessors []*evmtypes.MsgEthereumTx - for _, txBz := range blk.Block.Txs[:transaction.Index] { - tx, err := a.clientCtx.TxConfig.TxDecoder()(txBz) - if err != nil { - a.logger.Debug("failed to decode transaction in block", "height", blk.Block.Height, "error", err.Error()) - continue - } - for _, msg := range tx.GetMsgs() { - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - continue - } - - predecessors = append(predecessors, ethMsg) - } - } - - tx, err := a.clientCtx.TxConfig.TxDecoder()(transaction.Tx) - if err != nil { - a.logger.Debug("tx not found", "hash", hash) - return nil, err - } - - // add predecessor messages in current cosmos tx - for i := 0; i < parsedTx.MsgIndex; i++ { - ethMsg, ok := tx.GetMsgs()[i].(*evmtypes.MsgEthereumTx) - if !ok { - continue - } - predecessors = append(predecessors, ethMsg) - } - - ethMessage, ok := tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) - if !ok { - a.logger.Debug("invalid transaction type", "type", fmt.Sprintf("%T", tx)) - return nil, fmt.Errorf("invalid transaction type %T", tx) - } - - traceTxRequest := evmtypes.QueryTraceTxRequest{ - Msg: ethMessage, - Predecessors: predecessors, - BlockNumber: blk.Block.Height, - BlockTime: blk.Block.Time, - BlockHash: common.Bytes2Hex(blk.BlockID.Hash), - } - - if config != nil { - traceTxRequest.TraceConfig = config - } - - // minus one to get the context of block beginning - contextHeight := transaction.Height - 1 - if contextHeight < 1 { - // 0 is a special value in `ContextWithHeight` - contextHeight = 1 - } - traceResult, err := a.queryClient.TraceTx(rpctypes.ContextWithHeight(contextHeight), &traceTxRequest) - if err != nil { - return nil, err - } - - // Response format is unknown due to custom tracer config param - // More information can be found here https://geth.ethereum.org/docs/dapp/tracing-filtered - var decodedResult interface{} - err = json.Unmarshal(traceResult.Data, &decodedResult) - if err != nil { - return nil, err - } - - return decodedResult, nil + return a.backend.TraceTransaction(hash, config) } // TraceBlockByNumber returns the structured logs created during the execution of @@ -190,7 +80,7 @@ func (a *API) TraceBlockByNumber(height rpctypes.BlockNumber, config *evmtypes.T return nil, err } - return a.traceBlock(height, config, resBlock) + return a.backend.TraceBlock(rpctypes.BlockNumber(resBlock.Block.Height), config, resBlock) } // TraceBlockByHash returns the structured logs created during the execution of @@ -209,68 +99,7 @@ func (a *API) TraceBlockByHash(hash common.Hash, config *evmtypes.TraceConfig) ( return nil, errors.New("block not found") } - return a.traceBlock(rpctypes.BlockNumber(resBlock.Block.Height), config, resBlock) -} - -// traceBlock configures a new tracer according to the provided configuration, and -// executes all the transactions contained within. The return value will be one item -// per transaction, dependent on the requested tracer. -func (a *API) traceBlock(height rpctypes.BlockNumber, config *evmtypes.TraceConfig, block *tmrpctypes.ResultBlock) ([]*evmtypes.TxTraceResult, error) { - txs := block.Block.Txs - txsLength := len(txs) - - if txsLength == 0 { - // If there are no transactions return empty array - return []*evmtypes.TxTraceResult{}, nil - } - - txDecoder := a.clientCtx.TxConfig.TxDecoder() - - var txsMessages []*evmtypes.MsgEthereumTx - for i, tx := range txs { - decodedTx, err := txDecoder(tx) - if err != nil { - a.logger.Error("failed to decode transaction", "hash", txs[i].Hash(), "error", err.Error()) - continue - } - - for _, msg := range decodedTx.GetMsgs() { - ethMessage, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - // Just considers Ethereum transactions - continue - } - txsMessages = append(txsMessages, ethMessage) - } - } - - // minus one to get the context at the beginning of the block - contextHeight := height - 1 - if contextHeight < 1 { - // 0 is a special value for `ContextWithHeight`. - contextHeight = 1 - } - ctxWithHeight := rpctypes.ContextWithHeight(int64(contextHeight)) - - traceBlockRequest := &evmtypes.QueryTraceBlockRequest{ - Txs: txsMessages, - TraceConfig: config, - BlockNumber: block.Block.Height, - BlockTime: block.Block.Time, - BlockHash: common.Bytes2Hex(block.BlockID.Hash), - } - - res, err := a.queryClient.TraceBlock(ctxWithHeight, traceBlockRequest) - if err != nil { - return nil, err - } - - decodedResults := make([]*evmtypes.TxTraceResult, txsLength) - if err := json.Unmarshal(res.Data, &decodedResults); err != nil { - return nil, err - } - - return decodedResults, nil + return a.backend.TraceBlock(rpctypes.BlockNumber(resBlock.Block.Height), config, resBlock) } // BlockProfile turns on goroutine profiling for nsec seconds and writes profile data to diff --git a/rpc/namespaces/ethereum/eth/api.go b/rpc/namespaces/ethereum/eth/api.go index 60421be3..571251f8 100644 --- a/rpc/namespaces/ethereum/eth/api.go +++ b/rpc/namespaces/ethereum/eth/api.go @@ -2,40 +2,19 @@ package eth import ( "context" - "encoding/json" "fmt" - "math" "math/big" - "github.com/evmos/ethermint/ethereum/eip712" - "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/rpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "github.com/pkg/errors" - "github.com/spf13/viper" "github.com/tendermint/tendermint/libs/log" - tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" - sdkmath "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/crypto/keyring" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - - "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/evmos/ethermint/crypto/hd" "github.com/evmos/ethermint/rpc/backend" rpctypes "github.com/evmos/ethermint/rpc/types" @@ -43,159 +22,294 @@ import ( evmtypes "github.com/evmos/ethermint/x/evm/types" ) +// The Ethereum API allows applications to connect to an Evmos node that is +// part of the Evmos blockchain. Developers can interact with on-chain EVM data +// and send different types of transactions to the network by utilizing the +// endpoints provided by the API. The API follows a JSON-RPC standard. If not +// otherwise specified, the interface is derived from the Alchemy Ethereum API: +// https://docs.alchemy.com/alchemy/apis/ethereum +type EthereumAPI interface { + // Getting Blocks + // + // Retrieves information from a particular block in the blockchain. + BlockNumber() (hexutil.Uint64, error) + GetBlockByNumber(ethBlockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) + GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) + + // Reading Transactions + // + // Retrieves information on the state data for addresses regardless of whether + // it is a user or a smart contract. + GetTransactionByHash(hash common.Hash) (*rpctypes.RPCTransaction, error) + GetTransactionCount(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Uint64, error) + GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) + GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint + GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint + GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) + GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) + // eth_getBlockReceipts + + // Writing Transactions + // + // Allows developers to both send ETH from one address to another, write data + // on-chain, and interact with smart contracts. + SendRawTransaction(data hexutil.Bytes) (common.Hash, error) + SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) + // eth_sendPrivateTransaction + // eth_cancel PrivateTransaction + + // Account Information + // + // Returns information regarding an address's stored on-chain data. + Accounts() ([]common.Address, error) + GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) + GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) + GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) + GetProof(address common.Address, storageKeys []string, blockNrOrHash rpctypes.BlockNumberOrHash) (*rpctypes.AccountResult, error) + + // EVM/Smart Contract Execution + // + // Allows developers to read data from the blockchain which includes executing + // smart contracts. However, no data is published to the Ethereum network. + Call(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, _ *rpctypes.StateOverride) (hexutil.Bytes, error) + + // Chain Information + // + // Returns information on the Ethereum network and internal settings. + ProtocolVersion() hexutil.Uint + GasPrice() (*hexutil.Big, error) + EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) + FeeHistory(blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*rpctypes.FeeHistoryResult, error) + MaxPriorityFeePerGas() (*hexutil.Big, error) + ChainId() (*hexutil.Big, error) + + // Getting Uncles + // + // Returns information on uncle blocks are which are network rejected blocks and replaced by a canonical block instead. + GetUncleByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) map[string]interface{} + GetUncleByBlockNumberAndIndex(number, idx hexutil.Uint) map[string]interface{} + GetUncleCountByBlockHash(hash common.Hash) hexutil.Uint + GetUncleCountByBlockNumber(blockNum rpctypes.BlockNumber) hexutil.Uint + + // Proof of Work + Hashrate() hexutil.Uint64 + Mining() bool + + // Other + Syncing() (interface{}, error) + Coinbase() (string, error) + Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) + GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) + SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) + FillTransaction(args evmtypes.TransactionArgs) (*rpctypes.SignTransactionResult, error) + Resend(ctx context.Context, args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) + GetPendingTransactions() ([]*rpctypes.RPCTransaction, error) + // eth_signTransaction (on Ethereum.org) + // eth_getCompilers (on Ethereum.org) + // eth_compileSolidity (on Ethereum.org) + // eth_compileLLL (on Ethereum.org) + // eth_compileSerpent (on Ethereum.org) + // eth_getWork (on Ethereum.org) + // eth_submitWork (on Ethereum.org) + // eth_submitHashrate (on Ethereum.org) +} + +var _ EthereumAPI = (*PublicAPI)(nil) + // PublicAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. type PublicAPI struct { - ctx context.Context - clientCtx client.Context - queryClient *rpctypes.QueryClient - chainIDEpoch *big.Int - logger log.Logger - backend backend.EVMBackend - nonceLock *rpctypes.AddrLocker - signer ethtypes.Signer + ctx context.Context + logger log.Logger + backend backend.EVMBackend } // NewPublicAPI creates an instance of the public ETH Web3 API. func NewPublicAPI( logger log.Logger, - clientCtx client.Context, backend backend.EVMBackend, - nonceLock *rpctypes.AddrLocker, ) *PublicAPI { - eip155ChainID, err := ethermint.ParseChainID(clientCtx.ChainID) - if err != nil { - panic(err) - } - - algos, _ := clientCtx.Keyring.SupportedAlgorithms() - - if !algos.Contains(hd.EthSecp256k1) { - kr, err := keyring.New( - sdk.KeyringServiceName(), - viper.GetString(flags.FlagKeyringBackend), - clientCtx.KeyringDir, - clientCtx.Input, - clientCtx.Codec, - hd.EthSecp256k1Option(), - ) - if err != nil { - panic(err) - } - - clientCtx = clientCtx.WithKeyring(kr) - } - - // The signer used by the API should always be the 'latest' known one because we expect - // signers to be backwards-compatible with old transactions. - cfg := backend.ChainConfig() - if cfg == nil { - cfg = evmtypes.DefaultChainConfig().EthereumConfig(eip155ChainID) - } - - signer := ethtypes.LatestSigner(cfg) - api := &PublicAPI{ - ctx: context.Background(), - clientCtx: clientCtx, - queryClient: rpctypes.NewQueryClient(clientCtx), - chainIDEpoch: eip155ChainID, - logger: logger.With("client", "json-rpc"), - backend: backend, - nonceLock: nonceLock, - signer: signer, + ctx: context.Background(), + logger: logger.With("client", "json-rpc"), + backend: backend, } return api } -// ClientCtx returns client context -func (e *PublicAPI) ClientCtx() client.Context { - return e.clientCtx +/////////////////////////////////////////////////////////////////////////////// +/// Blocks /// +/////////////////////////////////////////////////////////////////////////////// + +// BlockNumber returns the current block number. +func (e *PublicAPI) BlockNumber() (hexutil.Uint64, error) { + e.logger.Debug("eth_blockNumber") + return e.backend.BlockNumber() } -func (e *PublicAPI) QueryClient() *rpctypes.QueryClient { - return e.queryClient +// GetBlockByNumber returns the block identified by number. +func (e *PublicAPI) GetBlockByNumber(ethBlockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) { + e.logger.Debug("eth_getBlockByNumber", "number", ethBlockNum, "full", fullTx) + return e.backend.GetBlockByNumber(ethBlockNum, fullTx) } -func (e *PublicAPI) Ctx() context.Context { - return e.ctx +// GetBlockByHash returns the block identified by hash. +func (e *PublicAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + e.logger.Debug("eth_getBlockByHash", "hash", hash.Hex(), "full", fullTx) + return e.backend.GetBlockByHash(hash, fullTx) } +/////////////////////////////////////////////////////////////////////////////// +/// Read Txs /// +/////////////////////////////////////////////////////////////////////////////// + +// GetTransactionByHash returns the transaction identified by hash. +func (e *PublicAPI) GetTransactionByHash(hash common.Hash) (*rpctypes.RPCTransaction, error) { + e.logger.Debug("eth_getTransactionByHash", "hash", hash.Hex()) + return e.backend.GetTransactionByHash(hash) +} + +// GetTransactionCount returns the number of transactions at the given address up to the given block number. +func (e *PublicAPI) GetTransactionCount(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Uint64, error) { + e.logger.Debug("eth_getTransactionCount", "address", address.Hex(), "block number or hash", blockNrOrHash) + blockNum, err := e.backend.GetBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } + return e.backend.GetTransactionCount(address, blockNum) +} + +// GetTransactionReceipt returns the transaction receipt identified by hash. +func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { + hexTx := hash.Hex() + e.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) + + return e.backend.GetTransactionReceipt(hash) +} + +// GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. +func (e *PublicAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { + e.logger.Debug("eth_getBlockTransactionCountByHash", "hash", hash.Hex()) + + return e.backend.GetBlockTransactionCountByHash(hash) +} + +// GetBlockTransactionCountByNumber returns the number of transactions in the block identified by number. +func (e *PublicAPI) GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint { + e.logger.Debug("eth_getBlockTransactionCountByNumber", "height", blockNum.Int64()) + + return e.backend.GetBlockTransactionCountByNumber(blockNum) +} + +// GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. +func (e *PublicAPI) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + e.logger.Debug("eth_getTransactionByBlockHashAndIndex", "hash", hash.Hex(), "index", idx) + return e.backend.GetTransactionByBlockHashAndIndex(hash, idx) +} + +// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. +func (e *PublicAPI) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + e.logger.Debug("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx) + + block, err := e.backend.GetTendermintBlockByNumber(blockNum) + if err != nil { + e.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) + return nil, nil + } + + if block.Block == nil { + e.logger.Debug("block not found", "height", blockNum.Int64()) + return nil, nil + } + + return e.backend.GetTransactionByBlockAndIndex(block, idx) +} + +/////////////////////////////////////////////////////////////////////////////// +/// Write Txs /// +/////////////////////////////////////////////////////////////////////////////// + +// SendRawTransaction send a raw Ethereum transaction. +func (e *PublicAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { + e.logger.Debug("eth_sendRawTransaction", "length", len(data)) + return e.backend.SendRawTransaction(data) +} + +// SendTransaction sends an Ethereum transaction. +func (e *PublicAPI) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) { + e.logger.Debug("eth_sendTransaction", "args", args.String()) + return e.backend.SendTransaction(args) +} + +/////////////////////////////////////////////////////////////////////////////// +/// Account Information /// +/////////////////////////////////////////////////////////////////////////////// + +// Accounts returns the list of accounts available to this node. +func (e *PublicAPI) Accounts() ([]common.Address, error) { + e.logger.Debug("eth_accounts") + return e.backend.Accounts() +} + +// GetBalance returns the provided account's balance up to the provided block number. +func (e *PublicAPI) GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) { + e.logger.Debug("eth_getBalance", "address", address.String(), "block number or hash", blockNrOrHash) + return e.backend.GetBalance(address, blockNrOrHash) +} + +// GetStorageAt returns the contract storage at the given address, block number, and key. +func (e *PublicAPI) GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { + e.logger.Debug("eth_getStorageAt", "address", address.Hex(), "key", key, "block number or hash", blockNrOrHash) + return e.backend.GetStorageAt(address, key, blockNrOrHash) +} + +// GetCode returns the contract code at the given address and block number. +func (e *PublicAPI) GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { + e.logger.Debug("eth_getCode", "address", address.Hex(), "block number or hash", blockNrOrHash) + return e.backend.GetCode(address, blockNrOrHash) +} + +// GetProof returns an account object with proof and any storage proofs +func (e *PublicAPI) GetProof(address common.Address, storageKeys []string, blockNrOrHash rpctypes.BlockNumberOrHash) (*rpctypes.AccountResult, error) { + e.logger.Debug("eth_getProof", "address", address.Hex(), "keys", storageKeys, "block number or hash", blockNrOrHash) + return e.backend.GetProof(address, storageKeys, blockNrOrHash) +} + +/////////////////////////////////////////////////////////////////////////////// +/// EVM/Smart Contract Execution /// +/////////////////////////////////////////////////////////////////////////////// + +// Call performs a raw contract call. +func (e *PublicAPI) Call(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, _ *rpctypes.StateOverride) (hexutil.Bytes, error) { + e.logger.Debug("eth_call", "args", args.String(), "block number or hash", blockNrOrHash) + + blockNum, err := e.backend.GetBlockNumber(blockNrOrHash) + if err != nil { + return nil, err + } + data, err := e.backend.DoCall(args, blockNum) + if err != nil { + return []byte{}, err + } + + return (hexutil.Bytes)(data.Ret), nil +} + +/////////////////////////////////////////////////////////////////////////////// +/// Event Logs /// +/////////////////////////////////////////////////////////////////////////////// +// FILTER API + +/////////////////////////////////////////////////////////////////////////////// +/// Chain Information /// +/////////////////////////////////////////////////////////////////////////////// + // ProtocolVersion returns the supported Ethereum protocol version. func (e *PublicAPI) ProtocolVersion() hexutil.Uint { e.logger.Debug("eth_protocolVersion") return hexutil.Uint(ethermint.ProtocolVersion) } -// ChainId is the EIP-155 replay-protection chain id for the current ethereum chain config. -func (e *PublicAPI) ChainId() (*hexutil.Big, error) { //nolint - e.logger.Debug("eth_chainId") - // if current block is at or past the EIP-155 replay-protection fork block, return chainID from config - bn, err := e.backend.BlockNumber() - if err != nil { - e.logger.Debug("failed to fetch latest block number", "error", err.Error()) - return (*hexutil.Big)(e.chainIDEpoch), nil - } - - if config := e.backend.ChainConfig(); config.IsEIP155(new(big.Int).SetUint64(uint64(bn))) { - return (*hexutil.Big)(config.ChainID), nil - } - - return nil, fmt.Errorf("chain not synced beyond EIP-155 replay-protection fork block") -} - -// Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not -// yet received the latest block headers from its pears. In case it is synchronizing: -// - startingBlock: block number this node started to synchronize from -// - currentBlock: block number this node is currently importing -// - highestBlock: block number of the highest block header this node has received from peers -// - pulledStates: number of state entries processed until now -// - knownStates: number of known state entries that still need to be pulled -func (e *PublicAPI) Syncing() (interface{}, error) { - e.logger.Debug("eth_syncing") - - status, err := e.clientCtx.Client.Status(e.ctx) - if err != nil { - return false, err - } - - if !status.SyncInfo.CatchingUp { - return false, nil - } - - return map[string]interface{}{ - "startingBlock": hexutil.Uint64(status.SyncInfo.EarliestBlockHeight), - "currentBlock": hexutil.Uint64(status.SyncInfo.LatestBlockHeight), - // "highestBlock": nil, // NA - // "pulledStates": nil, // NA - // "knownStates": nil, // NA - }, nil -} - -// Coinbase is the address that staking rewards will be send to (alias for Etherbase). -func (e *PublicAPI) Coinbase() (string, error) { - e.logger.Debug("eth_coinbase") - - coinbase, err := e.backend.GetCoinbase() - if err != nil { - return "", err - } - ethAddr := common.BytesToAddress(coinbase.Bytes()) - return ethAddr.Hex(), nil -} - -// Mining returns whether or not this node is currently mining. Always false. -func (e *PublicAPI) Mining() bool { - e.logger.Debug("eth_mining") - return false -} - -// Hashrate returns the current node's hashrate. Always 0. -func (e *PublicAPI) Hashrate() hexutil.Uint64 { - e.logger.Debug("eth_hashrate") - return 0 -} - // GasPrice returns the current gas price based on Ethermint's gas price oracle. func (e *PublicAPI) GasPrice() (*hexutil.Big, error) { e.logger.Debug("eth_gasPrice") @@ -226,6 +340,17 @@ func (e *PublicAPI) GasPrice() (*hexutil.Big, error) { return (*hexutil.Big)(result), nil } +// EstimateGas returns an estimate of gas usage for the given smart contract call. +func (e *PublicAPI) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) { + e.logger.Debug("eth_estimateGas") + return e.backend.EstimateGas(args, blockNrOptional) +} + +func (e *PublicAPI) FeeHistory(blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*rpctypes.FeeHistoryResult, error) { + e.logger.Debug("eth_feeHistory") + return e.backend.FeeHistory(blockCount, lastBlock, rewardPercentiles) +} + // MaxPriorityFeePerGas returns a suggestion for a gas tip cap for dynamic fee transactions. func (e *PublicAPI) MaxPriorityFeePerGas() (*hexutil.Big, error) { e.logger.Debug("eth_maxPriorityFeePerGas") @@ -237,156 +362,24 @@ func (e *PublicAPI) MaxPriorityFeePerGas() (*hexutil.Big, error) { return (*hexutil.Big)(tipcap), nil } -func (e *PublicAPI) FeeHistory(blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*rpctypes.FeeHistoryResult, error) { - e.logger.Debug("eth_feeHistory") - return e.backend.FeeHistory(blockCount, lastBlock, rewardPercentiles) +// ChainId is the EIP-155 replay-protection chain id for the current ethereum chain config. +func (e *PublicAPI) ChainId() (*hexutil.Big, error) { //nolint + e.logger.Debug("eth_chainId") + return e.backend.ChainID() } -// Accounts returns the list of accounts available to this node. -func (e *PublicAPI) Accounts() ([]common.Address, error) { - e.logger.Debug("eth_accounts") +/////////////////////////////////////////////////////////////////////////////// +/// Uncles /// +/////////////////////////////////////////////////////////////////////////////// - addresses := make([]common.Address, 0) // return [] instead of nil if empty - - infos, err := e.clientCtx.Keyring.List() - if err != nil { - return addresses, err - } - - for _, info := range infos { - pubKey, err := info.GetPubKey() - if err != nil { - return nil, err - } - addressBytes := pubKey.Address().Bytes() - addresses = append(addresses, common.BytesToAddress(addressBytes)) - } - - return addresses, nil +// GetUncleByBlockHashAndIndex returns the uncle identified by hash and index. Always returns nil. +func (e *PublicAPI) GetUncleByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) map[string]interface{} { + return nil } -// BlockNumber returns the current block number. -func (e *PublicAPI) BlockNumber() (hexutil.Uint64, error) { - e.logger.Debug("eth_blockNumber") - return e.backend.BlockNumber() -} - -// GetBalance returns the provided account's balance up to the provided block number. -func (e *PublicAPI) GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) { - e.logger.Debug("eth_getBalance", "address", address.String(), "block number or hash", blockNrOrHash) - - blockNum, err := e.getBlockNumber(blockNrOrHash) - if err != nil { - return nil, err - } - - req := &evmtypes.QueryBalanceRequest{ - Address: address.String(), - } - - _, err = e.backend.GetTendermintBlockByNumber(blockNum) - if err != nil { - return nil, err - } - - res, err := e.queryClient.Balance(rpctypes.ContextWithHeight(blockNum.Int64()), req) - if err != nil { - return nil, err - } - - val, ok := sdkmath.NewIntFromString(res.Balance) - if !ok { - return nil, errors.New("invalid balance") - } - - // balance can only be negative in case of pruned node - if val.IsNegative() { - return nil, errors.New("couldn't fetch balance. Node state is pruned") - } - - return (*hexutil.Big)(val.BigInt()), nil -} - -// GetStorageAt returns the contract storage at the given address, block number, and key. -func (e *PublicAPI) GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { - e.logger.Debug("eth_getStorageAt", "address", address.Hex(), "key", key, "block number or hash", blockNrOrHash) - - blockNum, err := e.getBlockNumber(blockNrOrHash) - if err != nil { - return nil, err - } - - req := &evmtypes.QueryStorageRequest{ - Address: address.String(), - Key: key, - } - - res, err := e.queryClient.Storage(rpctypes.ContextWithHeight(blockNum.Int64()), req) - if err != nil { - return nil, err - } - - value := common.HexToHash(res.Value) - return value.Bytes(), nil -} - -// GetTransactionCount returns the number of transactions at the given address up to the given block number. -func (e *PublicAPI) GetTransactionCount(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Uint64, error) { - e.logger.Debug("eth_getTransactionCount", "address", address.Hex(), "block number or hash", blockNrOrHash) - blockNum, err := e.getBlockNumber(blockNrOrHash) - if err != nil { - return nil, err - } - return e.backend.GetTransactionCount(address, blockNum) -} - -// GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. -func (e *PublicAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { - e.logger.Debug("eth_getBlockTransactionCountByHash", "hash", hash.Hex()) - - block, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes()) - if err != nil { - e.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error()) - return nil - } - - if block.Block == nil { - e.logger.Debug("block not found", "hash", hash.Hex()) - return nil - } - - blockRes, err := e.backend.GetTendermintBlockResultByNumber(&block.Block.Height) - if err != nil { - return nil - } - - ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(block, blockRes) - n := hexutil.Uint(len(ethMsgs)) - return &n -} - -// GetBlockTransactionCountByNumber returns the number of transactions in the block identified by number. -func (e *PublicAPI) GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint { - e.logger.Debug("eth_getBlockTransactionCountByNumber", "height", blockNum.Int64()) - block, err := e.backend.GetTendermintBlockByNumber(blockNum) - if err != nil { - e.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) - return nil - } - - if block.Block == nil { - e.logger.Debug("block not found", "height", blockNum.Int64()) - return nil - } - - blockRes, err := e.backend.GetTendermintBlockResultByNumber(&block.Block.Height) - if err != nil { - return nil - } - - ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(block, blockRes) - n := hexutil.Uint(len(ethMsgs)) - return &n +// GetUncleByBlockNumberAndIndex returns the uncle identified by number and index. Always returns nil. +func (e *PublicAPI) GetUncleByBlockNumberAndIndex(number, idx hexutil.Uint) map[string]interface{} { + return nil } // GetUncleCountByBlockHash returns the number of uncles in the block identified by hash. Always zero. @@ -399,25 +392,54 @@ func (e *PublicAPI) GetUncleCountByBlockNumber(blockNum rpctypes.BlockNumber) he return 0 } -// GetCode returns the contract code at the given address and block number. -func (e *PublicAPI) GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { - e.logger.Debug("eth_getCode", "address", address.Hex(), "block number or hash", blockNrOrHash) +/////////////////////////////////////////////////////////////////////////////// +/// Proof of Work /// +/////////////////////////////////////////////////////////////////////////////// - blockNum, err := e.getBlockNumber(blockNrOrHash) +// Hashrate returns the current node's hashrate. Always 0. +func (e *PublicAPI) Hashrate() hexutil.Uint64 { + e.logger.Debug("eth_hashrate") + return 0 +} + +// Mining returns whether or not this node is currently mining. Always false. +func (e *PublicAPI) Mining() bool { + e.logger.Debug("eth_mining") + return false +} + +/////////////////////////////////////////////////////////////////////////////// +/// Other /// +/////////////////////////////////////////////////////////////////////////////// + +// Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not +// yet received the latest block headers from its pears. In case it is synchronizing: +// - startingBlock: block number this node started to synchronize from +// - currentBlock: block number this node is currently importing +// - highestBlock: block number of the highest block header this node has received from peers +// - pulledStates: number of state entries processed until now +// - knownStates: number of known state entries that still need to be pulled +func (e *PublicAPI) Syncing() (interface{}, error) { + e.logger.Debug("eth_syncing") + return e.backend.Syncing() +} + +// Coinbase is the address that staking rewards will be send to (alias for Etherbase). +func (e *PublicAPI) Coinbase() (string, error) { + e.logger.Debug("eth_coinbase") + + coinbase, err := e.backend.GetCoinbase() if err != nil { - return nil, err + return "", err } + ethAddr := common.BytesToAddress(coinbase.Bytes()) + return ethAddr.Hex(), nil +} - req := &evmtypes.QueryCodeRequest{ - Address: address.String(), - } - - res, err := e.queryClient.Code(rpctypes.ContextWithHeight(blockNum.Int64()), req) - if err != nil { - return nil, err - } - - return res.Code, nil +// Sign signs the provided data using the private key of address via Geth's signature standard. +func (e *PublicAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { + e.logger.Debug("eth_sign", "address", address.Hex(), "data", common.Bytes2Hex(data)) + return e.backend.Sign(address, data) } // GetTransactionLogs returns the logs given a transaction hash. @@ -450,60 +472,10 @@ func (e *PublicAPI) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, err return parsedTx.ParseTxLogs() } -// Sign signs the provided data using the private key of address via Geth's signature standard. -func (e *PublicAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { - e.logger.Debug("eth_sign", "address", address.Hex(), "data", common.Bytes2Hex(data)) - - from := sdk.AccAddress(address.Bytes()) - - _, err := e.clientCtx.Keyring.KeyByAddress(from) - if err != nil { - e.logger.Error("failed to find key in keyring", "address", address.String()) - return nil, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error()) - } - - // Sign the requested hash with the wallet - signature, _, err := e.clientCtx.Keyring.SignByAddress(from, data) - if err != nil { - e.logger.Error("keyring.SignByAddress failed", "address", address.Hex()) - return nil, err - } - - signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper - return signature, nil -} - // SignTypedData signs EIP-712 conformant typed data func (e *PublicAPI) SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) { e.logger.Debug("eth_signTypedData", "address", address.Hex(), "data", typedData) - from := sdk.AccAddress(address.Bytes()) - - _, err := e.clientCtx.Keyring.KeyByAddress(from) - if err != nil { - e.logger.Error("failed to find key in keyring", "address", address.String()) - return nil, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error()) - } - - sigHash, err := eip712.ComputeTypedDataHash(typedData) - if err != nil { - return nil, err - } - - // Sign the requested hash with the wallet - signature, _, err := e.clientCtx.Keyring.SignByAddress(from, sigHash) - if err != nil { - e.logger.Error("keyring.SignByAddress failed", "address", address.Hex()) - return nil, err - } - - signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper - return signature, nil -} - -// SendTransaction sends an Ethereum transaction. -func (e *PublicAPI) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) { - e.logger.Debug("eth_sendTransaction", "args", args.String()) - return e.backend.SendTransaction(args) + return e.backend.SignTypedData(address, typedData) } // FillTransaction fills the defaults (nonce, gas, gasPrice or 1559 fields) @@ -530,464 +502,11 @@ func (e *PublicAPI) FillTransaction(args evmtypes.TransactionArgs) (*rpctypes.Si }, nil } -// SendRawTransaction send a raw Ethereum transaction. -func (e *PublicAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { - e.logger.Debug("eth_sendRawTransaction", "length", len(data)) - - // RLP decode raw transaction bytes - tx := ðtypes.Transaction{} - if err := tx.UnmarshalBinary(data); err != nil { - e.logger.Error("transaction decoding failed", "error", err.Error()) - return common.Hash{}, err - } - - // check the local node config in case unprotected txs are disabled - if !e.backend.UnprotectedAllowed() && !tx.Protected() { - // Ensure only eip155 signed transactions are submitted if EIP155Required is set. - return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") - } - - ethereumTx := &evmtypes.MsgEthereumTx{} - if err := ethereumTx.FromEthereumTx(tx); err != nil { - e.logger.Error("transaction converting failed", "error", err.Error()) - return common.Hash{}, err - } - - if err := ethereumTx.ValidateBasic(); err != nil { - e.logger.Debug("tx failed basic validation", "error", err.Error()) - return common.Hash{}, err - } - - // 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 - } - - cosmosTx, err := ethereumTx.BuildTx(e.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom) - if err != nil { - e.logger.Error("failed to build cosmos tx", "error", err.Error()) - return common.Hash{}, err - } - - // Encode transaction by default Tx encoder - txBytes, err := e.clientCtx.TxConfig.TxEncoder()(cosmosTx) - if err != nil { - e.logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) - return common.Hash{}, err - } - - txHash := ethereumTx.AsTransaction().Hash() - - syncCtx := e.clientCtx.WithBroadcastMode(flags.BroadcastSync) - rsp, err := syncCtx.BroadcastTx(txBytes) - if rsp != nil && rsp.Code != 0 { - err = sdkerrors.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) - } - if err != nil { - e.logger.Error("failed to broadcast tx", "error", err.Error()) - return txHash, err - } - - return txHash, nil -} - // Resend accepts an existing transaction and a new gas price and limit. It will remove // the given transaction from the pool and reinsert it with the new gas price and limit. -func (e *PublicAPI) Resend(ctx context.Context, args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) { +func (e *PublicAPI) Resend(_ context.Context, args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) { e.logger.Debug("eth_resend", "args", args.String()) - if args.Nonce == nil { - return common.Hash{}, fmt.Errorf("missing transaction nonce in transaction spec") - } - - args, err := e.backend.SetTxDefaults(args) - if err != nil { - return common.Hash{}, err - } - - matchTx := args.ToTransaction().AsTransaction() - - // Before replacing the old transaction, ensure the _new_ transaction fee is reasonable. - price := matchTx.GasPrice() - if gasPrice != nil { - price = gasPrice.ToInt() - } - gas := matchTx.Gas() - if gasLimit != nil { - gas = uint64(*gasLimit) - } - if err := rpctypes.CheckTxFee(price, gas, e.backend.RPCTxFeeCap()); err != nil { - return common.Hash{}, err - } - - pending, err := e.backend.PendingTransactions() - if err != nil { - return common.Hash{}, err - } - - for _, tx := range pending { - // FIXME does Resend api possible at all? https://github.com/evmos/ethermint/issues/905 - p, err := evmtypes.UnwrapEthereumMsg(tx, common.Hash{}) - if err != nil { - // not valid ethereum tx - continue - } - - pTx := p.AsTransaction() - - wantSigHash := e.signer.Hash(matchTx) - pFrom, err := ethtypes.Sender(e.signer, pTx) - if err != nil { - continue - } - - if pFrom == *args.From && e.signer.Hash(pTx) == wantSigHash { - // Match. Re-sign and send the transaction. - if gasPrice != nil && (*big.Int)(gasPrice).Sign() != 0 { - args.GasPrice = gasPrice - } - if gasLimit != nil && *gasLimit != 0 { - args.Gas = gasLimit - } - - return e.backend.SendTransaction(args) // TODO: this calls SetTxDefaults again, refactor to avoid calling it twice - } - } - - return common.Hash{}, fmt.Errorf("transaction %#x not found", matchTx.Hash()) -} - -// Call performs a raw contract call. -func (e *PublicAPI) Call(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, _ *rpctypes.StateOverride) (hexutil.Bytes, error) { - e.logger.Debug("eth_call", "args", args.String(), "block number or hash", blockNrOrHash) - - blockNum, err := e.getBlockNumber(blockNrOrHash) - if err != nil { - return nil, err - } - data, err := e.doCall(args, blockNum) - if err != nil { - return []byte{}, err - } - - return (hexutil.Bytes)(data.Ret), nil -} - -// DoCall performs a simulated call operation through the evmtypes. It returns the -// estimated gas used on the operation or an error if fails. -func (e *PublicAPI) doCall( - args evmtypes.TransactionArgs, blockNr rpctypes.BlockNumber, -) (*evmtypes.MsgEthereumTxResponse, error) { - bz, err := json.Marshal(&args) - if err != nil { - return nil, err - } - - req := evmtypes.EthCallRequest{ - Args: bz, - GasCap: e.backend.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. - ctx := rpctypes.ContextWithHeight(blockNr.Int64()) - timeout := e.backend.RPCEVMTimeout() - - // Setup context so it may be canceled the call has completed - // or, in case of unmetered gas, setup a context with a timeout. - var cancel context.CancelFunc - if timeout > 0 { - ctx, cancel = context.WithTimeout(ctx, timeout) - } else { - ctx, cancel = context.WithCancel(ctx) - } - - // Make sure the context is canceled when the call has completed - // this makes sure resources are cleaned up. - defer cancel() - - res, err := e.queryClient.EthCall(ctx, &req) - if err != nil { - return nil, err - } - - if res.Failed() { - if res.VmError != vm.ErrExecutionReverted.Error() { - return nil, status.Error(codes.Internal, res.VmError) - } - return nil, evmtypes.NewExecErrorWithReason(res.Ret) - } - - return res, nil -} - -// EstimateGas returns an estimate of gas usage for the given smart contract call. -func (e *PublicAPI) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) { - e.logger.Debug("eth_estimateGas") - return e.backend.EstimateGas(args, blockNrOptional) -} - -// GetBlockByHash returns the block identified by hash. -func (e *PublicAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { - e.logger.Debug("eth_getBlockByHash", "hash", hash.Hex(), "full", fullTx) - return e.backend.GetBlockByHash(hash, fullTx) -} - -// GetBlockByNumber returns the block identified by number. -func (e *PublicAPI) GetBlockByNumber(ethBlockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) { - e.logger.Debug("eth_getBlockByNumber", "number", ethBlockNum, "full", fullTx) - return e.backend.GetBlockByNumber(ethBlockNum, fullTx) -} - -// GetTransactionByHash returns the transaction identified by hash. -func (e *PublicAPI) GetTransactionByHash(hash common.Hash) (*rpctypes.RPCTransaction, error) { - e.logger.Debug("eth_getTransactionByHash", "hash", hash.Hex()) - return e.backend.GetTransactionByHash(hash) -} - -// GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. -func (e *PublicAPI) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { - e.logger.Debug("eth_getTransactionByBlockHashAndIndex", "hash", hash.Hex(), "index", idx) - - block, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes()) - if err != nil { - e.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error()) - return nil, nil - } - - if block.Block == nil { - e.logger.Debug("block not found", "hash", hash.Hex()) - return nil, nil - } - - return e.getTransactionByBlockAndIndex(block, idx) -} - -// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. -func (e *PublicAPI) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { - e.logger.Debug("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx) - - block, err := e.backend.GetTendermintBlockByNumber(blockNum) - if err != nil { - e.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) - return nil, nil - } - - if block.Block == nil { - e.logger.Debug("block not found", "height", blockNum.Int64()) - return nil, nil - } - - return e.getTransactionByBlockAndIndex(block, idx) -} - -// getTransactionByBlockAndIndex is the common code shared by `GetTransactionByBlockNumberAndIndex` and `GetTransactionByBlockHashAndIndex`. -func (e *PublicAPI) getTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { - blockRes, err := e.backend.GetTendermintBlockResultByNumber(&block.Block.Height) - if err != nil { - return nil, nil - } - - var msg *evmtypes.MsgEthereumTx - // try /tx_search first - res, err := e.backend.GetTxByTxIndex(block.Block.Height, uint(idx)) - if err == nil { - tx, err := e.clientCtx.TxConfig.TxDecoder()(res.Tx) - if err != nil { - e.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) - return nil, nil - } - - parsedTxs, err := rpctypes.ParseTxResult(&res.TxResult) - if err != nil { - return nil, fmt.Errorf("failed to parse tx events: %d, %v", idx, err) - } - - parsedTx := parsedTxs.GetTxByTxIndex(int(idx)) - if parsedTx == nil { - return nil, fmt.Errorf("ethereum tx not found in msgs: %d", idx) - } - - var ok bool - // msgIndex is inferred from tx events, should be within bound. - msg, ok = tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) - if !ok { - e.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) - return nil, nil - } - } else { - i := int(idx) - ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(block, blockRes) - if i >= len(ethMsgs) { - e.logger.Debug("block txs index out of bound", "index", i) - return nil, nil - } - - msg = ethMsgs[i] - } - - baseFee, err := e.backend.BaseFee(blockRes) - if err != nil { - // handle the error for pruned node. - e.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", block.Block.Height, "error", err) - } - - return rpctypes.NewTransactionFromMsg( - msg, - common.BytesToHash(block.Block.Hash()), - uint64(block.Block.Height), - uint64(idx), - baseFee, - ) -} - -// GetTransactionReceipt returns the transaction receipt identified by hash. -func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { - hexTx := hash.Hex() - e.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) - - res, err := e.backend.GetTxByEthHash(hash) - if err != nil { - e.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) - return nil, nil - } - - // don't ignore the txs which exceed block gas limit. - if !backend.TxSuccessOrExceedsBlockGasLimit(&res.TxResult) { - return nil, nil - } - - parsedTxs, err := rpctypes.ParseTxResult(&res.TxResult) - if err != nil { - return nil, fmt.Errorf("failed to parse tx events: %s, %v", hexTx, err) - } - - parsedTx := parsedTxs.GetTxByHash(hash) - if parsedTx == nil { - return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) - } - - 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 - } - - tx, err := e.clientCtx.TxConfig.TxDecoder()(res.Tx) - if err != nil { - e.logger.Debug("decoding failed", "error", err.Error()) - return nil, fmt.Errorf("failed to decode tx: %w", err) - } - - if res.TxResult.Code != 0 { - // tx failed, we should return gas limit as gas used, because that's how the fee get deducted. - for i := 0; i <= parsedTx.MsgIndex; i++ { - gasLimit := tx.GetMsgs()[i].(*evmtypes.MsgEthereumTx).GetGas() - parsedTxs.Txs[i].GasUsed = gasLimit - } - } - - // the `msgIndex` is inferred from tx events, should be within the bound, - // and the tx is found by eth tx hash, so the msg type must be correct. - ethMsg := tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) - - txData, err := evmtypes.UnpackTxData(ethMsg.Data) - if err != nil { - e.logger.Error("failed to unpack tx data", "error", err.Error()) - return nil, err - } - - cumulativeGasUsed := uint64(0) - blockRes, err := e.backend.GetTendermintBlockResultByNumber(&res.Height) - if err != nil { - e.logger.Debug("failed to retrieve block results", "height", res.Height, "error", err.Error()) - return nil, nil - } - for i := 0; i < int(res.Index) && i < len(blockRes.TxsResults); i++ { - cumulativeGasUsed += uint64(blockRes.TxsResults[i].GasUsed) - } - cumulativeGasUsed += parsedTxs.AccumulativeGasUsed(parsedTx.MsgIndex) - - // Get the transaction result from the log - var status hexutil.Uint - if res.TxResult.Code != 0 || parsedTx.Failed { - status = hexutil.Uint(ethtypes.ReceiptStatusFailed) - } else { - status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful) - } - - from, err := ethMsg.GetSender(e.chainIDEpoch) - if err != nil { - return nil, err - } - - // parse tx logs from events - logs, err := parsedTx.ParseTxLogs() - if err != nil { - e.logger.Debug("failed to parse logs", "hash", hexTx, "error", err.Error()) - } - - if parsedTx.EthTxIndex == -1 { - // Fallback to find tx index by iterating all valid eth transactions - msgs := e.backend.GetEthereumMsgsFromTendermintBlock(resBlock, blockRes) - for i := range msgs { - if msgs[i].Hash == hexTx { - parsedTx.EthTxIndex = int64(i) - break - } - } - } - - if parsedTx.EthTxIndex == -1 { - return nil, errors.New("can't find index of ethereum tx") - } - - receipt := map[string]interface{}{ - // Consensus fields: These fields are defined by the Yellow Paper - "status": status, - "cumulativeGasUsed": hexutil.Uint64(cumulativeGasUsed), - "logsBloom": ethtypes.BytesToBloom(ethtypes.LogsBloom(logs)), - "logs": logs, - - // Implementation fields: These fields are added by geth when processing a transaction. - // They are stored in the chain database. - "transactionHash": hash, - "contractAddress": nil, - "gasUsed": hexutil.Uint64(parsedTx.GasUsed), - - // Inclusion information: These fields provide information about the inclusion of the - // transaction corresponding to this receipt. - "blockHash": common.BytesToHash(resBlock.Block.Header.Hash()).Hex(), - "blockNumber": hexutil.Uint64(res.Height), - "transactionIndex": hexutil.Uint64(parsedTx.EthTxIndex), - - // sender and receiver (contract or EOA) addreses - "from": from, - "to": txData.GetTo(), - } - - if logs == nil { - receipt["logs"] = [][]*ethtypes.Log{} - } - - // If the ContractAddress is 20 0x0 bytes, assume it is not a contract creation - if txData.GetTo() == nil { - receipt["contractAddress"] = crypto.CreateAddress(from, txData.GetNonce()) - } - - if dynamicTx, ok := txData.(*evmtypes.DynamicFeeTx); ok { - baseFee, err := e.backend.BaseFee(blockRes) - if err != nil { - // tolerate the error for pruned node. - e.logger.Error("fetch basefee failed, node is pruned?", "height", res.Height, "error", err) - } else { - receipt["effectiveGasPrice"] = hexutil.Big(*dynamicTx.EffectiveGasPrice(baseFee)) - } - } - - return receipt, nil + return e.backend.Resend(args, gasPrice, gasLimit) } // GetPendingTransactions returns the transactions that are in the transaction pool @@ -1026,127 +545,3 @@ func (e *PublicAPI) GetPendingTransactions() ([]*rpctypes.RPCTransaction, error) return result, nil } - -// GetUncleByBlockHashAndIndex returns the uncle identified by hash and index. Always returns nil. -func (e *PublicAPI) GetUncleByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) map[string]interface{} { - return nil -} - -// GetUncleByBlockNumberAndIndex returns the uncle identified by number and index. Always returns nil. -func (e *PublicAPI) GetUncleByBlockNumberAndIndex(number, idx hexutil.Uint) map[string]interface{} { - return nil -} - -// GetProof returns an account object with proof and any storage proofs -func (e *PublicAPI) GetProof(address common.Address, storageKeys []string, blockNrOrHash rpctypes.BlockNumberOrHash) (*rpctypes.AccountResult, error) { - e.logger.Debug("eth_getProof", "address", address.Hex(), "keys", storageKeys, "block number or hash", blockNrOrHash) - - blockNum, err := e.getBlockNumber(blockNrOrHash) - if err != nil { - return nil, err - } - - height := blockNum.Int64() - _, err = e.backend.GetTendermintBlockByNumber(blockNum) - if err != nil { - // Get 'latest' proof if query is in the future - // this imitates geth behavior - height = 0 - } - ctx := rpctypes.ContextWithHeight(height) - - // if the height is equal to zero, meaning the query condition of the block is either "pending" or "latest" - if height == 0 { - bn, err := e.backend.BlockNumber() - if err != nil { - return nil, err - } - - if bn > math.MaxInt64 { - return nil, fmt.Errorf("not able to query block number greater than MaxInt64") - } - - height = int64(bn) - } - - clientCtx := e.clientCtx.WithHeight(height) - - // query storage proofs - storageProofs := make([]rpctypes.StorageResult, len(storageKeys)) - - for i, key := range storageKeys { - hexKey := common.HexToHash(key) - valueBz, proof, err := e.queryClient.GetProof(clientCtx, evmtypes.StoreKey, evmtypes.StateKey(address, hexKey.Bytes())) - if err != nil { - return nil, err - } - - // check for proof - var proofStr string - if proof != nil { - proofStr = proof.String() - } - - storageProofs[i] = rpctypes.StorageResult{ - Key: key, - Value: (*hexutil.Big)(new(big.Int).SetBytes(valueBz)), - Proof: []string{proofStr}, - } - } - - // query EVM account - req := &evmtypes.QueryAccountRequest{ - Address: address.String(), - } - - res, err := e.queryClient.Account(ctx, req) - if err != nil { - return nil, err - } - - // query account proofs - accountKey := authtypes.AddressStoreKey(sdk.AccAddress(address.Bytes())) - _, proof, err := e.queryClient.GetProof(clientCtx, authtypes.StoreKey, accountKey) - if err != nil { - return nil, err - } - - // check for proof - var accProofStr string - if proof != nil { - accProofStr = proof.String() - } - - balance, ok := sdkmath.NewIntFromString(res.Balance) - if !ok { - return nil, errors.New("invalid balance") - } - - return &rpctypes.AccountResult{ - Address: address, - AccountProof: []string{accProofStr}, - Balance: (*hexutil.Big)(balance.BigInt()), - CodeHash: common.HexToHash(res.CodeHash), - Nonce: hexutil.Uint64(res.Nonce), - StorageHash: common.Hash{}, // NOTE: Ethermint doesn't have a storage hash. TODO: implement? - StorageProof: storageProofs, - }, nil -} - -// getBlockNumber returns the BlockNumber from BlockNumberOrHash -func (e *PublicAPI) getBlockNumber(blockNrOrHash rpctypes.BlockNumberOrHash) (rpctypes.BlockNumber, error) { - switch { - case blockNrOrHash.BlockHash == nil && blockNrOrHash.BlockNumber == nil: - return rpctypes.EthEarliestBlockNumber, fmt.Errorf("types BlockHash and BlockNumber cannot be both nil") - case blockNrOrHash.BlockHash != nil: - blockNumber, err := e.backend.GetBlockNumberByHash(*blockNrOrHash.BlockHash) - if err != nil { - return rpctypes.EthEarliestBlockNumber, err - } - return rpctypes.NewBlockNumber(blockNumber), nil - case blockNrOrHash.BlockNumber != nil: - return *blockNrOrHash.BlockNumber, nil - default: - return rpctypes.EthEarliestBlockNumber, nil - } -} diff --git a/rpc/namespaces/ethereum/eth/filters/api.go b/rpc/namespaces/ethereum/eth/filters/api.go index a998e6f7..669580a5 100644 --- a/rpc/namespaces/ethereum/eth/filters/api.go +++ b/rpc/namespaces/ethereum/eth/filters/api.go @@ -23,6 +23,17 @@ import ( evmtypes "github.com/evmos/ethermint/x/evm/types" ) +// FilterAPI gathers +type FilterAPI interface { + GetLogs(ctx context.Context, crit filters.FilterCriteria) ([]*ethtypes.Log, error) + GetFilterChanges(id rpc.ID) (interface{}, error) + GetFilterLogs(ctx context.Context, id rpc.ID) ([]*ethtypes.Log, error) + NewBlockFilter() rpc.ID + NewFilter(criteria filters.FilterCriteria) (rpc.ID, error) + NewPendingTransactionFilter() rpc.ID + UninstallFilter(id rpc.ID) bool +} + // Backend defines the methods requided by the PublicFilterAPI backend type Backend interface { GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) diff --git a/rpc/namespaces/ethereum/miner/api.go b/rpc/namespaces/ethereum/miner/api.go index 64bcb8d0..98313b3b 100644 --- a/rpc/namespaces/ethereum/miner/api.go +++ b/rpc/namespaces/ethereum/miner/api.go @@ -1,187 +1,36 @@ package miner import ( - "math/big" - - sdkmath "cosmossdk.io/math" - - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/server" - sdkconfig "github.com/cosmos/cosmos-sdk/server/config" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - - authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" - distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/tendermint/tendermint/libs/log" - tmtypes "github.com/tendermint/tendermint/types" "github.com/evmos/ethermint/rpc/backend" - rpctypes "github.com/evmos/ethermint/rpc/types" - "github.com/evmos/ethermint/server/config" ) // API is the private miner prefixed set of APIs in the Miner JSON-RPC spec. type API struct { - ctx *server.Context - logger log.Logger - clientCtx client.Context - backend backend.EVMBackend + ctx *server.Context + logger log.Logger + backend backend.EVMBackend } // NewPrivateAPI creates an instance of the Miner API. func NewPrivateAPI( ctx *server.Context, - clientCtx client.Context, backend backend.EVMBackend, ) *API { return &API{ - ctx: ctx, - clientCtx: clientCtx, - logger: ctx.Logger.With("api", "miner"), - backend: backend, + ctx: ctx, + logger: ctx.Logger.With("api", "miner"), + backend: backend, } } // SetEtherbase sets the etherbase of the miner func (api *API) SetEtherbase(etherbase common.Address) bool { api.logger.Debug("miner_setEtherbase") - - delAddr, err := api.backend.GetCoinbase() - if err != nil { - api.logger.Debug("failed to get coinbase address", "error", err.Error()) - return false - } - - withdrawAddr := sdk.AccAddress(etherbase.Bytes()) - msg := distributiontypes.NewMsgSetWithdrawAddress(delAddr, withdrawAddr) - - if err := msg.ValidateBasic(); err != nil { - api.logger.Debug("tx failed basic validation", "error", err.Error()) - return false - } - - // Assemble transaction from fields - builder, ok := api.clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) - if !ok { - api.logger.Debug("clientCtx.TxConfig.NewTxBuilder returns unsupported builder", "error", err.Error()) - return false - } - - err = builder.SetMsgs(msg) - if err != nil { - api.logger.Error("builder.SetMsgs failed", "error", err.Error()) - return false - } - - // Fetch minimun gas price to calculate fees using the configuration. - appConf := config.GetConfig(api.ctx.Viper) - - minGasPrices := appConf.GetMinGasPrices() - if len(minGasPrices) == 0 || minGasPrices.Empty() { - api.logger.Debug("the minimun fee is not set") - return false - } - minGasPriceValue := minGasPrices[0].Amount - denom := minGasPrices[0].Denom - - delCommonAddr := common.BytesToAddress(delAddr.Bytes()) - nonce, err := api.backend.GetTransactionCount(delCommonAddr, rpctypes.EthPendingBlockNumber) - if err != nil { - api.logger.Debug("failed to get nonce", "error", err.Error()) - return false - } - - txFactory := tx.Factory{} - txFactory = txFactory. - WithChainID(api.clientCtx.ChainID). - WithKeybase(api.clientCtx.Keyring). - WithTxConfig(api.clientCtx.TxConfig). - WithSequence(uint64(*nonce)). - WithGasAdjustment(1.25) - - _, gas, err := tx.CalculateGas(api.clientCtx, txFactory, msg) - if err != nil { - api.logger.Debug("failed to calculate gas", "error", err.Error()) - return false - } - - txFactory = txFactory.WithGas(gas) - - value := new(big.Int).SetUint64(gas * minGasPriceValue.Ceil().TruncateInt().Uint64()) - fees := sdk.Coins{sdk.NewCoin(denom, sdkmath.NewIntFromBigInt(value))} - builder.SetFeeAmount(fees) - builder.SetGasLimit(gas) - - keyInfo, err := api.clientCtx.Keyring.KeyByAddress(delAddr) - if err != nil { - api.logger.Debug("failed to get the wallet address using the keyring", "error", err.Error()) - return false - } - - if err := tx.Sign(txFactory, keyInfo.Name, builder, false); err != nil { - api.logger.Debug("failed to sign tx", "error", err.Error()) - return false - } - - // Encode transaction by default Tx encoder - txEncoder := api.clientCtx.TxConfig.TxEncoder() - txBytes, err := txEncoder(builder.GetTx()) - if err != nil { - api.logger.Debug("failed to encode eth tx using default encoder", "error", err.Error()) - return false - } - - tmHash := common.BytesToHash(tmtypes.Tx(txBytes).Hash()) - - // Broadcast transaction in sync mode (default) - // NOTE: If error is encountered on the node, the broadcast will not return an error - syncCtx := api.clientCtx.WithBroadcastMode(flags.BroadcastSync) - rsp, err := syncCtx.BroadcastTx(txBytes) - if rsp != nil && rsp.Code != 0 { - err = sdkerrors.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) - } - if err != nil { - api.logger.Debug("failed to broadcast tx", "error", err.Error()) - return false - } - - api.logger.Debug("broadcasted tx to set miner withdraw address (etherbase)", "hash", tmHash.String()) - return true -} - -// SetGasPrice sets the minimum accepted gas price for the miner. -// NOTE: this function accepts only integers to have the same interface than go-eth -// to use float values, the gas prices must be configured using the configuration file -func (api *API) SetGasPrice(gasPrice hexutil.Big) bool { - api.logger.Info(api.ctx.Viper.ConfigFileUsed()) - appConf := config.GetConfig(api.ctx.Viper) - - var unit string - minGasPrices := appConf.GetMinGasPrices() - - // fetch the base denom from the sdk Config in case it's not currently defined on the node config - if len(minGasPrices) == 0 || minGasPrices.Empty() { - var err error - unit, err = sdk.GetBaseDenom() - if err != nil { - api.logger.Debug("could not get the denom of smallest unit registered", "error", err.Error()) - return false - } - } else { - unit = minGasPrices[0].Denom - } - - c := sdk.NewDecCoin(unit, sdkmath.NewIntFromBigInt(gasPrice.ToInt())) - - appConf.SetMinGasPrices(sdk.DecCoins{c}) - sdkconfig.WriteConfigFile(api.ctx.Viper.ConfigFileUsed(), appConf) - api.logger.Info("Your configuration file was modified. Please RESTART your node.", "gas-price", c.String()) - return true + return api.backend.SetEtherbase(etherbase) } diff --git a/rpc/namespaces/ethereum/personal/api.go b/rpc/namespaces/ethereum/personal/api.go index feb6d348..0b9c09ec 100644 --- a/rpc/namespaces/ethereum/personal/api.go +++ b/rpc/namespaces/ethereum/personal/api.go @@ -8,14 +8,11 @@ import ( "github.com/evmos/ethermint/rpc/backend" - "github.com/cosmos/cosmos-sdk/client" - "github.com/evmos/ethermint/crypto/hd" ethermint "github.com/evmos/ethermint/types" "github.com/tendermint/tendermint/libs/log" - sdkcrypto "github.com/cosmos/cosmos-sdk/crypto" "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" @@ -24,13 +21,11 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" - "github.com/evmos/ethermint/crypto/ethsecp256k1" evmtypes "github.com/evmos/ethermint/x/evm/types" ) // PrivateAccountAPI is the personal_ prefixed set of APIs in the Web3 JSON-RPC spec. type PrivateAccountAPI struct { - clientCtx client.Context backend backend.EVMBackend logger log.Logger hdPathIter ethermint.HDPathIterator @@ -39,7 +34,6 @@ type PrivateAccountAPI struct { // NewAPI creates an instance of the public Personal Eth API. func NewAPI( logger log.Logger, - clientCtx client.Context, backend backend.EVMBackend, ) *PrivateAccountAPI { cfg := sdk.GetConfig() @@ -51,7 +45,6 @@ func NewAPI( } return &PrivateAccountAPI{ - clientCtx: clientCtx, logger: logger.With("api", "personal"), hdPathIter: iterator, backend: backend, @@ -65,55 +58,13 @@ func NewAPI( // NOTE: The key will be both armored and encrypted using the same passphrase. func (api *PrivateAccountAPI) ImportRawKey(privkey, password string) (common.Address, error) { api.logger.Debug("personal_importRawKey") - priv, err := crypto.HexToECDSA(privkey) - if err != nil { - return common.Address{}, err - } - - privKey := ðsecp256k1.PrivKey{Key: crypto.FromECDSA(priv)} - - addr := sdk.AccAddress(privKey.PubKey().Address().Bytes()) - ethereumAddr := common.BytesToAddress(addr) - - // return if the key has already been imported - if _, err := api.clientCtx.Keyring.KeyByAddress(addr); err == nil { - return ethereumAddr, nil - } - - // ignore error as we only care about the length of the list - list, _ := api.clientCtx.Keyring.List() - privKeyName := fmt.Sprintf("personal_%d", len(list)) - - armor := sdkcrypto.EncryptArmorPrivKey(privKey, password, ethsecp256k1.KeyType) - - if err := api.clientCtx.Keyring.ImportPrivKey(privKeyName, armor, password); err != nil { - return common.Address{}, err - } - - api.logger.Info("key successfully imported", "name", privKeyName, "address", ethereumAddr.String()) - - return ethereumAddr, nil + return api.backend.ImportRawKey(privkey, password) } // ListAccounts will return a list of addresses for accounts this node manages. func (api *PrivateAccountAPI) ListAccounts() ([]common.Address, error) { api.logger.Debug("personal_listAccounts") - addrs := []common.Address{} - - list, err := api.clientCtx.Keyring.List() - if err != nil { - return nil, err - } - - for _, info := range list { - pubKey, err := info.GetPubKey() - if err != nil { - return nil, err - } - addrs = append(addrs, common.BytesToAddress(pubKey.Address())) - } - - return addrs, nil + return api.backend.ListAccounts() } // LockAccount will lock the account associated with the given address when it's unlocked. @@ -134,7 +85,7 @@ func (api *PrivateAccountAPI) NewAccount(password string) (common.Address, error // create the mnemonic and save the account hdPath := api.hdPathIter() - info, _, err := api.clientCtx.Keyring.NewMnemonic(name, keyring.English, hdPath.String(), password, hd.EthSecp256k1) + info, err := api.backend.NewMnemonic(name, keyring.English, hdPath.String(), password, hd.EthSecp256k1) if err != nil { return common.Address{}, err } @@ -164,19 +115,6 @@ func (api *PrivateAccountAPI) UnlockAccount(_ context.Context, addr common.Addre // able to decrypt the key it fails. func (api *PrivateAccountAPI) SendTransaction(_ context.Context, args evmtypes.TransactionArgs, _ string) (common.Hash, error) { api.logger.Debug("personal_sendTransaction", "address", args.To.String()) - - if args.From == nil { - return common.Hash{}, fmt.Errorf("from address cannot be nil in send transaction") - } - - addr := sdk.AccAddress(args.From.Bytes()) - - // check if the key is on the keyring - _, err := api.clientCtx.Keyring.KeyByAddress(addr) - if err != nil { - return common.Hash{}, err - } - return api.backend.SendTransaction(args) } @@ -191,17 +129,7 @@ func (api *PrivateAccountAPI) SendTransaction(_ context.Context, args evmtypes.T // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign func (api *PrivateAccountAPI) Sign(_ context.Context, data hexutil.Bytes, addr common.Address, _ string) (hexutil.Bytes, error) { api.logger.Debug("personal_sign", "data", data, "address", addr.String()) - - cosmosAddr := sdk.AccAddress(addr.Bytes()) - - sig, _, err := api.clientCtx.Keyring.SignByAddress(cosmosAddr, accounts.TextHash(data)) - if err != nil { - api.logger.Error("failed to sign with key", "data", data, "address", addr.String(), "error", err.Error()) - return nil, err - } - - sig[crypto.RecoveryIDOffset] += 27 // transform V from 0/1 to 27/28 - return sig, nil + return api.backend.Sign(addr, data) } // EcRecover returns the address for the account that was used to create the signature. diff --git a/rpc/websockets.go b/rpc/websockets.go index e859d919..97a57acd 100644 --- a/rpc/websockets.go +++ b/rpc/websockets.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "math/big" "net" "net/http" @@ -301,7 +301,7 @@ func (s *websocketsServer) tcpGetAndSendResponse(wsConn *wsConn, mb []byte) erro defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrap(err, "could not read body from response") }