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, ) }