diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b18bf764..75c76e42f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - [\#774](https://github.com/cosmos/evm/pull/774) Emit proper allowance amount in erc20 event. - [\#790](https://github.com/cosmos/evm/pull/790) fix panic in historical query due to missing EvmCoinInfo. - [\#800](https://github.com/cosmos/evm/pull/800) Fix denom exponent validation in virtual fee deduct in vm module. +- [\#812](https://github.com/cosmos/evm/pull/812) Patch evm tx index and log indexes, cleanup EmitTxHashEvent and ResetTransientGasUsed. - [\#817](https://github.com/cosmos/evm/pull/817) Align GetCoinbaseAddress to handle empty proposer address in contexts like CheckTx where proposer doesn't exist. - [\#814](https://github.com/cosmos/evm/pull/814) Fix duplicated events in post tx processor. - [\#816](https://github.com/cosmos/evm/pull/816) Avoid nil pointer when RPC requests execute before evmCoinInfo initialization in PreBlock with defaultEvmCoinInfo fallback. diff --git a/ante/evm/11_emit_event.go b/ante/evm/11_emit_event.go deleted file mode 100644 index d3541d883..000000000 --- a/ante/evm/11_emit_event.go +++ /dev/null @@ -1,25 +0,0 @@ -package evm - -import ( - "strconv" - - evmtypes "github.com/cosmos/evm/x/vm/types" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// EmitTxHashEvent emits the Ethereum tx -// -// FIXME: This is Technical debt. Ideally the sdk.Tx hash should be the Ethereum -// tx hash (msg.Hash) instead of using events for indexing Eth txs. -func EmitTxHashEvent(ctx sdk.Context, msg *evmtypes.MsgEthereumTx, blockTxIndex uint64) { - // emit ethereum tx hash as an event so that it can be indexed by CometBFT for query purposes - // 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, msg.Hash().String()), - sdk.NewAttribute(evmtypes.AttributeKeyTxIndex, strconv.FormatUint(blockTxIndex, 10)), // #nosec G115 - ), - ) -} diff --git a/ante/evm/mono_decorator.go b/ante/evm/mono_decorator.go index f25ac91db..3405be0ed 100644 --- a/ante/evm/mono_decorator.go +++ b/ante/evm/mono_decorator.go @@ -265,9 +265,6 @@ func (md MonoDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, ne return ctx, err } - // Emit event unconditionally - ctx.TxIndex() will be valid during block execution - EmitTxHashEvent(ctx, ethMsg, uint64(ctx.TxIndex())) // #nosec G115 -- no overlfow here - if err := CheckTxFee(txFeeInfo, decUtils.TxFee, decUtils.TxGasLimit); err != nil { return ctx, err } diff --git a/evmd/app.go b/evmd/app.go index b92e46e55..bbdd3aef5 100644 --- a/evmd/app.go +++ b/evmd/app.go @@ -1,15 +1,21 @@ package evmd import ( + "context" "encoding/json" "errors" "fmt" "io" "os" + goruntime "runtime" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "github.com/spf13/cast" + // Force-load the tracer engines to trigger registration due to Go-Ethereum v1.10.15 changes "github.com/ethereum/go-ethereum/common" - "github.com/spf13/cast" _ "github.com/ethereum/go-ethereum/eth/tracers/js" _ "github.com/ethereum/go-ethereum/eth/tracers/native" @@ -73,6 +79,7 @@ import ( upgradetypes "cosmossdk.io/x/upgrade/types" "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/blockstm" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/grpc/cmtservice" @@ -199,6 +206,18 @@ type EVMD struct { configurator module.Configurator } +type customRunner struct { + *blockstm.STMRunner +} + +func (r *customRunner) Run(ctx context.Context, ms storetypes.MultiStore, txs [][]byte, deliverTx sdk.DeliverTxFunc) ([]*abci.ExecTxResult, error) { + results, err := r.STMRunner.Run(ctx, ms, txs, deliverTx) + if err != nil { + return nil, err + } + return evmtypes.PatchTxResponses(results), nil +} + // NewExampleApp returns a reference to an initialized EVMD. func NewExampleApp( logger log.Logger, @@ -215,13 +234,13 @@ func NewExampleApp( legacyAmino := encodingConfig.Amino interfaceRegistry := encodingConfig.InterfaceRegistry txConfig := encodingConfig.TxConfig - + txDecoder := encodingConfig.TxConfig.TxDecoder() bApp := baseapp.NewBaseApp( appName, logger, db, // use transaction decoder to support the sdk.Tx interface instead of sdk.StdTx - encodingConfig.TxConfig.TxDecoder(), + txDecoder, baseAppOptions..., ) bApp.SetCommitMultiStoreTracer(traceStore) @@ -775,6 +794,18 @@ func NewExampleApp( } } + bApp.SetBlockSTMTxRunner(&customRunner{ + STMRunner: blockstm.NewSTMRunner( + encodingConfig.TxConfig.TxDecoder(), + nonTransientKeys, + min(goruntime.GOMAXPROCS(0), goruntime.NumCPU()), + true, + func(ms storetypes.MultiStore) string { + return app.EVMKeeper.GetParams(sdk.NewContext(ms, cmtproto.Header{}, false, log.NewNopLogger())).EvmDenom + }, + ), + }) + return app } diff --git a/evmd/go.sum b/evmd/go.sum index d238ffaab..a41c95c85 100644 --- a/evmd/go.sum +++ b/evmd/go.sum @@ -986,6 +986,8 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= diff --git a/local_node.sh b/local_node.sh index c94f59993..a7e410825 100755 --- a/local_node.sh +++ b/local_node.sh @@ -272,7 +272,6 @@ if [[ $overwrite == "y" || $overwrite == "Y" ]]; then sed -i.bak 's/prometheus-retention-time = "0"/prometheus-retention-time = "1000000000000"/g' "$APP_TOML" sed -i.bak 's/enabled = false/enabled = true/g' "$APP_TOML" sed -i.bak 's/enable = false/enable = true/g' "$APP_TOML" - sed -i.bak 's/enable-indexer = false/enable-indexer = true/g' "$APP_TOML" # --------- maybe generate additional users --------- # start with provided/default list diff --git a/rpc/backend/comet_to_eth.go b/rpc/backend/comet_to_eth.go index 8b4847726..3edc62966 100644 --- a/rpc/backend/comet_to_eth.go +++ b/rpc/backend/comet_to_eth.go @@ -273,6 +273,15 @@ func (b *Backend) ReceiptsFromCometBlock( return nil, fmt.Errorf("failed to convert tx result to eth receipt: %w", err) } + if txResult.EthTxIndex == -1 { + var err error + // Fallback to find tx index by iterating all valid eth transactions + txResult.EthTxIndex, err = FindEthTxIndexByHash(ethMsg.Hash(), resBlock, blockRes, b) + if err != nil { + return nil, err + } + } + bloom := ethtypes.CreateBloom(ðtypes.Receipt{Logs: logs}) receipt := ðtypes.Receipt{ diff --git a/rpc/backend/tx_info.go b/rpc/backend/tx_info.go index 9c84198ff..ea52d60f5 100644 --- a/rpc/backend/tx_info.go +++ b/rpc/backend/tx_info.go @@ -3,7 +3,6 @@ package backend import ( "encoding/json" "fmt" - "math" "math/big" "time" @@ -59,22 +58,13 @@ func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransac } if res.EthTxIndex == -1 { + var err error // Fallback to find tx index by iterating all valid eth transactions - msgs := b.EthMsgsFromCometBlock(block, blockRes) - for i := range msgs { - if msgs[i].Hash() == txHash { - if i > math.MaxInt32 { - return nil, errors.New("tx index overflow") - } - res.EthTxIndex = int32(i) //#nosec G115 -- checked for int overflow already - break - } + res.EthTxIndex, err = FindEthTxIndexByHash(txHash, block, blockRes, b) + if err != nil { + return nil, err } } - // if we still unable to find the eth tx index, return error, shouldn't happen. - if res.EthTxIndex == -1 { - return nil, errors.New("can't find index of ethereum tx") - } baseFee, err := b.BaseFee(blockRes) if err != nil { diff --git a/rpc/backend/utils.go b/rpc/backend/utils.go index ca46109b0..fb70d3ff9 100644 --- a/rpc/backend/utils.go +++ b/rpc/backend/utils.go @@ -337,3 +337,17 @@ func GetHexProofs(proof *crypto.ProofOps) []string { } return proofs } + +// Fallback to find tx index by iterating all valid eth transactions for legacy blocks that don't have AttributeKeyTxIndex in events +func FindEthTxIndexByHash(txHash common.Hash, block *cmtrpctypes.ResultBlock, blockRes *cmtrpctypes.ResultBlockResults, b *Backend) (int32, error) { + msgs := b.EthMsgsFromCometBlock(block, blockRes) + for i := range msgs { + if msgs[i].Hash() == txHash { + if i > math.MaxInt32 { + return -1, fmt.Errorf("tx index overflow") + } + return int32(i), nil //#nosec G115 -- checked for int overflow already + } + } + return -1, fmt.Errorf("can't find index of ethereum tx") +} diff --git a/rpc/types/events.go b/rpc/types/events.go index 34761d811..f5b8d4975 100644 --- a/rpc/types/events.go +++ b/rpc/types/events.go @@ -188,6 +188,10 @@ func (p *ParsedTxs) updateTx(eventIndex int, attrs []abci.EventAttribute) error // if hash is different, index the new one too p.TxHashes[tx.Hash] = eventIndex } + // preserve EthTxIndex from the first event set by PatchTxResponses if not set in the second event from msg_server + if tx.EthTxIndex == -1 { + tx.EthTxIndex = p.Txs[eventIndex].EthTxIndex + } // override the tx because the second event is more trustworthy p.Txs[eventIndex] = tx return nil diff --git a/tests/integration/rpc/backend/test_utils.go b/tests/integration/rpc/backend/test_utils.go index 8b3aae1d2..bd1c59840 100644 --- a/tests/integration/rpc/backend/test_utils.go +++ b/tests/integration/rpc/backend/test_utils.go @@ -3,9 +3,15 @@ package backend import ( "fmt" + "github.com/ethereum/go-ethereum/common" + + abci "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/proto/tendermint/crypto" + cmtrpctypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/cometbft/cometbft/types" backend2 "github.com/cosmos/evm/rpc/backend" + evmtypes "github.com/cosmos/evm/x/vm/types" ) func mookProofs(num int, withData bool) *crypto.ProofOps { @@ -52,3 +58,194 @@ func (s *TestSuite) TestGetHexProofs() { }) } } + +func (s *TestSuite) TestFindEthTxIndexByHash() { + txHash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + tx := evmtypes.NewTx(&evmtypes.EvmTxArgs{ + ChainID: s.backend.EvmChainID, + Nonce: 0, + GasLimit: 100000, + }) + tx.From = s.from.Bytes() + txEncoder := s.backend.ClientCtx.TxConfig.TxEncoder() + builder := s.backend.ClientCtx.TxConfig.NewTxBuilder() + err := builder.SetMsgs(tx) + s.Require().NoError(err) + txBz, err := txEncoder(builder.GetTx()) + s.Require().NoError(err) + + testCases := []struct { + name string + setupMock func() (*cmtrpctypes.ResultBlock, *cmtrpctypes.ResultBlockResults) + txHash common.Hash + expectError bool + errorMsg string + expectedIdx int32 + }{ + { + name: "tx found at index 0", + setupMock: func() (*cmtrpctypes.ResultBlock, *cmtrpctypes.ResultBlockResults) { + block := &cmtrpctypes.ResultBlock{ + Block: &types.Block{ + Header: types.Header{Height: 1}, + Data: types.Data{ + Txs: []types.Tx{txBz}, + }, + }, + } + blockRes := &cmtrpctypes.ResultBlockResults{ + TxsResults: []*abci.ExecTxResult{ + {Code: 0, GasUsed: 21000}, + }, + } + return block, blockRes + }, + txHash: tx.Hash(), + expectError: false, + expectedIdx: 0, + }, + { + name: "tx not found", + setupMock: func() (*cmtrpctypes.ResultBlock, *cmtrpctypes.ResultBlockResults) { + otherTx := evmtypes.NewTx(&evmtypes.EvmTxArgs{ + ChainID: s.backend.EvmChainID, + Nonce: 1, + GasLimit: 100000, + }) + otherTx.From = s.from.Bytes() + + builder := s.backend.ClientCtx.TxConfig.NewTxBuilder() + _ = builder.SetMsgs(otherTx) + otherTxBz, _ := txEncoder(builder.GetTx()) + + block := &cmtrpctypes.ResultBlock{ + Block: &types.Block{ + Header: types.Header{Height: 1}, + Data: types.Data{ + Txs: []types.Tx{otherTxBz}, + }, + }, + } + blockRes := &cmtrpctypes.ResultBlockResults{ + TxsResults: []*abci.ExecTxResult{ + {Code: 0, GasUsed: 21000}, + }, + } + return block, blockRes + }, + txHash: txHash, // Different hash + expectError: true, + errorMsg: "can't find index of ethereum tx", + }, + { + name: "empty block", + setupMock: func() (*cmtrpctypes.ResultBlock, *cmtrpctypes.ResultBlockResults) { + block := &cmtrpctypes.ResultBlock{ + Block: &types.Block{ + Header: types.Header{Height: 1}, + Data: types.Data{Txs: []types.Tx{}}, + }, + } + blockRes := &cmtrpctypes.ResultBlockResults{ + TxsResults: []*abci.ExecTxResult{}, + } + return block, blockRes + }, + txHash: txHash, + expectError: true, + errorMsg: "can't find index of ethereum tx", + }, + { + name: "tx with failed result code", + setupMock: func() (*cmtrpctypes.ResultBlock, *cmtrpctypes.ResultBlockResults) { + block := &cmtrpctypes.ResultBlock{ + Block: &types.Block{ + Header: types.Header{Height: 1}, + Data: types.Data{ + Txs: []types.Tx{txBz}, + }, + }, + } + blockRes := &cmtrpctypes.ResultBlockResults{ + TxsResults: []*abci.ExecTxResult{ + {Code: 1, GasUsed: 21000, Log: "execution reverted"}, // Failed tx + }, + } + return block, blockRes + }, + txHash: tx.Hash(), + expectError: true, + errorMsg: "can't find index of ethereum tx", // Will be filtered out by EthMsgsFromCometBlock + }, + { + name: "multiple txs, target at index 1", + setupMock: func() (*cmtrpctypes.ResultBlock, *cmtrpctypes.ResultBlockResults) { + tx1 := evmtypes.NewTx(&evmtypes.EvmTxArgs{ + ChainID: s.backend.EvmChainID, + Nonce: 0, + GasLimit: 100000, + }) + tx1.From = s.from.Bytes() + + builder1 := s.backend.ClientCtx.TxConfig.NewTxBuilder() + _ = builder1.SetMsgs(tx1) + tx1Bz, _ := txEncoder(builder1.GetTx()) + + tx2 := evmtypes.NewTx(&evmtypes.EvmTxArgs{ + ChainID: s.backend.EvmChainID, + Nonce: 1, + GasLimit: 100000, + }) + tx2.From = s.from.Bytes() + + builder2 := s.backend.ClientCtx.TxConfig.NewTxBuilder() + _ = builder2.SetMsgs(tx2) + tx2Bz, _ := txEncoder(builder2.GetTx()) + + block := &cmtrpctypes.ResultBlock{ + Block: &types.Block{ + Header: types.Header{Height: 1}, + Data: types.Data{ + Txs: []types.Tx{tx1Bz, tx2Bz}, + }, + }, + } + blockRes := &cmtrpctypes.ResultBlockResults{ + TxsResults: []*abci.ExecTxResult{ + {Code: 0, GasUsed: 21000}, + {Code: 0, GasUsed: 21000}, + }, + } + return block, blockRes + }, + txHash: func() common.Hash { + tx2 := evmtypes.NewTx(&evmtypes.EvmTxArgs{ + ChainID: s.backend.EvmChainID, + Nonce: 1, + GasLimit: 100000, + }) + tx2.From = s.from.Bytes() + return tx2.Hash() + }(), + expectError: false, + expectedIdx: 1, + }, + } + + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + block, blockRes := tc.setupMock() + + idx, err := backend2.FindEthTxIndexByHash(tc.txHash, block, blockRes, s.backend) + + if tc.expectError { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.errorMsg) + s.Require().Equal(int32(-1), idx) + } else { + s.Require().NoError(err) + s.Require().Equal(tc.expectedIdx, idx) + } + }) + } +} diff --git a/tests/speedtest/go.sum b/tests/speedtest/go.sum index 5a7b98de2..2cbe0f77b 100644 --- a/tests/speedtest/go.sum +++ b/tests/speedtest/go.sum @@ -965,6 +965,8 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= diff --git a/x/vm/keeper/abci.go b/x/vm/keeper/abci.go index ce4f39404..35716a1ff 100644 --- a/x/vm/keeper/abci.go +++ b/x/vm/keeper/abci.go @@ -41,7 +41,6 @@ func (k *Keeper) EndBlock(ctx sdk.Context) error { } k.CollectTxBloom(ctx) - k.ResetTransientGasUsed(ctx) return nil } diff --git a/x/vm/keeper/config.go b/x/vm/keeper/config.go index ae8473df6..d609b100c 100644 --- a/x/vm/keeper/config.go +++ b/x/vm/keeper/config.go @@ -38,8 +38,8 @@ func (k *Keeper) EVMConfig(ctx sdk.Context, proposerAddress sdk.ConsAddress) (*s // TxConfig loads `TxConfig` from current transient storage func (k *Keeper) TxConfig(ctx sdk.Context, txHash common.Hash) statedb.TxConfig { return statedb.NewTxConfig( - txHash, // TxHash - uint(ctx.TxIndex()), //#nosec G115 // TxIndex + txHash, // TxHash + 0, // TxIndex - patched in PatchTxResponses ) } diff --git a/x/vm/keeper/keeper.go b/x/vm/keeper/keeper.go index 70c238f2d..053d98dc1 100644 --- a/x/vm/keeper/keeper.go +++ b/x/vm/keeper/keeper.go @@ -362,19 +362,6 @@ func (k Keeper) GetMinGasPrice(ctx sdk.Context) math.LegacyDec { return k.feeMarketWrapper.GetParams(ctx).MinGasPrice } -// ResetTransientGasUsed reset gas used to prepare for execution of cosmos tx, called in EndBlocker. -func (k Keeper) ResetTransientGasUsed(ctx sdk.Context) { - store := prefix.NewObjStore(ctx.ObjectStore(k.objectKey), - types.KeyPrefixObjectGasUsed) - it := store.Iterator(nil, nil) - - defer it.Close() - - for ; it.Valid(); it.Next() { - store.Delete(it.Key()) - } -} - // GetTransientGasUsed returns the gas used by current cosmos tx. func (k Keeper) GetTransientGasUsed(ctx sdk.Context) uint64 { store := ctx.ObjectStore(k.objectKey) diff --git a/x/vm/keeper/state_transition.go b/x/vm/keeper/state_transition.go index 192f00ad5..038d29f3d 100644 --- a/x/vm/keeper/state_transition.go +++ b/x/vm/keeper/state_transition.go @@ -244,7 +244,6 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*t GasUsed: res.GasUsed, BlockHash: common.BytesToHash(ctx.HeaderHash()), BlockNumber: big.NewInt(ctx.BlockHeight()), - TransactionIndex: uint(ctx.TxIndex()), //#nosec G115 } if res.Failed() { diff --git a/x/vm/types/response.go b/x/vm/types/response.go new file mode 100644 index 000000000..f49e67159 --- /dev/null +++ b/x/vm/types/response.go @@ -0,0 +1,93 @@ +package types + +import ( + "strconv" + + abci "github.com/cometbft/cometbft/abci/types" + + proto "github.com/cosmos/gogoproto/proto" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// PatchTxResponses fills the evm tx index and log indexes in the tx result. +// Note: txIndex starts at 0 and is incremented for each Ethereum transaction found. +// This ensures proper indexing when multiple EVM transactions are included in a block. +func PatchTxResponses(input []*abci.ExecTxResult) []*abci.ExecTxResult { + var ( + txIndex uint64 + logIndex uint64 + ) + for _, res := range input { + // assume no error result in msg handler + if res.Code != 0 { + continue + } + + var txMsgData sdk.TxMsgData + if err := proto.Unmarshal(res.Data, &txMsgData); err != nil { + panic(err) + } + + var ( + anteEvents []abci.Event + // if the response data is modified and need to be marshaled back + dataDirty bool + ) + for i, rsp := range txMsgData.MsgResponses { + var response MsgEthereumTxResponse + if rsp.TypeUrl != "/"+proto.MessageName(&response) { + continue + } + + if err := proto.Unmarshal(rsp.Value, &response); err != nil { + panic(err) + } + + anteEvents = append(anteEvents, abci.Event{ + Type: EventTypeEthereumTx, + Attributes: []abci.EventAttribute{ + {Key: AttributeKeyEthereumTxHash, Value: response.Hash}, + {Key: AttributeKeyTxIndex, Value: strconv.FormatUint(txIndex, 10)}, + }, + }) + + if len(response.Logs) > 0 { + for _, log := range response.Logs { + log.TxIndex = txIndex + log.Index = logIndex + logIndex++ + } + + anyRsp, err := codectypes.NewAnyWithValue(&response) + if err != nil { + panic(err) + } + txMsgData.MsgResponses[i] = anyRsp + + dataDirty = true + } + + txIndex++ + } + + if len(anteEvents) > 0 { + // prepend ante events in front to emulate the side effect of `EthEmitEventDecorator` + events := make([]abci.Event, len(anteEvents)+len(res.Events)) + copy(events, anteEvents) + copy(events[len(anteEvents):], res.Events) + res.Events = events + + if dataDirty { + data, err := proto.Marshal(&txMsgData) + if err != nil { + panic(err) + } + + res.Data = data + } + } + } + return input +} diff --git a/x/vm/types/response_test.go b/x/vm/types/response_test.go new file mode 100644 index 000000000..080e2b2da --- /dev/null +++ b/x/vm/types/response_test.go @@ -0,0 +1,231 @@ +package types_test + +import ( + "strconv" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + abci "github.com/cometbft/cometbft/abci/types" + + evmtypes "github.com/cosmos/evm/x/vm/types" + proto "github.com/cosmos/gogoproto/proto" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func createEthTxResult(t *testing.T, hash string, numLogs int, code uint32) *abci.ExecTxResult { + t.Helper() + logs := make([]*evmtypes.Log, numLogs) + for i := 0; i < numLogs; i++ { + logs[i] = &evmtypes.Log{Data: []byte{byte(i)}} + } + response := &evmtypes.MsgEthereumTxResponse{ + Hash: common.BytesToHash([]byte(hash)).String(), + Logs: logs, + } + anyRsp, _ := codectypes.NewAnyWithValue(response) + txMsgData := &sdk.TxMsgData{ + MsgResponses: []*codectypes.Any{anyRsp}, + } + data, _ := proto.Marshal(txMsgData) + return &abci.ExecTxResult{ + Code: code, + Data: data, + } +} + +func unmarshalTxResponse(t *testing.T, result *abci.ExecTxResult) *evmtypes.MsgEthereumTxResponse { + t.Helper() + var txMsgData sdk.TxMsgData + err := proto.Unmarshal(result.Data, &txMsgData) + require.NoError(t, err) + var response evmtypes.MsgEthereumTxResponse + err = proto.Unmarshal(txMsgData.MsgResponses[0].Value, &response) + require.NoError(t, err) + return &response +} + +func requireEventTxIndex(t *testing.T, result *abci.ExecTxResult, expectedIdx string) { + t.Helper() + require.Len(t, result.Events, 1) + require.Equal(t, evmtypes.EventTypeEthereumTx, result.Events[0].Type) + require.Equal(t, expectedIdx, result.Events[0].Attributes[1].Value) +} + +func TestPatchTxResponses(t *testing.T) { + testCases := []struct { + name string + input []*abci.ExecTxResult + validate func(t *testing.T, result []*abci.ExecTxResult) + }{ + { + name: "empty input", + input: []*abci.ExecTxResult{}, + validate: func(t *testing.T, result []*abci.ExecTxResult) { + t.Helper() + require.Empty(t, result) + }, + }, + { + name: "single tx with no logs", + input: []*abci.ExecTxResult{createEthTxResult(t, "hash1", 0, 0)}, + validate: func(t *testing.T, result []*abci.ExecTxResult) { + t.Helper() + require.Len(t, result, 1) + requireEventTxIndex(t, result[0], "0") + }, + }, + { + name: "single tx with logs", + input: []*abci.ExecTxResult{createEthTxResult(t, "hash1", 2, 0)}, + validate: func(t *testing.T, result []*abci.ExecTxResult) { + t.Helper() + require.Len(t, result, 1) + requireEventTxIndex(t, result[0], "0") + response := unmarshalTxResponse(t, result[0]) + require.Len(t, response.Logs, 2) + require.Equal(t, uint64(0), response.Logs[0].TxIndex) + require.Equal(t, uint64(0), response.Logs[0].Index) + require.Equal(t, uint64(0), response.Logs[1].TxIndex) + require.Equal(t, uint64(1), response.Logs[1].Index) + }, + }, + { + name: "multiple txs with logs", + input: []*abci.ExecTxResult{ + createEthTxResult(t, "hash1", 2, 0), + createEthTxResult(t, "hash2", 3, 0), + }, + validate: func(t *testing.T, result []*abci.ExecTxResult) { + t.Helper() + require.Len(t, result, 2) + requireEventTxIndex(t, result[0], "0") + response1 := unmarshalTxResponse(t, result[0]) + require.Len(t, response1.Logs, 2) + require.Equal(t, uint64(0), response1.Logs[0].TxIndex) + require.Equal(t, uint64(0), response1.Logs[0].Index) + require.Equal(t, uint64(0), response1.Logs[1].TxIndex) + require.Equal(t, uint64(1), response1.Logs[1].Index) + + requireEventTxIndex(t, result[1], "1") + response2 := unmarshalTxResponse(t, result[1]) + require.Len(t, response2.Logs, 3) + require.Equal(t, uint64(1), response2.Logs[0].TxIndex) + require.Equal(t, uint64(2), response2.Logs[0].Index) + require.Equal(t, uint64(1), response2.Logs[1].TxIndex) + require.Equal(t, uint64(3), response2.Logs[1].Index) + require.Equal(t, uint64(1), response2.Logs[2].TxIndex) + require.Equal(t, uint64(4), response2.Logs[2].Index) + }, + }, + { + name: "failed tx should be skipped", + input: []*abci.ExecTxResult{createEthTxResult(t, "hash1", 1, 1)}, + validate: func(t *testing.T, result []*abci.ExecTxResult) { + t.Helper() + require.Len(t, result, 1) + require.Empty(t, result[0].Events) + }, + }, + { + name: "mixed success and failed txs", + input: []*abci.ExecTxResult{ + createEthTxResult(t, "hash1", 1, 0), // Success + createEthTxResult(t, "hash2", 1, 1), // Failed + createEthTxResult(t, "hash3", 1, 0), // Success + }, + validate: func(t *testing.T, result []*abci.ExecTxResult) { + t.Helper() + require.Len(t, result, 3) + requireEventTxIndex(t, result[0], "0") + require.Empty(t, result[1].Events) + requireEventTxIndex(t, result[2], "1") + response3 := unmarshalTxResponse(t, result[2]) + require.Equal(t, uint64(1), response3.Logs[0].TxIndex) + require.Equal(t, uint64(1), response3.Logs[0].Index) + }, + }, + { + name: "tx with existing events", + input: func() []*abci.ExecTxResult { + result := createEthTxResult(t, "hash1", 1, 0) + result.Events = []abci.Event{ + {Type: "existing_event", Attributes: []abci.EventAttribute{{Key: "key", Value: "value"}}}, + } + return []*abci.ExecTxResult{result} + }(), + validate: func(t *testing.T, result []*abci.ExecTxResult) { + t.Helper() + require.Len(t, result, 1) + require.Len(t, result[0].Events, 2) + require.Equal(t, evmtypes.EventTypeEthereumTx, result[0].Events[0].Type) + require.Equal(t, "existing_event", result[0].Events[1].Type) + }, + }, + { + name: "non-ethereum tx msg should be ignored", + input: func() []*abci.ExecTxResult { + anyRsp, _ := codectypes.NewAnyWithValue(&sdk.TxMsgData{}) + txMsgData := &sdk.TxMsgData{MsgResponses: []*codectypes.Any{anyRsp}} + data, _ := proto.Marshal(txMsgData) + return []*abci.ExecTxResult{{Code: 0, Data: data}} + }(), + validate: func(t *testing.T, result []*abci.ExecTxResult) { + t.Helper() + require.Len(t, result, 1) + require.Empty(t, result[0].Events) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := evmtypes.PatchTxResponses(tc.input) + tc.validate(t, result) + }) + } +} + +func TestPatchTxResponses_EventAttributes(t *testing.T) { + txHash := common.BytesToHash([]byte("test_hash")) + input := []*abci.ExecTxResult{createEthTxResult(t, txHash.Hex(), 0, 0)} + result := evmtypes.PatchTxResponses(input) + + require.Len(t, result, 1) + require.Len(t, result[0].Events, 1) + + event := result[0].Events[0] + require.Equal(t, evmtypes.EventTypeEthereumTx, event.Type) + require.Len(t, event.Attributes, 2) + require.Equal(t, evmtypes.AttributeKeyEthereumTxHash, event.Attributes[0].Key) + require.Equal(t, evmtypes.AttributeKeyTxIndex, event.Attributes[1].Key) + require.Equal(t, "0", event.Attributes[1].Value) +} + +func TestPatchTxResponses_LogIndex(t *testing.T) { + input := []*abci.ExecTxResult{ + createEthTxResult(t, "hash1", 2, 0), // Logs 0, 1 + createEthTxResult(t, "hash2", 3, 0), // Logs 2, 3, 4 + createEthTxResult(t, "hash3", 1, 0), // Log 5 + } + result := evmtypes.PatchTxResponses(input) + expectedLogIndexes := [][]uint64{ + {0, 1}, + {2, 3, 4}, + {5}, + } + for txIdx, expectedIndexes := range expectedLogIndexes { + response := unmarshalTxResponse(t, result[txIdx]) + require.Len(t, response.Logs, len(expectedIndexes)) + for logIdx, expectedIndex := range expectedIndexes { + require.Equal(t, expectedIndex, response.Logs[logIdx].Index) + require.Equal(t, uint64(txIdx), response.Logs[logIdx].TxIndex) //#nosec G115 + } + eventTxIndex, err := strconv.ParseUint(result[txIdx].Events[0].Attributes[1].Value, 10, 64) + require.NoError(t, err) + require.Equal(t, uint64(txIdx), eventTxIndex) //#nosec G115 + } +}