From 514785bd89b7a6cd75327762c316ae844124135c Mon Sep 17 00:00:00 2001 From: yihuang Date: Fri, 17 Dec 2021 06:30:22 +0800 Subject: [PATCH] rpc: optimize tx index lookup (#810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: #760 Solution: - emit tx index to cosmos events - rpc side try to use the events, but fallback to heavier approach when fails. Update rpc/ethereum/namespaces/eth/api.go changelog fix lint fix TxIndexFromEvents fix Update rpc/ethereum/backend/backend.go Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> --- CHANGELOG.md | 1 + rpc/ethereum/backend/backend.go | 111 +++++++++++++++++++-------- rpc/ethereum/namespaces/eth/api.go | 118 +++++++++++++++++------------ rpc/ethereum/types/utils.go | 25 ++++++ x/evm/keeper/msg_server.go | 4 + x/evm/spec/07_events.md | 1 + x/evm/types/events.go | 1 + 7 files changed, 182 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6739d79..7002086f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (app) [tharsis#794](https://github.com/tharsis/ethermint/pull/794) Setup in-place store migrators. * (ci) [tharsis#784](https://github.com/tharsis/ethermint/pull/784) Enable automatic backport of PRs. * (rpc) [tharsis#786](https://github.com/tharsis/ethermint/pull/786) Improve error message of `SendTransaction`/`SendRawTransaction` JSON-RPC APIs. +* (rpc) [tharsis#810](https://github.com/tharsis/ethermint/pull/810) Optimize tx index lookup in web3 rpc ### Bug Fixes diff --git a/rpc/ethereum/backend/backend.go b/rpc/ethereum/backend/backend.go index 77ce8f65..913990cb 100644 --- a/rpc/ethereum/backend/backend.go +++ b/rpc/ethereum/backend/backend.go @@ -73,6 +73,7 @@ type Backend interface { GetCoinbase() (sdk.AccAddress, error) GetTransactionByHash(txHash common.Hash) (*types.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(height int64) (*big.Int, error) @@ -83,7 +84,7 @@ type Backend interface { GetFilteredBlocks(from int64, to int64, filter [][]filters.BloomIV, filterAddresses bool) ([]int64, error) ChainConfig() *params.ChainConfig SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) - GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock) []*evmtypes.MsgEthereumTx + GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evmtypes.MsgEthereumTx } var _ Backend = (*EVMBackend)(nil) @@ -688,27 +689,57 @@ func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransac return nil, nil } - resBlock, err := e.clientCtx.Client.Block(e.ctx, &res.Height) + if res.TxResult.Code != 0 { + return nil, errors.New("invalid ethereum tx") + } + + tx, err := e.clientCtx.TxConfig.TxDecoder()(res.Tx) + if err != nil { + return nil, err + } + + if len(tx.GetMsgs()) != 1 { + return nil, errors.New("invalid ethereum tx") + } + + msg, ok := tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx) + if !ok { + return nil, errors.New("invalid ethereum tx") + } + + block, 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 + return nil, err } - var txIndex uint64 - msgs := e.GetEthereumMsgsFromTendermintBlock(resBlock) - - for i := range msgs { - if msgs[i].Hash == hexTx { - txIndex = uint64(i) - break + // Try to find txIndex from events + found := false + txIndex, err := types.TxIndexFromEvents(res.TxResult.Events) + if err == nil { + found = true + } else { + // Fallback to find tx index by iterating all valid eth transactions + blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, &block.Block.Height) + if err != nil { + return nil, nil + } + msgs := e.GetEthereumMsgsFromTendermintBlock(block, blockRes) + for i := range msgs { + if msgs[i].Hash == hexTx { + txIndex = uint64(i) + found = true + break + } } } - - msg := msgs[txIndex] + if !found { + return nil, errors.New("can't find index of ethereum tx") + } return types.NewTransactionFromMsg( msg, - common.BytesToHash(resBlock.Block.Hash()), + common.BytesToHash(block.BlockID.Hash.Bytes()), uint64(res.Height), txIndex, e.chainID, @@ -730,6 +761,22 @@ func (e *EVMBackend) GetTxByEthHash(hash common.Hash) (*tmrpctypes.ResultTx, err return resTxs.Txs[0], nil } +// GetTxByTxIndex uses `/tx_query` to find transaction by tx index of valid ethereum txs +func (e *EVMBackend) 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 := e.clientCtx.Client.TxSearch(e.ctx, query, false, nil, nil, "") + if err != nil { + return nil, err + } + if len(resTxs.Txs) == 0 { + return nil, errors.Errorf("ethereum tx not found for block %d index %d", height, index) + } + return resTxs.Txs[0], nil +} + func (e *EVMBackend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) { // Look up the wallet containing the requested signer _, err := e.clientCtx.Keyring.KeyByAddress(sdk.AccAddress(args.From.Bytes())) @@ -1021,32 +1068,34 @@ BLOCKS: // GetEthereumMsgsFromTendermintBlock returns all real MsgEthereumTxs from a Tendermint block. // It also ensures consistency over the correct txs indexes across RPC endpoints -func (e *EVMBackend) GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock) []*evmtypes.MsgEthereumTx { +func (e *EVMBackend) GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evmtypes.MsgEthereumTx { + // nolint: prealloc var result []*evmtypes.MsgEthereumTx - for _, tx := range block.Block.Txs { + txResults := blockRes.TxsResults + + for i, tx := range block.Block.Txs { + // check tx exists on EVM by cross checking with blockResults + if txResults[i].Code != 0 { + e.logger.Debug("invalid tx result code", "cosmos-hash", hexutil.Encode(tx.Hash())) + continue + } + tx, err := e.clientCtx.TxConfig.TxDecoder()(tx) if err != nil { e.logger.Debug("failed to decode transaction in block", "height", block.Block.Height, "error", err.Error()) continue } - - for _, msg := range tx.GetMsgs() { - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - continue - } - - hash := ethMsg.AsTransaction().Hash() - // check tx exists on EVM and has the correct block height - ethTx, err := e.GetTxByEthHash(hash) - if err != nil || ethTx.Height != block.Block.Height { - e.logger.Debug("failed to query eth tx hash", "hash", hash.Hex()) - continue - } - - result = append(result, ethMsg) + if len(tx.GetMsgs()) != 1 { + continue } + + ethMsg, ok := tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx) + if !ok { + continue + } + + result = append(result, ethMsg) } return result diff --git a/rpc/ethereum/namespaces/eth/api.go b/rpc/ethereum/namespaces/eth/api.go index 3b4d3af9..6608883b 100644 --- a/rpc/ethereum/namespaces/eth/api.go +++ b/rpc/ethereum/namespaces/eth/api.go @@ -17,6 +17,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/viper" "github.com/tendermint/tendermint/libs/log" + tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -309,18 +310,23 @@ func (e *PublicAPI) GetTransactionCount(address common.Address, blockNrOrHash rp func (e *PublicAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { e.logger.Debug("eth_getBlockTransactionCountByHash", "hash", hash.Hex()) - resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes()) + 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 resBlock.Block == nil { + if block.Block == nil { e.logger.Debug("block not found", "hash", hash.Hex()) return nil } - ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(resBlock) + blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, &block.Block.Height) + if err != nil { + return nil + } + + ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(block, blockRes) n := hexutil.Uint(len(ethMsgs)) return &n } @@ -328,18 +334,23 @@ func (e *PublicAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Ui // 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()) - resBlock, err := e.clientCtx.Client.Block(e.ctx, blockNum.TmHeight()) + block, err := e.clientCtx.Client.Block(e.ctx, blockNum.TmHeight()) if err != nil { e.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) return nil } - if resBlock.Block == nil { + if block.Block == nil { e.logger.Debug("block not found", "height", blockNum.Int64()) return nil } - ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(resBlock) + blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, &block.Block.Height) + if err != nil { + return nil + } + + ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(block, blockRes) n := hexutil.Uint(len(ethMsgs)) return &n } @@ -664,75 +675,86 @@ func (e *PublicAPI) GetTransactionByHash(hash common.Hash) (*rpctypes.RPCTransac return e.backend.GetTransactionByHash(hash) } +// getTransactionByBlockAndIndex is the common code shared by `GetTransactionByBlockNumberAndIndex` and `GetTransactionByBlockHashAndIndex`. +func (e *PublicAPI) getTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + 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 + } + if len(tx.GetMsgs()) != 1 { + e.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) + return nil, nil + } + var ok bool + msg, ok = tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx) + if !ok { + e.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) + return nil, nil + } + } else { + blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, &block.Block.Height) + if err != nil { + return nil, nil + } + + 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] + } + + return rpctypes.NewTransactionFromMsg( + msg, + common.BytesToHash(block.Block.Hash()), + uint64(block.Block.Height), + uint64(idx), + e.chainIDEpoch, + ) +} + // 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) - resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes()) + 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 resBlock.Block == nil { + if block.Block == nil { e.logger.Debug("block not found", "hash", hash.Hex()) return nil, nil } - i := int(idx) - ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(resBlock) - 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(resBlock.Block.Height) - if err != nil { - return nil, err - } - - return rpctypes.NewTransactionFromMsg( - msg, - hash, - uint64(resBlock.Block.Height), - uint64(idx), - baseFee, - ) + 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) - resBlock, err := e.clientCtx.Client.Block(e.ctx, blockNum.TmHeight()) + block, err := e.clientCtx.Client.Block(e.ctx, blockNum.TmHeight()) if err != nil { e.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) return nil, nil } - if resBlock.Block == nil { + if block.Block == nil { e.logger.Debug("block not found", "height", blockNum.Int64()) return nil, nil } - i := int(idx) - ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(resBlock) - if i >= len(ethMsgs) { - e.logger.Debug("block txs index out of bound", "index", i) - return nil, nil - } - - msg := ethMsgs[i] - - return rpctypes.NewTransactionFromMsg( - msg, - common.BytesToHash(resBlock.Block.Hash()), - uint64(resBlock.Block.Height), - uint64(idx), - e.chainIDEpoch, - ) + return e.getTransactionByBlockAndIndex(block, idx) } // GetTransactionReceipt returns the transaction receipt identified by hash. @@ -801,7 +823,7 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac // get eth index based on block's txs var txIndex uint64 - msgs := e.backend.GetEthereumMsgsFromTendermintBlock(resBlock) + msgs := e.backend.GetEthereumMsgsFromTendermintBlock(resBlock, blockRes) for i := range msgs { if msgs[i].Hash == hexTx { txIndex = uint64(i) diff --git a/rpc/ethereum/types/utils.go b/rpc/ethereum/types/utils.go index 4bfcd6ca..add10668 100644 --- a/rpc/ethereum/types/utils.go +++ b/rpc/ethereum/types/utils.go @@ -4,8 +4,10 @@ import ( "bytes" "context" "encoding/hex" + "errors" "fmt" "math/big" + "strconv" abci "github.com/tendermint/tendermint/abci/types" tmtypes "github.com/tendermint/tendermint/types" @@ -244,3 +246,26 @@ func BaseFeeFromEvents(events []abci.Event) *big.Int { } return nil } + +// TxIndexFromEvents parses the tx index from cosmos events +func TxIndexFromEvents(events []abci.Event) (uint64, error) { + for _, event := range events { + if event.Type != evmtypes.EventTypeEthereumTx { + continue + } + + for _, attr := range event.Attributes { + if bytes.Equal(attr.Key, []byte(evmtypes.AttributeKeyTxIndex)) { + result, err := strconv.ParseInt(string(attr.Value), 10, 64) + if err != nil { + return 0, err + } + if result < 0 { + return 0, errors.New("negative tx index") + } + return uint64(result), nil + } + } + } + return 0, errors.New("not found") +} diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 52a02f4b..bc052cd7 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" tmbytes "github.com/tendermint/tendermint/libs/bytes" tmtypes "github.com/tendermint/tendermint/types" @@ -26,6 +27,7 @@ func (k *Keeper) EthereumTx(goCtx context.Context, msg *types.MsgEthereumTx) (*t sender := msg.From tx := msg.AsTransaction() + txIndex := k.GetTxIndexTransient() response, err := k.ApplyTransaction(tx) if err != nil { @@ -36,6 +38,8 @@ func (k *Keeper) EthereumTx(goCtx context.Context, msg *types.MsgEthereumTx) (*t sdk.NewAttribute(sdk.AttributeKeyAmount, tx.Value().String()), // add event for ethereum transaction hash format sdk.NewAttribute(types.AttributeKeyEthereumTxHash, response.Hash), + // add event for index of valid ethereum tx + sdk.NewAttribute(types.AttributeKeyTxIndex, strconv.FormatInt(int64(txIndex), 10)), } if len(ctx.TxBytes()) > 0 { diff --git a/x/evm/spec/07_events.md b/x/evm/spec/07_events.md index e487e225..dc3e6a0a 100644 --- a/x/evm/spec/07_events.md +++ b/x/evm/spec/07_events.md @@ -15,6 +15,7 @@ The `x/evm` module emits the Cosmos SDK events after a state execution. The EVM | ethereum_tx | `"contract"` | `{hex_address}` | | ethereum_tx | `"txHash"` | `{tendermint_hex_hash}` | | ethereum_tx | `"ethereumTxHash"` | `{hex_hash}` | +| ethereum_tx | `"txIndex"` | `{tx_index}` | | tx_log | `"txLog"` | `{tx_log}` | | message | `"sender"` | `{eth_address}` | | message | `"action"` | `"ethereum"` | diff --git a/x/evm/types/events.go b/x/evm/types/events.go index 3eada906..0b8ca69a 100644 --- a/x/evm/types/events.go +++ b/x/evm/types/events.go @@ -10,6 +10,7 @@ const ( AttributeKeyRecipient = "recipient" AttributeKeyTxHash = "txHash" AttributeKeyEthereumTxHash = "ethereumTxHash" + AttributeKeyTxIndex = "txIndex" AttributeKeyTxType = "txType" AttributeKeyTxLog = "txLog" // tx failed in eth vm execution