From 39ab2dd96383c5e837ec9638cf9a73f819247fd7 Mon Sep 17 00:00:00 2001 From: yihuang Date: Tue, 31 May 2022 18:26:40 +0800 Subject: [PATCH] fix(rpc, ante): Emit Ethereum tx hash in `AnteHandler` to support query failed transactions (#1062) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Emit eth tx hash in ante handler to support query failed transactions WIP: #1045 Solution: - emit eth tx hash in ante handler - modify rpc to use it fix ante handler support failed tx in receipt add unit tests need to patch cosmos-sdk to work update cosmos-sdk to v0.45.x release branch fix failed status fix unit tests add unit test cases cleanup dead code Apply suggestions from code review Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> fix lint fix review suggestions fix build fix gas used of failed tx add back the redundant events * fix get tx by index * add unit tests for events * Update rpc/types/events.go Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> * update comments * refactoring * Update rpc/namespaces/ethereum/eth/api.go * fix lint * Apply suggestions from code review Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> --- CHANGELOG.md | 1 + app/ante/eth.go | 34 +++ app/ante/handler_options.go | 1 + app/ante/interfaces.go | 1 + go.mod | 2 +- go.sum | 4 +- rpc/ethereum/backend/backend.go | 43 ++-- rpc/ethereum/backend/utils.go | 21 ++ rpc/ethereum/namespaces/debug/api.go | 12 +- rpc/ethereum/namespaces/eth/api.go | 102 +++++---- rpc/ethereum/types/events.go | 236 ++++++++++++++++++++ rpc/ethereum/types/events_test.go | 321 +++++++++++++++++++++++++++ rpc/ethereum/types/utils.go | 104 --------- 13 files changed, 708 insertions(+), 174 deletions(-) create mode 100644 rpc/ethereum/types/events.go create mode 100644 rpc/ethereum/types/events_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5c63efb8..2207e2a379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (rpc) [tharsis#1059](https://github.com/tharsis/ethermint/pull/1059) Remove unnecessary event filtering logic on the `eth_baseFee` JSON-RPC endpoint. * (rpc) [tharsis#1082](https://github.com/tharsis/ethermint/pull/1082) fix gas price returned in getTransaction api. +* (ante) [tharsis#1062](https://github.com/tharsis/ethermint/pull/1062) Emit event of eth tx hash in ante handler to support query failed transactions. ### API Breaking diff --git a/app/ante/eth.go b/app/ante/eth.go index 5cadad99fa..8eadaf0d10 100644 --- a/app/ante/eth.go +++ b/app/ante/eth.go @@ -3,6 +3,7 @@ package ante import ( "errors" "math/big" + "strconv" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -535,3 +536,36 @@ func (mfd EthMempoolFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulat return next(ctx, tx, simulate) } + +// EthEmitEventDecorator emit events in ante handler in case of tx execution failed (out of block gas limit). +type EthEmitEventDecorator struct { + evmKeeper EVMKeeper +} + +// NewEthEmitEventDecorator creates a new EthEmitEventDecorator +func NewEthEmitEventDecorator(evmKeeper EVMKeeper) EthEmitEventDecorator { + return EthEmitEventDecorator{evmKeeper} +} + +// AnteHandle emits some basic events for the eth messages +func (eeed EthEmitEventDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + // After eth tx passed ante handler, the fee is deducted and nonce increased, it shouldn't be ignored by json-rpc, + // we need to emit some basic events at the very end of ante handler to be indexed by tendermint. + txIndex := eeed.evmKeeper.GetTxIndexTransient(ctx) + for i, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + + // emit ethereum tx hash as event, should be indexed by tm tx indexer for query purpose. + // it's emitted in ante handler so we can query failed transaction (out of block gas limit). + ctx.EventManager().EmitEvent(sdk.NewEvent( + evmtypes.EventTypeEthereumTx, + sdk.NewAttribute(evmtypes.AttributeKeyEthereumTxHash, msgEthTx.Hash), + sdk.NewAttribute(evmtypes.AttributeKeyTxIndex, strconv.FormatUint(txIndex+uint64(i), 10)), + )) + } + + return next(ctx, tx, simulate) +} diff --git a/app/ante/handler_options.go b/app/ante/handler_options.go index 7664b33236..f065e6882c 100644 --- a/app/ante/handler_options.go +++ b/app/ante/handler_options.go @@ -57,6 +57,7 @@ func newEthAnteHandler(options HandlerOptions) sdk.AnteHandler { NewEthGasConsumeDecorator(options.EvmKeeper, options.MaxTxGasWanted), NewCanTransferDecorator(options.EvmKeeper), NewEthIncrementSenderSequenceDecorator(options.AccountKeeper), // innermost AnteDecorator. + NewEthEmitEventDecorator(options.EvmKeeper), // emit eth tx hash and index at the very last ante handler. ) } diff --git a/app/ante/interfaces.go b/app/ante/interfaces.go index 95c299b070..68478dbf20 100644 --- a/app/ante/interfaces.go +++ b/app/ante/interfaces.go @@ -26,6 +26,7 @@ type EVMKeeper interface { GetBaseFee(ctx sdk.Context, ethCfg *params.ChainConfig) *big.Int GetBalance(ctx sdk.Context, addr common.Address) *big.Int ResetTransientGasUsed(ctx sdk.Context) + GetTxIndexTransient(ctx sdk.Context) uint64 } type protoTxProvider interface { diff --git a/go.mod b/go.mod index f77d99a68b..9a73299ac0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.17 require ( github.com/btcsuite/btcd v0.22.0-beta github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce - github.com/cosmos/cosmos-sdk v0.45.4 + github.com/cosmos/cosmos-sdk v0.45.5-0.20220523154235-2921a1c3c918 github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/ibc-go/v2 v2.2.0 github.com/davecgh/go-spew v1.1.1 diff --git a/go.sum b/go.sum index e9cb826a1b..2e660529cf 100644 --- a/go.sum +++ b/go.sum @@ -253,8 +253,8 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cosmos/btcutil v1.0.4 h1:n7C2ngKXo7UC9gNyMNLbzqz7Asuf+7Qv4gnX/rOdQ44= github.com/cosmos/btcutil v1.0.4/go.mod h1:Ffqc8Hn6TJUdDgHBwIZLtrLQC1KdJ9jGJl/TvgUaxbU= github.com/cosmos/cosmos-sdk v0.45.1/go.mod h1:XXS/asyCqWNWkx2rW6pSuen+EVcpAFxq6khrhnZgHaQ= -github.com/cosmos/cosmos-sdk v0.45.4 h1:eStDAhJdMY8n5arbBRe+OwpNeBSunxSBHp1g55ulfdA= -github.com/cosmos/cosmos-sdk v0.45.4/go.mod h1:WOqtDxN3eCCmnYLVla10xG7lEXkFjpTaqm2a2WasgCc= +github.com/cosmos/cosmos-sdk v0.45.5-0.20220523154235-2921a1c3c918 h1:adHQCXXYYLO+VxH9aSifiKofXwOwRUBx0lxny5fKQCg= +github.com/cosmos/cosmos-sdk v0.45.5-0.20220523154235-2921a1c3c918/go.mod h1:WOqtDxN3eCCmnYLVla10xG7lEXkFjpTaqm2a2WasgCc= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= diff --git a/rpc/ethereum/backend/backend.go b/rpc/ethereum/backend/backend.go index 63a014d88c..185d37d569 100644 --- a/rpc/ethereum/backend/backend.go +++ b/rpc/ethereum/backend/backend.go @@ -373,8 +373,8 @@ func (e *EVMBackend) EthBlockFromTendermint( for _, txsResult := range resBlockResult.TxsResults { // workaround for cosmos-sdk bug. https://github.com/cosmos/cosmos-sdk/issues/10832 - if txsResult.GetCode() == 11 && txsResult.GetLog() == "no block gas left to run tx: out of gas" { - // block gas limit has exceeded, other txs must have failed for the same reason. + if ShouldIgnoreGasUsed(txsResult) { + // block gas limit has exceeded, other txs must have failed with same reason. break } gasUsed += uint64(txsResult.GetGasUsed()) @@ -534,6 +534,7 @@ func (e *EVMBackend) GetCoinbase() (sdk.AccAddress, error) { func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransaction, error) { res, err := e.GetTxByEthHash(txHash) hexTx := txHash.Hex() + if err != nil { // try to find tx in mempool txs, err := e.PendingTransactions() @@ -568,12 +569,17 @@ func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransac return nil, nil } - if res.TxResult.Code != 0 { + if !TxSuccessOrExceedsBlockGasLimit(&res.TxResult) { return nil, errors.New("invalid ethereum tx") } - msgIndex, attrs := types.FindTxAttributes(res.TxResult.Events, hexTx) - if msgIndex < 0 { + 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) } @@ -583,7 +589,7 @@ func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransac } // the `msgIndex` is inferred from tx events, should be within the bound. - msg, ok := tx.GetMsgs()[msgIndex].(*evmtypes.MsgEthereumTx) + msg, ok := tx.GetMsgs()[parsedTx.MsgIndex].(*evmtypes.MsgEthereumTx) if !ok { return nil, errors.New("invalid ethereum tx") } @@ -594,12 +600,7 @@ func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransac return nil, err } - // Try to find txIndex from events - found := false - txIndex, err := types.GetUint64Attribute(attrs, evmtypes.AttributeKeyTxIndex) - if err == nil { - found = true - } else { + if parsedTx.EthTxIndex == -1 { // 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 { @@ -608,22 +609,27 @@ func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransac msgs := e.GetEthereumMsgsFromTendermintBlock(block, blockRes) for i := range msgs { if msgs[i].Hash == hexTx { - txIndex = uint64(i) - found = true + parsedTx.EthTxIndex = int64(i) break } } } - if !found { + if parsedTx.EthTxIndex == -1 { return nil, errors.New("can't find index of ethereum tx") } + baseFee, err := e.BaseFee(block.Block.Height) + if err != nil { + e.logger.Debug("HeaderByHash BaseFee failed", "height", block.Block.Height, "error", err.Error()) + return nil, err + } + return types.NewTransactionFromMsg( msg, common.BytesToHash(block.BlockID.Hash.Bytes()), uint64(res.Height), - txIndex, - nil, + uint64(parsedTx.EthTxIndex), + baseFee, ) } @@ -902,7 +908,8 @@ func (e *EVMBackend) GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.Result for i, tx := range block.Block.Txs { // check tx exists on EVM by cross checking with blockResults - if txResults[i].Code != 0 { + // include the tx that exceeds block gas limit + if !TxSuccessOrExceedsBlockGasLimit(txResults[i]) { e.logger.Debug("invalid tx result code", "cosmos-hash", hexutil.Encode(tx.Hash())) continue } diff --git a/rpc/ethereum/backend/utils.go b/rpc/ethereum/backend/utils.go index 16e129f02f..07337c7384 100644 --- a/rpc/ethereum/backend/utils.go +++ b/rpc/ethereum/backend/utils.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math/big" + "strings" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -19,6 +20,10 @@ import ( evmtypes "github.com/tharsis/ethermint/x/evm/types" ) +// ExceedBlockGasLimitError defines the error message when tx execution exceeds the block gas limit. +// The tx fee is deducted in ante handler, so it shouldn't be ignored in JSON-RPC API. +const ExceedBlockGasLimitError = "out of gas in location: block gas meter; gasWanted:" + // SetTxDefaults populates tx message with default values in case they are not // provided on the args func (e *EVMBackend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) { @@ -251,3 +256,19 @@ func ParseTxLogsFromEvent(event abci.Event) ([]*ethtypes.Log, error) { } return evmtypes.LogsToEthereum(logs), nil } + +// TxExceedBlockGasLimit returns true if the tx exceeds block gas limit. +func TxExceedBlockGasLimit(res *abci.ResponseDeliverTx) bool { + return strings.Contains(res.Log, ExceedBlockGasLimitError) +} + +// TxSuccessOrExceedsBlockGasLimit returns if the tx should be included in json-rpc responses +func TxSuccessOrExceedsBlockGasLimit(res *abci.ResponseDeliverTx) bool { + return res.Code == 0 || TxExceedBlockGasLimit(res) +} + +// ShouldIgnoreGasUsed returns true if the gasUsed in result should be ignored +// workaround for issue: https://github.com/cosmos/cosmos-sdk/issues/10832 +func ShouldIgnoreGasUsed(res *abci.ResponseDeliverTx) bool { + return res.GetCode() == 11 && strings.Contains(res.GetLog(), "no block gas left to run tx: out of gas") +} diff --git a/rpc/ethereum/namespaces/debug/api.go b/rpc/ethereum/namespaces/debug/api.go index 5dacaeb2aa..42ab14ff56 100644 --- a/rpc/ethereum/namespaces/debug/api.go +++ b/rpc/ethereum/namespaces/debug/api.go @@ -89,8 +89,12 @@ func (a *API) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) ( return nil, err } - msgIndex, _ := rpctypes.FindTxAttributes(transaction.TxResult.Events, hash.Hex()) - if msgIndex < 0 { + 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()) } @@ -124,7 +128,7 @@ func (a *API) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) ( } // add predecessor messages in current cosmos tx - for i := 0; i < msgIndex; i++ { + for i := 0; i < parsedTx.MsgIndex; i++ { ethMsg, ok := tx.GetMsgs()[i].(*evmtypes.MsgEthereumTx) if !ok { continue @@ -132,7 +136,7 @@ func (a *API) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) ( predecessors = append(predecessors, ethMsg) } - ethMessage, ok := tx.GetMsgs()[msgIndex].(*evmtypes.MsgEthereumTx) + 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) diff --git a/rpc/ethereum/namespaces/eth/api.go b/rpc/ethereum/namespaces/eth/api.go index 741fd3a5e3..e681663812 100644 --- a/rpc/ethereum/namespaces/eth/api.go +++ b/rpc/ethereum/namespaces/eth/api.go @@ -401,12 +401,23 @@ func (e *PublicAPI) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, err return nil, nil } - msgIndex, _ := rpctypes.FindTxAttributes(res.TxResult.Events, hexTx) - if msgIndex < 0 { + if res.TxResult.Code != 0 { + // failed, return empty logs + 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(txHash) + if parsedTx == nil { return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) } + // parse tx logs from events - return backend.TxLogsFromEvents(res.TxResult.Events, msgIndex) + return parsedTx.ParseTxLogs() } // Sign signs the provided data using the private key of address via Geth's signature standard. @@ -704,15 +715,20 @@ func (e *PublicAPI) getTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, e.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) return nil, nil } - // find msg index in events - msgIndex := rpctypes.FindTxAttributesByIndex(res.TxResult.Events, uint64(idx)) - if msgIndex < 0 { - 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()[msgIndex].(*evmtypes.MsgEthereumTx) + 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 @@ -794,8 +810,18 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac return nil, nil } - msgIndex, attrs := rpctypes.FindTxAttributes(res.TxResult.Events, hexTx) - if msgIndex < 0 { + // 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) } @@ -811,14 +837,18 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac return nil, fmt.Errorf("failed to decode tx: %w", err) } - // the `msgIndex` is inferred from tx events, should be within the bound. - msg := tx.GetMsgs()[msgIndex] - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - e.logger.Debug(fmt.Sprintf("invalid tx type: %T", msg)) - return nil, fmt.Errorf("invalid tx type: %T", msg) + 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()) @@ -831,27 +861,14 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac 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 += rpctypes.AccumulativeGasUsedOfMsg(res.TxResult.Events, msgIndex) - - var gasUsed uint64 - if len(tx.GetMsgs()) == 1 { - // backward compatibility - gasUsed = uint64(res.TxResult.GasUsed) - } else { - gasUsed, err = rpctypes.GetUint64Attribute(attrs, evmtypes.AttributeKeyTxGasUsed) - if err != nil { - return nil, err - } - } + cumulativeGasUsed += parsedTxs.AccumulativeGasUsed(parsedTx.MsgIndex) // Get the transaction result from the log - _, found := attrs[evmtypes.AttributeKeyEthereumTxFailed] var status hexutil.Uint - if found { + if res.TxResult.Code != 0 || parsedTx.Failed { status = hexutil.Uint(ethtypes.ReceiptStatusFailed) } else { status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful) @@ -863,28 +880,23 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac } // parse tx logs from events - logs, err := backend.TxLogsFromEvents(res.TxResult.Events, msgIndex) + logs, err := parsedTx.ParseTxLogs() if err != nil { - e.logger.Debug("logs not found", "hash", hexTx, "error", err.Error()) + e.logger.Debug("failed to parse logs", "hash", hexTx, "error", err.Error()) } - // Try to find txIndex from events - found = false - txIndex, err := rpctypes.GetUint64Attribute(attrs, evmtypes.AttributeKeyTxIndex) - if err == nil { - found = true - } else { + 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 { - txIndex = uint64(i) - found = true + parsedTx.EthTxIndex = int64(i) break } } } - if !found { + + if parsedTx.EthTxIndex == -1 { return nil, errors.New("can't find index of ethereum tx") } @@ -899,14 +911,14 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac // They are stored in the chain database. "transactionHash": hash, "contractAddress": nil, - "gasUsed": hexutil.Uint64(gasUsed), + "gasUsed": hexutil.Uint64(parsedTx.GasUsed), "type": hexutil.Uint(txData.TxType()), // 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(txIndex), + "transactionIndex": hexutil.Uint64(parsedTx.EthTxIndex), // sender and receiver (contract or EOA) addreses "from": from, diff --git a/rpc/ethereum/types/events.go b/rpc/ethereum/types/events.go new file mode 100644 index 0000000000..68324b1dad --- /dev/null +++ b/rpc/ethereum/types/events.go @@ -0,0 +1,236 @@ +package types + +import ( + "encoding/json" + "strconv" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + abci "github.com/tendermint/tendermint/abci/types" + evmtypes "github.com/tharsis/ethermint/x/evm/types" +) + +// EventFormat is the format version of the events. +// +// To fix the issue of tx exceeds block gas limit, we changed the event format in a breaking way. +// But to avoid forcing clients to re-sync from scatch, we make json-rpc logic to be compatible with both formats. +type EventFormat int + +const ( + eventFormatUnknown EventFormat = iota + + // Event Format 1 (the format used before PR #1062): + // ``` + // ethereum_tx(amount, ethereumTxHash, [txIndex, txGasUsed], txHash, [receipient], ethereumTxFailed) + // tx_log(txLog, txLog, ...) + // ethereum_tx(amount, ethereumTxHash, [txIndex, txGasUsed], txHash, [receipient], ethereumTxFailed) + // tx_log(txLog, txLog, ...) + // ... + // ``` + eventFormat1 + + // Event Format 2 (the format used after PR #1062): + // ``` + // ethereum_tx(ethereumTxHash, txIndex) + // ethereum_tx(ethereumTxHash, txIndex) + // ... + // ethereum_tx(amount, ethereumTxHash, txIndex, txGasUsed, txHash, [receipient], ethereumTxFailed) + // tx_log(txLog, txLog, ...) + // ethereum_tx(amount, ethereumTxHash, txIndex, txGasUsed, txHash, [receipient], ethereumTxFailed) + // tx_log(txLog, txLog, ...) + // ... + // ``` + // If the transaction exceeds block gas limit, it only emits the first part. + eventFormat2 +) + +// ParsedTx is the tx infos parsed from events. +type ParsedTx struct { + MsgIndex int + + // the following fields are parsed from events + + Hash common.Hash + // -1 means uninitialized + EthTxIndex int64 + GasUsed uint64 + Failed bool + // unparsed tx log json strings + RawLogs [][]byte +} + +// NewParsedTx initialize a ParsedTx +func NewParsedTx(msgIndex int) ParsedTx { + return ParsedTx{MsgIndex: msgIndex, EthTxIndex: -1} +} + +// ParseTxLogs decode the raw logs into ethereum format. +func (p ParsedTx) ParseTxLogs() ([]*ethtypes.Log, error) { + logs := make([]*evmtypes.Log, 0, len(p.RawLogs)) + for _, raw := range p.RawLogs { + var log evmtypes.Log + if err := json.Unmarshal(raw, &log); err != nil { + return nil, err + } + + logs = append(logs, &log) + } + return evmtypes.LogsToEthereum(logs), nil +} + +// ParsedTxs is the tx infos parsed from eth tx events. +type ParsedTxs struct { + // one item per message + Txs []ParsedTx + // map tx hash to msg index + TxHashes map[common.Hash]int +} + +// ParseTxResult parse eth tx infos from cosmos-sdk events. +// It supports two event formats, the formats are described in the comments of the format constants. +func ParseTxResult(result *abci.ResponseDeliverTx) (*ParsedTxs, error) { + format := eventFormatUnknown + // the index of current ethereum_tx event in format 1 or the second part of format 2 + eventIndex := -1 + + p := &ParsedTxs{ + TxHashes: make(map[common.Hash]int), + } + for _, event := range result.Events { + switch event.Type { + case evmtypes.EventTypeEthereumTx: + if format == eventFormatUnknown { + // discover the format version by inspect the first ethereum_tx event. + if len(event.Attributes) > 2 { + format = eventFormat1 + } else { + format = eventFormat2 + } + } + + if len(event.Attributes) == 2 { + // the first part of format 2 + if err := p.newTx(event.Attributes); err != nil { + return nil, err + } + } else { + // format 1 or second part of format 2 + eventIndex++ + if format == eventFormat1 { + // append tx + if err := p.newTx(event.Attributes); err != nil { + return nil, err + } + } else { + // the second part of format 2, update tx fields + if err := p.updateTx(eventIndex, event.Attributes); err != nil { + return nil, err + } + } + } + case evmtypes.EventTypeTxLog: + // reuse the eventIndex set by previous ethereum_tx event + p.Txs[eventIndex].RawLogs = parseRawLogs(event.Attributes) + } + } + + // some old versions miss some events, fill it with tx result + if len(p.Txs) == 1 { + p.Txs[0].GasUsed = uint64(result.GasUsed) + } + + return p, nil +} + +// newTx parse a new tx from events, called during parsing. +func (p *ParsedTxs) newTx(attrs []abci.EventAttribute) error { + msgIndex := len(p.Txs) + tx := NewParsedTx(msgIndex) + if err := fillTxAttributes(&tx, attrs); err != nil { + return err + } + p.Txs = append(p.Txs, tx) + p.TxHashes[tx.Hash] = msgIndex + return nil +} + +// updateTx updates an exiting tx from events, called during parsing. +func (p *ParsedTxs) updateTx(eventIndex int, attrs []abci.EventAttribute) error { + return fillTxAttributes(&p.Txs[eventIndex], attrs) +} + +// GetTxByHash find ParsedTx by tx hash, returns nil if not exists. +func (p *ParsedTxs) GetTxByHash(hash common.Hash) *ParsedTx { + if idx, ok := p.TxHashes[hash]; ok { + return &p.Txs[idx] + } + return nil +} + +// GetTxByMsgIndex returns ParsedTx by msg index +func (p *ParsedTxs) GetTxByMsgIndex(i int) *ParsedTx { + if i < 0 || i >= len(p.Txs) { + return nil + } + return &p.Txs[i] +} + +// GetTxByTxIndex returns ParsedTx by tx index +func (p *ParsedTxs) GetTxByTxIndex(txIndex int) *ParsedTx { + if len(p.Txs) == 0 { + return nil + } + // assuming the `EthTxIndex` increase continuously, + // convert TxIndex to MsgIndex by subtract the begin TxIndex. + msgIndex := txIndex - int(p.Txs[0].EthTxIndex) + // GetTxByMsgIndex will check the bound + return p.GetTxByMsgIndex(msgIndex) +} + +// AccumulativeGasUsed calculates the accumulated gas used within the batch of txs +func (p *ParsedTxs) AccumulativeGasUsed(msgIndex int) (result uint64) { + for i := 0; i <= msgIndex; i++ { + result += p.Txs[i].GasUsed + } + return result +} + +// fillTxAttribute parse attributes by name, less efficient than hardcode the index, but more stable against event +// format changes. +func fillTxAttribute(tx *ParsedTx, key []byte, value []byte) error { + switch string(key) { + case evmtypes.AttributeKeyEthereumTxHash: + tx.Hash = common.HexToHash(string(value)) + case evmtypes.AttributeKeyTxIndex: + txIndex, err := strconv.ParseInt(string(value), 10, 64) + if err != nil { + return err + } + tx.EthTxIndex = txIndex + case evmtypes.AttributeKeyTxGasUsed: + gasUsed, err := strconv.ParseInt(string(value), 10, 64) + if err != nil { + return err + } + tx.GasUsed = uint64(gasUsed) + case evmtypes.AttributeKeyEthereumTxFailed: + tx.Failed = len(value) > 0 + } + return nil +} + +func fillTxAttributes(tx *ParsedTx, attrs []abci.EventAttribute) error { + for _, attr := range attrs { + if err := fillTxAttribute(tx, attr.Key, attr.Value); err != nil { + return err + } + } + return nil +} + +func parseRawLogs(attrs []abci.EventAttribute) (logs [][]byte) { + for _, attr := range attrs { + logs = append(logs, attr.Value) + } + return logs +} diff --git a/rpc/ethereum/types/events_test.go b/rpc/ethereum/types/events_test.go new file mode 100644 index 0000000000..55f8a361ff --- /dev/null +++ b/rpc/ethereum/types/events_test.go @@ -0,0 +1,321 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + evmtypes "github.com/tharsis/ethermint/x/evm/types" +) + +func TestParseTxResult(t *testing.T) { + rawLogs := [][]byte{ + []byte("{\"address\":\"0xdcC261c03cD2f33eBea404318Cdc1D9f8b78e1AD\",\"topics\":[\"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\",\"0x000000000000000000000000569608516a81c0b1247310a3e0cd001046da0663\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANB/GezJGOI=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":5}"), + []byte("{\"address\":\"0x569608516A81C0B1247310A3E0CD001046dA0663\",\"topics\":[\"0xe2403640ba68fed3a2f88b7557551d1993f84b99bb10ff833f0cf8db0c5e0486\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANB/GezJGOI=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":6}"), + []byte("{\"address\":\"0x569608516A81C0B1247310A3E0CD001046dA0663\",\"topics\":[\"0xf279e6a1f5e320cca91135676d9cb6e44ca8a08c0b88342bcdb1144f6511b568\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\",\"0x0000000000000000000000000000000000000000000000000000000000000001\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":7}"), + } + address := "0x57f96e6B86CdeFdB3d412547816a82E3E0EbF9D2" + txHash := common.BigToHash(big.NewInt(1)) + txHash2 := common.BigToHash(big.NewInt(2)) + + testCases := []struct { + name string + response abci.ResponseDeliverTx + expTxs []*ParsedTx // expected parse result, nil means expect error. + }{ + {"format 1 events", + abci.ResponseDeliverTx{ + GasUsed: 21000, + Events: []abci.Event{ + {Type: "coin_received", Attributes: []abci.EventAttribute{ + {Key: []byte("receiver"), Value: []byte("ethm12luku6uxehhak02py4rcz65zu0swh7wjun6msa")}, + {Key: []byte("amount"), Value: []byte("1252860basetcro")}, + }}, + {Type: "coin_spent", Attributes: []abci.EventAttribute{ + {Key: []byte("spender"), Value: []byte("ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl")}, + {Key: []byte("amount"), Value: []byte("1252860basetcro")}, + }}, + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash.Hex())}, + {Key: []byte("txIndex"), Value: []byte("10")}, + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + }}, + {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{ + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[0]}, + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[1]}, + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[2]}, + }}, + {Type: "message", Attributes: []abci.EventAttribute{ + {Key: []byte("action"), Value: []byte("/ethermint.evm.v1.MsgEthereumTx")}, + {Key: []byte("key"), Value: []byte("ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl")}, + {Key: []byte("module"), Value: []byte("evm")}, + {Key: []byte("sender"), Value: []byte(address)}, + }}, + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash2.Hex())}, + {Key: []byte("txIndex"), Value: []byte("11")}, + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + {Key: []byte("ethereumTxFailed"), Value: []byte("contract reverted")}, + }}, + {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{}}, + }, + }, + []*ParsedTx{ + { + MsgIndex: 0, + Hash: txHash, + EthTxIndex: 10, + GasUsed: 21000, + Failed: false, + RawLogs: rawLogs, + }, + { + MsgIndex: 1, + Hash: txHash2, + EthTxIndex: 11, + GasUsed: 21000, + Failed: true, + RawLogs: nil, + }, + }, + }, + {"format 2 events", + abci.ResponseDeliverTx{ + GasUsed: 21000, + Events: []abci.Event{ + {Type: "coin_received", Attributes: []abci.EventAttribute{ + {Key: []byte("receiver"), Value: []byte("ethm12luku6uxehhak02py4rcz65zu0swh7wjun6msa")}, + {Key: []byte("amount"), Value: []byte("1252860basetcro")}, + }}, + {Type: "coin_spent", Attributes: []abci.EventAttribute{ + {Key: []byte("spender"), Value: []byte("ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl")}, + {Key: []byte("amount"), Value: []byte("1252860basetcro")}, + }}, + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash.Hex())}, + {Key: []byte("txIndex"), Value: []byte("0")}, + }}, + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + }}, + {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{ + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[0]}, + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[1]}, + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[2]}, + }}, + {Type: "message", Attributes: []abci.EventAttribute{ + {Key: []byte("action"), Value: []byte("/ethermint.evm.v1.MsgEthereumTx")}, + {Key: []byte("key"), Value: []byte("ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl")}, + {Key: []byte("module"), Value: []byte("evm")}, + {Key: []byte("sender"), Value: []byte(address)}, + }}, + }, + }, + []*ParsedTx{ + { + MsgIndex: 0, + Hash: txHash, + EthTxIndex: 0, + GasUsed: 21000, + Failed: false, + RawLogs: rawLogs, + }, + }, + }, + {"format 1 events, failed", + abci.ResponseDeliverTx{ + GasUsed: 21000, + Events: []abci.Event{ + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash.Hex())}, + {Key: []byte("txIndex"), Value: []byte("10")}, + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + }}, + {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{ + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[0]}, + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[1]}, + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[2]}, + }}, + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash2.Hex())}, + {Key: []byte("txIndex"), Value: []byte("0x01")}, + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + {Key: []byte("ethereumTxFailed"), Value: []byte("contract reverted")}, + }}, + {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{}}, + }, + }, + nil, + }, + {"format 1 events, failed", + abci.ResponseDeliverTx{ + GasUsed: 21000, + Events: []abci.Event{ + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash.Hex())}, + {Key: []byte("txIndex"), Value: []byte("10")}, + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + }}, + {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{ + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[0]}, + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[1]}, + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[2]}, + }}, + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash2.Hex())}, + {Key: []byte("txIndex"), Value: []byte("10")}, + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("0x01")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + {Key: []byte("ethereumTxFailed"), Value: []byte("contract reverted")}, + }}, + {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{}}, + }, + }, + nil, + }, + {"format 2 events failed", + abci.ResponseDeliverTx{ + GasUsed: 21000, + Events: []abci.Event{ + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash.Hex())}, + {Key: []byte("txIndex"), Value: []byte("0x01")}, + }}, + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + }}, + }, + }, + nil, + }, + {"format 2 events failed", + abci.ResponseDeliverTx{ + GasUsed: 21000, + Events: []abci.Event{ + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash.Hex())}, + {Key: []byte("txIndex"), Value: []byte("10")}, + }}, + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("0x01")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + }}, + }, + }, + nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + parsed, err := ParseTxResult(&tc.response) + if tc.expTxs == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + for msgIndex, expTx := range tc.expTxs { + require.Equal(t, expTx, parsed.GetTxByMsgIndex(msgIndex)) + require.Equal(t, expTx, parsed.GetTxByHash(expTx.Hash)) + require.Equal(t, expTx, parsed.GetTxByTxIndex(int(expTx.EthTxIndex))) + _, err := expTx.ParseTxLogs() + require.NoError(t, err) + } + // non-exists tx hash + require.Nil(t, parsed.GetTxByHash(common.Hash{})) + // out of range + require.Nil(t, parsed.GetTxByMsgIndex(len(tc.expTxs))) + require.Nil(t, parsed.GetTxByTxIndex(99999999)) + } + }) + } +} + +func TestParseTxLogs(t *testing.T) { + rawLogs := [][]byte{ + []byte("{\"address\":\"0xdcC261c03cD2f33eBea404318Cdc1D9f8b78e1AD\",\"topics\":[\"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\",\"0x000000000000000000000000569608516a81c0b1247310a3e0cd001046da0663\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANB/GezJGOI=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":5}"), + []byte("{\"address\":\"0x569608516A81C0B1247310A3E0CD001046dA0663\",\"topics\":[\"0xe2403640ba68fed3a2f88b7557551d1993f84b99bb10ff833f0cf8db0c5e0486\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANB/GezJGOI=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":6}"), + []byte("{\"address\":\"0x569608516A81C0B1247310A3E0CD001046dA0663\",\"topics\":[\"0xf279e6a1f5e320cca91135676d9cb6e44ca8a08c0b88342bcdb1144f6511b568\",\"0x0000000000000000000000002eea2c1ae0cdd2622381c2f9201b2a07c037b1f6\",\"0x0000000000000000000000000000000000000000000000000000000000000001\"],\"data\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\",\"blockNumber\":1803258,\"transactionHash\":\"0xcf4354b55b9ac77436cf8b2f5c229ad3b3119b5196cd79ac5c6c382d9f7b0a71\",\"transactionIndex\":1,\"blockHash\":\"0xa69a510b0848180a094904ea9ae3f0ca2216029470c8e03e6941b402aba610d8\",\"logIndex\":7}"), + } + address := "0x57f96e6B86CdeFdB3d412547816a82E3E0EbF9D2" + txHash := common.BigToHash(big.NewInt(1)) + txHash2 := common.BigToHash(big.NewInt(2)) + response := abci.ResponseDeliverTx{ + GasUsed: 21000, + Events: []abci.Event{ + {Type: "coin_received", Attributes: []abci.EventAttribute{ + {Key: []byte("receiver"), Value: []byte("ethm12luku6uxehhak02py4rcz65zu0swh7wjun6msa")}, + {Key: []byte("amount"), Value: []byte("1252860basetcro")}, + }}, + {Type: "coin_spent", Attributes: []abci.EventAttribute{ + {Key: []byte("spender"), Value: []byte("ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl")}, + {Key: []byte("amount"), Value: []byte("1252860basetcro")}, + }}, + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash.Hex())}, + {Key: []byte("txIndex"), Value: []byte("10")}, + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + }}, + {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{ + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[0]}, + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[1]}, + {Key: []byte(evmtypes.AttributeKeyTxLog), Value: rawLogs[2]}, + }}, + {Type: "message", Attributes: []abci.EventAttribute{ + {Key: []byte("action"), Value: []byte("/ethermint.evm.v1.MsgEthereumTx")}, + {Key: []byte("key"), Value: []byte("ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl")}, + {Key: []byte("module"), Value: []byte("evm")}, + {Key: []byte("sender"), Value: []byte(address)}, + }}, + {Type: evmtypes.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ + {Key: []byte("ethereumTxHash"), Value: []byte(txHash2.Hex())}, + {Key: []byte("txIndex"), Value: []byte("11")}, + {Key: []byte("amount"), Value: []byte("1000")}, + {Key: []byte("txGasUsed"), Value: []byte("21000")}, + {Key: []byte("txHash"), Value: []byte("14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57")}, + {Key: []byte("recipient"), Value: []byte("0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7")}, + {Key: []byte("ethereumTxFailed"), Value: []byte("contract reverted")}, + }}, + {Type: evmtypes.EventTypeTxLog, Attributes: []abci.EventAttribute{}}, + }, + } + parsed, err := ParseTxResult(&response) + require.NoError(t, err) + tx1 := parsed.GetTxByMsgIndex(0) + txLogs1, err := tx1.ParseTxLogs() + require.NoError(t, err) + require.NotEmpty(t, txLogs1) + + tx2 := parsed.GetTxByMsgIndex(1) + txLogs2, err := tx2.ParseTxLogs() + require.Empty(t, txLogs2) +} diff --git a/rpc/ethereum/types/utils.go b/rpc/ethereum/types/utils.go index df09c6edbd..e6eaa9154c 100644 --- a/rpc/ethereum/types/utils.go +++ b/rpc/ethereum/types/utils.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "math/big" - "strconv" abci "github.com/tendermint/tendermint/abci/types" tmtypes "github.com/tendermint/tendermint/types" @@ -222,106 +221,3 @@ func BaseFeeFromEvents(events []abci.Event) *big.Int { } return nil } - -// FindTxAttributes returns the msg index of the eth tx in cosmos tx, and the attributes, -// returns -1 and nil if not found. -func FindTxAttributes(events []abci.Event, txHash string) (int, map[string]string) { - msgIndex := -1 - for _, event := range events { - if event.Type != evmtypes.EventTypeEthereumTx { - continue - } - - msgIndex++ - - value := FindAttribute(event.Attributes, []byte(evmtypes.AttributeKeyEthereumTxHash)) - if !bytes.Equal(value, []byte(txHash)) { - continue - } - - // found, convert attributes to map for later lookup - attrs := make(map[string]string, len(event.Attributes)) - for _, attr := range event.Attributes { - attrs[string(attr.Key)] = string(attr.Value) - } - return msgIndex, attrs - } - // not found - return -1, nil -} - -// FindTxAttributesByIndex search the msg in tx events by txIndex -// returns the msgIndex, returns -1 if not found. -func FindTxAttributesByIndex(events []abci.Event, txIndex uint64) int { - strIndex := []byte(strconv.FormatUint(txIndex, 10)) - txIndexKey := []byte(evmtypes.AttributeKeyTxIndex) - msgIndex := -1 - for _, event := range events { - if event.Type != evmtypes.EventTypeEthereumTx { - continue - } - - msgIndex++ - - value := FindAttribute(event.Attributes, txIndexKey) - if !bytes.Equal(value, strIndex) { - continue - } - - // found, convert attributes to map for later lookup - return msgIndex - } - // not found - return -1 -} - -// FindAttribute find event attribute with specified key, if not found returns nil. -func FindAttribute(attrs []abci.EventAttribute, key []byte) []byte { - for _, attr := range attrs { - if !bytes.Equal(attr.Key, key) { - continue - } - return attr.Value - } - return nil -} - -// GetUint64Attribute parses the uint64 value from event attributes -func GetUint64Attribute(attrs map[string]string, key string) (uint64, error) { - value, found := attrs[key] - if !found { - return 0, fmt.Errorf("tx index attribute not found: %s", key) - } - var result int64 - result, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return 0, err - } - if result < 0 { - return 0, fmt.Errorf("negative tx index: %d", result) - } - return uint64(result), nil -} - -// AccumulativeGasUsedOfMsg accumulate the gas used by msgs before `msgIndex`. -func AccumulativeGasUsedOfMsg(events []abci.Event, msgIndex int) (gasUsed uint64) { - for _, event := range events { - if event.Type != evmtypes.EventTypeEthereumTx { - continue - } - - if msgIndex < 0 { - break - } - msgIndex-- - - value := FindAttribute(event.Attributes, []byte(evmtypes.AttributeKeyTxGasUsed)) - var result int64 - result, err := strconv.ParseInt(string(value), 10, 64) - if err != nil { - continue - } - gasUsed += uint64(result) - } - return -}