package ante import ( "errors" "math" "math/big" "strconv" sdkmath "cosmossdk.io/math" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" authante "github.com/cosmos/cosmos-sdk/x/auth/ante" ethermint "github.com/evmos/ethermint/types" evmkeeper "github.com/evmos/ethermint/x/evm/keeper" "github.com/evmos/ethermint/x/evm/statedb" evmtypes "github.com/evmos/ethermint/x/evm/types" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" ) // EthSigVerificationDecorator validates an ethereum signatures type EthSigVerificationDecorator struct { evmKeeper EVMKeeper } // NewEthSigVerificationDecorator creates a new EthSigVerificationDecorator func NewEthSigVerificationDecorator(ek EVMKeeper) EthSigVerificationDecorator { return EthSigVerificationDecorator{ evmKeeper: ek, } } // AnteHandle validates checks that the registered chain id is the same as the one on the message, and // that the signer address matches the one defined on the message. // It's not skipped for RecheckTx, because it set `From` address which is critical from other ante handler to work. // Failure in RecheckTx will prevent tx to be included into block, especially when CheckTx succeed, in which case user // won't see the error message. func (esvd EthSigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { chainID := esvd.evmKeeper.ChainID() chainCfg := esvd.evmKeeper.GetChainConfig(ctx) ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) signer := ethtypes.MakeSigner(ethCfg, blockNum) for _, 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)) } allowUnprotectedTxs := esvd.evmKeeper.GetAllowUnprotectedTxs(ctx) ethTx := msgEthTx.AsTransaction() if !allowUnprotectedTxs && !ethTx.Protected() { return ctx, sdkerrors.Wrapf( sdkerrors.ErrNotSupported, "rejected unprotected Ethereum txs. Please EIP155 sign your transaction to protect it against replay-attacks") } sender, err := signer.Sender(ethTx) if err != nil { return ctx, sdkerrors.Wrapf( sdkerrors.ErrorInvalidSigner, "couldn't retrieve sender address from the ethereum transaction: %s", err.Error(), ) } // set up the sender to the transaction field if not already msgEthTx.From = sender.Hex() } return next(ctx, tx, simulate) } // EthAccountVerificationDecorator validates an account balance checks type EthAccountVerificationDecorator struct { ak evmtypes.AccountKeeper evmKeeper EVMKeeper } // NewEthAccountVerificationDecorator creates a new EthAccountVerificationDecorator func NewEthAccountVerificationDecorator(ak evmtypes.AccountKeeper, ek EVMKeeper) EthAccountVerificationDecorator { return EthAccountVerificationDecorator{ ak: ak, evmKeeper: ek, } } // AnteHandle validates checks that the sender balance is greater than the total transaction cost. // The account will be set to store if it doesn't exis, i.e cannot be found on store. // This AnteHandler decorator will fail if: // - any of the msgs is not a MsgEthereumTx // - from address is empty // - account balance is lower than the transaction cost func (avd EthAccountVerificationDecorator) AnteHandle( ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, ) (newCtx sdk.Context, err error) { if !ctx.IsCheckTx() { return next(ctx, tx, simulate) } 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)) } txData, err := evmtypes.UnpackTxData(msgEthTx.Data) if err != nil { return ctx, sdkerrors.Wrapf(err, "failed to unpack tx data any for tx %d", i) } // sender address should be in the tx cache from the previous AnteHandle call from := msgEthTx.GetFrom() if from.Empty() { return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "from address cannot be empty") } // check whether the sender address is EOA fromAddr := common.BytesToAddress(from) acct := avd.evmKeeper.GetAccount(ctx, fromAddr) if acct == nil { acc := avd.ak.NewAccountWithAddress(ctx, from) avd.ak.SetAccount(ctx, acc) acct = statedb.NewEmptyAccount() } else if acct.IsContract() { return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "the sender is not EOA: address %s, codeHash <%s>", fromAddr, acct.CodeHash) } if err := evmkeeper.CheckSenderBalance(sdkmath.NewIntFromBigInt(acct.Balance), txData); err != nil { return ctx, sdkerrors.Wrap(err, "failed to check sender balance") } } return next(ctx, tx, simulate) } // EthGasConsumeDecorator validates enough intrinsic gas for the transaction and // gas consumption. type EthGasConsumeDecorator struct { evmKeeper EVMKeeper maxGasWanted uint64 } // NewEthGasConsumeDecorator creates a new EthGasConsumeDecorator func NewEthGasConsumeDecorator( evmKeeper EVMKeeper, maxGasWanted uint64, ) EthGasConsumeDecorator { return EthGasConsumeDecorator{ evmKeeper, maxGasWanted, } } // AnteHandle validates that the Ethereum tx message has enough to cover intrinsic gas // (during CheckTx only) and that the sender has enough balance to pay for the gas cost. // // Intrinsic gas for a transaction is the amount of gas that the transaction uses before the // transaction is executed. The gas is a constant value plus any cost incurred by additional bytes // of data supplied with the transaction. // // This AnteHandler decorator will fail if: // - the message is not a MsgEthereumTx // - sender account cannot be found // - transaction's gas limit is lower than the intrinsic gas // - user doesn't have enough balance to deduct the transaction fees (gas_limit * gas_price) // - transaction or block gas meter runs out of gas // - sets the gas meter limit // - gas limit is greater than the block gas meter limit func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { // gas consumption limit already checked during CheckTx so there's no need to // verify it again during ReCheckTx if ctx.IsReCheckTx() { return next(ctx, tx, simulate) } chainCfg := egcd.evmKeeper.GetChainConfig(ctx) ethCfg := chainCfg.EthereumConfig(egcd.evmKeeper.ChainID()) blockHeight := big.NewInt(ctx.BlockHeight()) homestead := ethCfg.IsHomestead(blockHeight) istanbul := ethCfg.IsIstanbul(blockHeight) london := ethCfg.IsLondon(blockHeight) gasWanted := uint64(0) var events sdk.Events // Use the lowest priority of all the messages as the final one. minPriority := int64(math.MaxInt64) for _, 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)) } txData, err := evmtypes.UnpackTxData(msgEthTx.Data) if err != nil { return ctx, sdkerrors.Wrap(err, "failed to unpack tx data") } if ctx.IsCheckTx() && egcd.maxGasWanted != 0 { // We can't trust the tx gas limit, because we'll refund the unused gas. if txData.GetGas() > egcd.maxGasWanted { gasWanted += egcd.maxGasWanted } else { gasWanted += txData.GetGas() } } else { gasWanted += txData.GetGas() } evmDenom := egcd.evmKeeper.GetEVMDenom(ctx) fees, priority, err := egcd.evmKeeper.DeductTxCostsFromUserBalance( ctx, *msgEthTx, txData, evmDenom, homestead, istanbul, london, ) if err != nil { return ctx, sdkerrors.Wrapf(err, "failed to deduct transaction costs from user balance") } events = append(events, sdk.NewEvent( sdk.EventTypeTx, sdk.NewAttribute(sdk.AttributeKeyFee, fees.String()), ), ) if priority < minPriority { minPriority = priority } } ctx.EventManager().EmitEvents(events) blockGasLimit := ethermint.BlockGasLimit(ctx) // return error if the tx gas is greater than the block limit (max gas) // NOTE: it's important here to use the gas wanted instead of the gas consumed // from the tx gas pool. The later only has the value so far since the // EthSetupContextDecorator so it will never exceed the block gas limit. if gasWanted > blockGasLimit { return ctx, sdkerrors.Wrapf( sdkerrors.ErrOutOfGas, "tx gas (%d) exceeds block gas limit (%d)", gasWanted, blockGasLimit, ) } // Set tx GasMeter with a limit of GasWanted (i.e gas limit from the Ethereum tx). // The gas consumed will be then reset to the gas used by the state transition // in the EVM. // FIXME: use a custom gas configuration that doesn't add any additional gas and only // takes into account the gas consumed at the end of the EVM transaction. newCtx := ctx. WithGasMeter(ethermint.NewInfiniteGasMeterWithLimit(gasWanted)). WithPriority(minPriority) // we know that we have enough gas on the pool to cover the intrinsic gas return next(newCtx, tx, simulate) } // CanTransferDecorator checks if the sender is allowed to transfer funds according to the EVM block // context rules. type CanTransferDecorator struct { evmKeeper EVMKeeper } // NewCanTransferDecorator creates a new CanTransferDecorator instance. func NewCanTransferDecorator(evmKeeper EVMKeeper) CanTransferDecorator { return CanTransferDecorator{ evmKeeper: evmKeeper, } } // AnteHandle creates an EVM from the message and calls the BlockContext CanTransfer function to // see if the address can execute the transaction. func (ctd CanTransferDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { params := ctd.evmKeeper.GetParams(ctx) ethCfg := params.ChainConfig.EthereumConfig(ctd.evmKeeper.ChainID()) signer := ethtypes.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight())) for _, 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)) } baseFee := ctd.evmKeeper.GetBaseFee(ctx, ethCfg) coreMsg, err := msgEthTx.AsMessage(signer, baseFee) if err != nil { return ctx, sdkerrors.Wrapf( err, "failed to create an ethereum core.Message from signer %T", signer, ) } if evmtypes.IsLondon(ethCfg, ctx.BlockHeight()) { if baseFee == nil { return ctx, sdkerrors.Wrap( evmtypes.ErrInvalidBaseFee, "base fee is supported but evm block context value is nil", ) } if coreMsg.GasFeeCap().Cmp(baseFee) < 0 { return ctx, sdkerrors.Wrapf( sdkerrors.ErrInsufficientFee, "max fee per gas less than block base fee (%s < %s)", coreMsg.GasFeeCap(), baseFee, ) } } // NOTE: pass in an empty coinbase address and nil tracer as we don't need them for the check below cfg := &evmtypes.EVMConfig{ ChainConfig: ethCfg, Params: params, CoinBase: common.Address{}, BaseFee: baseFee, } stateDB := statedb.New(ctx, ctd.evmKeeper, statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash().Bytes()))) evm := ctd.evmKeeper.NewEVM(ctx, coreMsg, cfg, evmtypes.NewNoOpTracer(), stateDB) // check that caller has enough balance to cover asset transfer for **topmost** call // NOTE: here the gas consumed is from the context with the infinite gas meter if coreMsg.Value().Sign() > 0 && !evm.Context().CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) { return ctx, sdkerrors.Wrapf( sdkerrors.ErrInsufficientFunds, "failed to transfer %s from address %s using the EVM block context transfer function", coreMsg.Value(), coreMsg.From(), ) } } return next(ctx, tx, simulate) } // EthIncrementSenderSequenceDecorator increments the sequence of the signers. type EthIncrementSenderSequenceDecorator struct { ak evmtypes.AccountKeeper } // NewEthIncrementSenderSequenceDecorator creates a new EthIncrementSenderSequenceDecorator. func NewEthIncrementSenderSequenceDecorator(ak evmtypes.AccountKeeper) EthIncrementSenderSequenceDecorator { return EthIncrementSenderSequenceDecorator{ ak: ak, } } // AnteHandle handles incrementing the sequence of the signer (i.e sender). If the transaction is a // contract creation, the nonce will be incremented during the transaction execution and not within // this AnteHandler decorator. func (issd EthIncrementSenderSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { for _, 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)) } txData, err := evmtypes.UnpackTxData(msgEthTx.Data) if err != nil { return ctx, sdkerrors.Wrap(err, "failed to unpack tx data") } // increase sequence of sender acc := issd.ak.GetAccount(ctx, msgEthTx.GetFrom()) if acc == nil { return ctx, sdkerrors.Wrapf( sdkerrors.ErrUnknownAddress, "account %s is nil", common.BytesToAddress(msgEthTx.GetFrom().Bytes()), ) } nonce := acc.GetSequence() // we merged the nonce verification to nonce increment, so when tx includes multiple messages // with same sender, they'll be accepted. if txData.GetNonce() != nonce { return ctx, sdkerrors.Wrapf( sdkerrors.ErrInvalidSequence, "invalid nonce; got %d, expected %d", txData.GetNonce(), nonce, ) } if err := acc.SetSequence(nonce + 1); err != nil { return ctx, sdkerrors.Wrapf(err, "failed to set sequence to %d", acc.GetSequence()+1) } issd.ak.SetAccount(ctx, acc) } return next(ctx, tx, simulate) } // EthValidateBasicDecorator is adapted from ValidateBasicDecorator from cosmos-sdk, it ignores ErrNoSignatures type EthValidateBasicDecorator struct { evmKeeper EVMKeeper } // NewEthValidateBasicDecorator creates a new EthValidateBasicDecorator func NewEthValidateBasicDecorator(ek EVMKeeper) EthValidateBasicDecorator { return EthValidateBasicDecorator{ evmKeeper: ek, } } // AnteHandle handles basic validation of tx func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { // no need to validate basic on recheck tx, call next antehandler if ctx.IsReCheckTx() { return next(ctx, tx, simulate) } err := tx.ValidateBasic() // ErrNoSignatures is fine with eth tx if err != nil && !errors.Is(err, sdkerrors.ErrNoSignatures) { return ctx, sdkerrors.Wrap(err, "tx basic validation failed") } // For eth type cosmos tx, some fields should be veified as zero values, // since we will only verify the signature against the hash of the MsgEthereumTx.Data wrapperTx, ok := tx.(protoTxProvider) if !ok { return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid tx type %T, didn't implement interface protoTxProvider", tx) } protoTx := wrapperTx.GetProtoTx() body := protoTx.Body if body.Memo != "" || body.TimeoutHeight != uint64(0) || len(body.NonCriticalExtensionOptions) > 0 { return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx body Memo TimeoutHeight NonCriticalExtensionOptions should be empty") } if len(body.ExtensionOptions) != 1 { return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx length of ExtensionOptions should be 1") } txFee := sdk.Coins{} txGasLimit := uint64(0) chainCfg := vbd.evmKeeper.GetChainConfig(ctx) chainID := vbd.evmKeeper.ChainID() ethCfg := chainCfg.EthereumConfig(chainID) baseFee := vbd.evmKeeper.GetBaseFee(ctx, ethCfg) enableCreate := vbd.evmKeeper.GetEnableCreate(ctx) enableCall := vbd.evmKeeper.GetEnableCall(ctx) evmDenom := vbd.evmKeeper.GetEVMDenom(ctx) for _, msg := range protoTx.GetMsgs() { msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) if !ok { return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) } // Validate `From` field if msgEthTx.From != "" { return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid From %s, expect empty string", msgEthTx.From) } txGasLimit += msgEthTx.GetGas() txData, err := evmtypes.UnpackTxData(msgEthTx.Data) if err != nil { return ctx, sdkerrors.Wrap(err, "failed to unpack MsgEthereumTx Data") } // return error if contract creation or call are disabled through governance if !enableCreate && txData.GetTo() == nil { return ctx, sdkerrors.Wrap(evmtypes.ErrCreateDisabled, "failed to create new contract") } else if !enableCall && txData.GetTo() != nil { return ctx, sdkerrors.Wrap(evmtypes.ErrCallDisabled, "failed to call contract") } if baseFee == nil && txData.TxType() == ethtypes.DynamicFeeTxType { return ctx, sdkerrors.Wrap(ethtypes.ErrTxTypeNotSupported, "dynamic fee tx not supported") } txFee = txFee.Add(sdk.NewCoin(evmDenom, sdkmath.NewIntFromBigInt(txData.Fee()))) } authInfo := protoTx.AuthInfo if len(authInfo.SignerInfos) > 0 { return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx AuthInfo SignerInfos should be empty") } if authInfo.Fee.Payer != "" || authInfo.Fee.Granter != "" { return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx AuthInfo Fee payer and granter should be empty") } if !authInfo.Fee.Amount.IsEqual(txFee) { return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid AuthInfo Fee Amount (%s != %s)", authInfo.Fee.Amount, txFee) } if authInfo.Fee.GasLimit != txGasLimit { return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid AuthInfo Fee GasLimit (%d != %d)", authInfo.Fee.GasLimit, txGasLimit) } sigs := protoTx.Signatures if len(sigs) > 0 { return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx Signatures should be empty") } return next(ctx, tx, simulate) } // EthSetupContextDecorator is adapted from SetUpContextDecorator from cosmos-sdk, it ignores gas consumption // by setting the gas meter to infinite type EthSetupContextDecorator struct { evmKeeper EVMKeeper } func NewEthSetUpContextDecorator(evmKeeper EVMKeeper) EthSetupContextDecorator { return EthSetupContextDecorator{ evmKeeper: evmKeeper, } } func (esc EthSetupContextDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { // all transactions must implement GasTx _, ok := tx.(authante.GasTx) if !ok { return newCtx, sdkerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be GasTx") } // We need to setup an empty gas config so that the gas is consistent with Ethereum. newCtx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()). WithKVGasConfig(storetypes.GasConfig{}). WithTransientKVGasConfig(storetypes.GasConfig{}) // Reset transient gas used to prepare the execution of current cosmos tx. // Transient gas-used is necessary to sum the gas-used of cosmos tx, when it contains multiple eth msgs. esc.evmKeeper.ResetTransientGasUsed(ctx) return next(newCtx, tx, simulate) } // EthMempoolFeeDecorator will check if the transaction's effective fee is at least as large // as the local validator's minimum gasFee (defined in validator config). // If fee is too low, decorator returns error and tx is rejected from mempool. // Note this only applies when ctx.CheckTx = true // If fee is high enough or not CheckTx, then call next AnteHandler // CONTRACT: Tx must implement FeeTx to use MempoolFeeDecorator type EthMempoolFeeDecorator struct { evmKeeper EVMKeeper } func NewEthMempoolFeeDecorator(ek EVMKeeper) EthMempoolFeeDecorator { return EthMempoolFeeDecorator{ evmKeeper: ek, } } // AnteHandle ensures that the provided fees meet a minimum threshold for the validator. // This check only for local mempool purposes, and thus it is only run on (Re)CheckTx. // The logic is also skipped if the London hard fork and EIP-1559 are enabled. func (mfd EthMempoolFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { if !ctx.IsCheckTx() || simulate { return next(ctx, tx, simulate) } chainCfg := mfd.evmKeeper.GetChainConfig(ctx) ethCfg := chainCfg.EthereumConfig(mfd.evmKeeper.ChainID()) baseFee := mfd.evmKeeper.GetBaseFee(ctx, ethCfg) // skip check as the London hard fork and EIP-1559 are enabled if baseFee != nil { return next(ctx, tx, simulate) } evmDenom := mfd.evmKeeper.GetEVMDenom(ctx) minGasPrice := ctx.MinGasPrices().AmountOf(evmDenom) for _, msg := range tx.GetMsgs() { ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) if !ok { return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) } fee := sdk.NewDecFromBigInt(ethMsg.GetFee()) gasLimit := sdk.NewDecFromBigInt(new(big.Int).SetUint64(ethMsg.GetGas())) requiredFee := minGasPrice.Mul(gasLimit) if fee.LT(requiredFee) { return ctx, sdkerrors.Wrapf( sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", fee, requiredFee, ) } } 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) }