From 7d8664043e700628026f0ef08cc29c86ea780e73 Mon Sep 17 00:00:00 2001 From: yihuang Date: Fri, 14 Jan 2022 17:37:33 +0800 Subject: [PATCH] impr: support batch eth txs (#901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * support batch eth tx Closes: 896 Allow multiple MsgEthereumTx in single tx * fix transaction receipt api * fix tx receipt api and accumulate tx gas used * fix lint * fix test * fix rpc test * cleanup * fix cumulativeGasUsed and gasUsed * fix lint * Update app/ante/eth.go Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> * Update app/ante/eth.go Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> * Update rpc/ethereum/backend/utils.go Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> * pr suggestions * typo * fix lint Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> --- CHANGELOG.md | 1 + app/ante/ante_test.go | 6 +- app/ante/eth.go | 255 +++++++++------------ app/ante/eth_test.go | 13 +- app/ante/handler_options.go | 3 +- app/ante/interfaces.go | 1 + rpc/ethereum/backend/backend.go | 49 ++-- rpc/ethereum/backend/utils.go | 82 +++++-- rpc/ethereum/namespaces/eth/api.go | 118 +++++++--- rpc/ethereum/namespaces/eth/filters/api.go | 1 - rpc/ethereum/types/utils.go | 102 +++++++-- rpc/websockets.go | 60 ++--- x/evm/client/rest/rest.go | 16 +- x/evm/keeper/keeper.go | 34 +++ x/evm/keeper/msg_server.go | 4 +- x/evm/keeper/state_transition.go | 9 +- x/evm/spec/02_state.md | 3 +- x/evm/spec/07_events.md | 1 + x/evm/types/events.go | 1 + x/evm/types/key.go | 2 + x/evm/types/utils.go | 19 +- x/evm/types/utils_test.go | 6 +- 22 files changed, 466 insertions(+), 320 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea8b26ea..86fdfdd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (evm) [tharsis#827](https://github.com/tharsis/ethermint/issues/827) Speed up creation of event logs by using the slice insertion idiom with indices. * (ante) [tharsis#819](https://github.com/tharsis/ethermint/pull/819) remove redundant ante handlers * (app) [tharsis#873](https://github.com/tharsis/ethermint/pull/873) Validate code hash in GenesisAccount +* (evm) [tharsis#901](https://github.com/tharsis/ethermint/pull/901) Support multiple MsgEthereumTx in single tx. ### Bug Fixes diff --git a/app/ante/ante_test.go b/app/ante/ante_test.go index 5ca53093..00b6a02f 100644 --- a/app/ante/ante_test.go +++ b/app/ante/ante_test.go @@ -146,7 +146,7 @@ func (suite AnteTestSuite) TestAnteHandler() { func() sdk.Tx { signedTx := evmtypes.NewTx( suite.app.EvmKeeper.ChainID(), - 3, + 6, &to, big.NewInt(10), 100000, @@ -167,7 +167,7 @@ func (suite AnteTestSuite) TestAnteHandler() { func() sdk.Tx { signedTx := evmtypes.NewTx( suite.app.EvmKeeper.ChainID(), - 4, + 7, &to, big.NewInt(10), 100000, @@ -186,7 +186,7 @@ func (suite AnteTestSuite) TestAnteHandler() { { "fail - CheckTx (cosmos tx is not valid)", func() sdk.Tx { - signedTx := evmtypes.NewTx(suite.app.EvmKeeper.ChainID(), 4, &to, big.NewInt(10), 100000, big.NewInt(1), nil, nil, nil, nil) + signedTx := evmtypes.NewTx(suite.app.EvmKeeper.ChainID(), 8, &to, big.NewInt(10), 100000, big.NewInt(1), nil, nil, nil, nil) signedTx.From = addr.Hex() txBuilder := suite.CreateTestTxBuilder(signedTx, privKey, 1, false) diff --git a/app/ante/eth.go b/app/ante/eth.go index 09d0bd67..9f74039c 100644 --- a/app/ante/eth.go +++ b/app/ante/eth.go @@ -35,10 +35,6 @@ func NewEthSigVerificationDecorator(ek EVMKeeper) EthSigVerificationDecorator { // 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) { - if tx == nil || len(tx.GetMsgs()) != 1 { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "only 1 ethereum msg supported per tx") - } - chainID := esvd.evmKeeper.ChainID() params := esvd.evmKeeper.GetParams(ctx) @@ -47,26 +43,27 @@ func (esvd EthSigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, s blockNum := big.NewInt(ctx.BlockHeight()) signer := ethtypes.MakeSigner(ethCfg, blockNum) - msg := tx.GetMsgs()[0] - msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, (*evmtypes.MsgEthereumTx)(nil)) + 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)) + } + + sender, err := signer.Sender(msgEthTx.AsTransaction()) + if err != nil { + return ctx, sdkerrors.Wrapf( + sdkerrors.ErrorInvalidSigner, + "couldn't retrieve sender address ('%s') from the ethereum transaction: %s", + msgEthTx.From, + err.Error(), + ) + } + + // set up the sender to the transaction field if not already + msgEthTx.From = sender.Hex() } - sender, err := signer.Sender(msgEthTx.AsTransaction()) - if err != nil { - return ctx, sdkerrors.Wrapf( - sdkerrors.ErrorInvalidSigner, - "couldn't retrieve sender address ('%s') from the ethereum transaction: %s", - msgEthTx.From, - err.Error(), - ) - } - - // set up the sender to the transaction field if not already - msgEthTx.From = sender.Hex() - - return next(ctx, msgEthTx, simulate) + return next(ctx, tx, simulate) } // EthAccountVerificationDecorator validates an account balance checks @@ -99,7 +96,7 @@ func (avd EthAccountVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx for i, msg := range tx.GetMsgs() { msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) if !ok { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, (*evmtypes.MsgEthereumTx)(nil)) + return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) } txData, err := evmtypes.UnpackTxData(msgEthTx.Data) @@ -134,58 +131,6 @@ func (avd EthAccountVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx return next(ctx, tx, simulate) } -// EthNonceVerificationDecorator checks that the account nonce from the transaction matches -// the sender account sequence. -type EthNonceVerificationDecorator struct { - ak evmtypes.AccountKeeper -} - -// NewEthNonceVerificationDecorator creates a new EthNonceVerificationDecorator -func NewEthNonceVerificationDecorator(ak evmtypes.AccountKeeper) EthNonceVerificationDecorator { - return EthNonceVerificationDecorator{ - ak: ak, - } -} - -// AnteHandle validates that the transaction nonces are valid and equivalent to the sender account’s -// current nonce. -func (nvd EthNonceVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { - // no need to check the nonce on ReCheckTx - if ctx.IsReCheckTx() { - return next(ctx, tx, simulate) - } - - for _, msg := range tx.GetMsgs() { - msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, (*evmtypes.MsgEthereumTx)(nil)) - } - - // sender address should be in the tx cache from the previous AnteHandle call - seq, err := nvd.ak.GetSequence(ctx, msgEthTx.GetFrom()) - if err != nil { - return ctx, sdkerrors.Wrapf(err, "sequence not found for address %s", msgEthTx.From) - } - - txData, err := evmtypes.UnpackTxData(msgEthTx.Data) - if err != nil { - return ctx, sdkerrors.Wrap(err, "failed to unpack tx data") - } - - // if multiple transactions are submitted in succession with increasing nonces, - // all will be rejected except the first, since the first needs to be included in a block - // before the sequence increments - if txData.GetNonce() != seq { - return ctx, sdkerrors.Wrapf( - sdkerrors.ErrInvalidSequence, - "invalid nonce; got %d, expected %d", txData.GetNonce(), seq, - ) - } - } - - return next(ctx, tx, simulate) -} - // EthGasConsumeDecorator validates enough intrinsic gas for the transaction and // gas consumption. type EthGasConsumeDecorator struct { @@ -231,7 +176,7 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula for _, msg := range tx.GetMsgs() { msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) if !ok { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, (*evmtypes.MsgEthereumTx)(nil)) + return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) } txData, err := evmtypes.UnpackTxData(msgEthTx.Data) @@ -298,7 +243,7 @@ func (ctd CanTransferDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate for _, msg := range tx.GetMsgs() { msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) if !ok { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, (*evmtypes.MsgEthereumTx)(nil)) + return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) } baseFee := ctd.evmKeeper.BaseFee(ctx, ethCfg) @@ -369,23 +314,40 @@ func NewEthIncrementSenderSequenceDecorator(ak evmtypes.AccountKeeper) EthIncrem // 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() { - // increment sequence of all signers - for _, addr := range msg.GetSigners() { - acc := issd.ak.GetAccount(ctx, addr) - - if acc == nil { - return ctx, sdkerrors.Wrapf( - sdkerrors.ErrUnknownAddress, - "account %s (%s) is nil", common.BytesToAddress(addr.Bytes()), addr, - ) - } - - if err := acc.SetSequence(acc.GetSequence() + 1); err != nil { - return ctx, sdkerrors.Wrapf(err, "failed to set sequence to %d", acc.GetSequence()+1) - } - - issd.ak.SetAccount(ctx, acc) + 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) @@ -430,30 +392,31 @@ func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simu return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx length of ExtensionOptions should be 1") } - if len(protoTx.GetMsgs()) != 1 { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "only 1 ethereum msg supported per tx") - } - msg := protoTx.GetMsgs()[0] - msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, (*evmtypes.MsgEthereumTx)(nil)) - } - ethGasLimit := msgEthTx.GetGas() + txFee := sdk.Coins{} + txGasLimit := uint64(0) - txData, err := evmtypes.UnpackTxData(msgEthTx.Data) - if err != nil { - return ctx, sdkerrors.Wrap(err, "failed to unpack MsgEthereumTx Data") - } + 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)) + } + txGasLimit += msgEthTx.GetGas() - params := vbd.evmKeeper.GetParams(ctx) - chainID := vbd.evmKeeper.ChainID() - ethCfg := params.ChainConfig.EthereumConfig(chainID) - baseFee := vbd.evmKeeper.BaseFee(ctx, ethCfg) - if baseFee == nil && txData.TxType() == ethtypes.DynamicFeeTxType { - return ctx, sdkerrors.Wrap(ethtypes.ErrTxTypeNotSupported, "dynamic fee tx not supported") - } + txData, err := evmtypes.UnpackTxData(msgEthTx.Data) + if err != nil { + return ctx, sdkerrors.Wrap(err, "failed to unpack MsgEthereumTx Data") + } - ethFeeAmount := sdk.Coins{sdk.NewCoin(params.EvmDenom, sdk.NewIntFromBigInt(txData.Fee()))} + params := vbd.evmKeeper.GetParams(ctx) + chainID := vbd.evmKeeper.ChainID() + ethCfg := params.ChainConfig.EthereumConfig(chainID) + baseFee := vbd.evmKeeper.BaseFee(ctx, ethCfg) + if baseFee == nil && txData.TxType() == ethtypes.DynamicFeeTxType { + return ctx, sdkerrors.Wrap(ethtypes.ErrTxTypeNotSupported, "dynamic fee tx not supported") + } + + txFee = txFee.Add(sdk.NewCoin(params.EvmDenom, sdk.NewIntFromBigInt(txData.Fee()))) + } authInfo := protoTx.AuthInfo if len(authInfo.SignerInfos) > 0 { @@ -464,12 +427,12 @@ func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simu return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx AuthInfo Fee payer and granter should be empty") } - if !authInfo.Fee.Amount.IsEqual(ethFeeAmount) { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "invalid eth tx AuthInfo Fee Amount") + 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 != ethGasLimit { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "invalid eth tx AuthInfo Fee GasLimit") + if authInfo.Fee.GasLimit != txGasLimit { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid AuthInfo Fee GasLimit (%d != %d)", authInfo.Fee.GasLimit, txGasLimit) } sigs := protoTx.Signatures @@ -483,10 +446,14 @@ func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simu // EthSetupContextDecorator is adapted from SetUpContextDecorator from cosmos-sdk, it ignores gas consumption // by setting the gas meter to infinite -type EthSetupContextDecorator struct{} +type EthSetupContextDecorator struct { + evmKeeper EVMKeeper +} -func NewEthSetUpContextDecorator() EthSetupContextDecorator { - return EthSetupContextDecorator{} +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) { @@ -497,6 +464,9 @@ func (esc EthSetupContextDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simul } newCtx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + // 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) } @@ -523,31 +493,30 @@ func NewEthMempoolFeeDecorator(ek EVMKeeper, fmk evmtypes.FeeMarketKeeper) EthMe // is only ran on check tx. func (mfd EthMempoolFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { if ctx.IsCheckTx() && !simulate { - if len(tx.GetMsgs()) != 1 { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "only 1 ethereum msg supported per tx") - } - msg, ok := tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx) - if !ok { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, (*evmtypes.MsgEthereumTx)(nil)) - } + 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)) + } - var feeAmt *big.Int + var feeAmt *big.Int - params := mfd.evmKeeper.GetParams(ctx) - chainID := mfd.evmKeeper.ChainID() - ethCfg := params.ChainConfig.EthereumConfig(chainID) - evmDenom := params.EvmDenom - baseFee := mfd.evmKeeper.BaseFee(ctx, ethCfg) - if baseFee != nil { - feeAmt = msg.GetEffectiveFee(baseFee) - } else { - feeAmt = msg.GetFee() - } + params := mfd.evmKeeper.GetParams(ctx) + chainID := mfd.evmKeeper.ChainID() + ethCfg := params.ChainConfig.EthereumConfig(chainID) + evmDenom := params.EvmDenom + baseFee := mfd.evmKeeper.BaseFee(ctx, ethCfg) + if baseFee != nil { + feeAmt = ethMsg.GetEffectiveFee(baseFee) + } else { + feeAmt = ethMsg.GetFee() + } - glDec := sdk.NewDec(int64(msg.GetGas())) - requiredFee := ctx.MinGasPrices().AmountOf(evmDenom).Mul(glDec) - if sdk.NewDecFromBigInt(feeAmt).LT(requiredFee) { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeAmt, requiredFee) + glDec := sdk.NewDec(int64(ethMsg.GetGas())) + requiredFee := ctx.MinGasPrices().AmountOf(evmDenom).Mul(glDec) + if sdk.NewDecFromBigInt(feeAmt).LT(requiredFee) { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeAmt, requiredFee) + } } } diff --git a/app/ante/eth_test.go b/app/ante/eth_test.go index f8f75fbc..e181e1fc 100644 --- a/app/ante/eth_test.go +++ b/app/ante/eth_test.go @@ -32,7 +32,7 @@ func (suite AnteTestSuite) TestEthSigVerificationDecorator() { reCheckTx bool expPass bool }{ - {"ReCheckTx", nil, true, false}, + {"ReCheckTx", &invalidTx{}, true, false}, {"invalid transaction type", &invalidTx{}, false, false}, { "invalid sender", @@ -145,7 +145,8 @@ func (suite AnteTestSuite) TestNewEthAccountVerificationDecorator() { } func (suite AnteTestSuite) TestEthNonceVerificationDecorator() { - dec := ante.NewEthNonceVerificationDecorator(suite.app.AccountKeeper) + suite.SetupTest() + dec := ante.NewEthIncrementSenderSequenceDecorator(suite.app.AccountKeeper) addr := tests.GenerateAddress() @@ -159,7 +160,7 @@ func (suite AnteTestSuite) TestEthNonceVerificationDecorator() { reCheckTx bool expPass bool }{ - {"ReCheckTx", nil, func() {}, true, true}, + {"ReCheckTx", &invalidTx{}, func() {}, true, false}, {"invalid transaction type", &invalidTx{}, func() {}, false, false}, {"sender account not found", tx, func() {}, false, false}, { @@ -406,13 +407,13 @@ func (suite AnteTestSuite) TestEthIncrementSenderSequenceDecorator() { "invalid transaction type", &invalidTx{}, func() {}, - false, true, + false, false, }, { "no signers", evmtypes.NewTx(suite.app.EvmKeeper.ChainID(), 1, &to, big.NewInt(10), 1000, big.NewInt(1), nil, nil, nil, nil), func() {}, - false, true, + false, false, }, { "account not set to store", @@ -467,7 +468,7 @@ func (suite AnteTestSuite) TestEthIncrementSenderSequenceDecorator() { } func (suite AnteTestSuite) TestEthSetupContextDecorator() { - dec := ante.NewEthSetUpContextDecorator() + dec := ante.NewEthSetUpContextDecorator(suite.app.EvmKeeper) tx := evmtypes.NewTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil, nil, nil) testCases := []struct { diff --git a/app/ante/handler_options.go b/app/ante/handler_options.go index 83d35a10..b960df39 100644 --- a/app/ante/handler_options.go +++ b/app/ante/handler_options.go @@ -48,12 +48,11 @@ func (options HandlerOptions) Validate() error { func newEthAnteHandler(options HandlerOptions) sdk.AnteHandler { return sdk.ChainAnteDecorators( - NewEthSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first + NewEthSetUpContextDecorator(options.EvmKeeper), // outermost AnteDecorator. SetUpContext must be called first NewEthMempoolFeeDecorator(options.EvmKeeper, options.FeeMarketKeeper), // Check eth effective gas price against minimal-gas-prices NewEthValidateBasicDecorator(options.EvmKeeper), NewEthSigVerificationDecorator(options.EvmKeeper), NewEthAccountVerificationDecorator(options.AccountKeeper, options.BankKeeper, options.EvmKeeper), - NewEthNonceVerificationDecorator(options.AccountKeeper), NewEthGasConsumeDecorator(options.EvmKeeper), NewCanTransferDecorator(options.EvmKeeper, options.FeeMarketKeeper), NewEthIncrementSenderSequenceDecorator(options.AccountKeeper), // innermost AnteDecorator. diff --git a/app/ante/interfaces.go b/app/ante/interfaces.go index 52096543..cfe435ef 100644 --- a/app/ante/interfaces.go +++ b/app/ante/interfaces.go @@ -25,6 +25,7 @@ type EVMKeeper interface { ) (sdk.Coins, error) BaseFee(ctx sdk.Context, ethCfg *params.ChainConfig) *big.Int GetBalance(ctx sdk.Context, addr common.Address) *big.Int + ResetTransientGasUsed(ctx sdk.Context) } type protoTxProvider interface { diff --git a/rpc/ethereum/backend/backend.go b/rpc/ethereum/backend/backend.go index 16063c77..0cb68c05 100644 --- a/rpc/ethereum/backend/backend.go +++ b/rpc/ethereum/backend/backend.go @@ -66,7 +66,6 @@ type Backend interface { HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) PendingTransactions() ([]*sdk.Tx, error) - GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) GetCoinbase() (sdk.AccAddress, error) @@ -536,18 +535,6 @@ func (e *EVMBackend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, erro return ethHeader, nil } -// GetTransactionLogs returns the logs given a transaction hash. -// It returns an error if there's an encoding error. -// If no logs are found for the tx hash, the error is nil. -func (e *EVMBackend) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) { - tx, err := e.GetTxByEthHash(txHash) - if err != nil { - return nil, err - } - - return TxLogsFromEvents(tx.TxResult.Events) -} - // PendingTransactions returns the transactions that are in the transaction pool // and have a from address that is one of the accounts this node manages. func (e *EVMBackend) PendingTransactions() ([]*sdk.Tx, error) { @@ -578,12 +565,12 @@ func (e *EVMBackend) GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error) { blockLogs := [][]*ethtypes.Log{} for _, txResult := range blockRes.TxsResults { - logs, err := TxLogsFromEvents(txResult.Events) + logs, err := AllTxLogsFromEvents(txResult.Events) if err != nil { return nil, err } - blockLogs = append(blockLogs, logs) + blockLogs = append(blockLogs, logs...) } return blockLogs, nil @@ -667,7 +654,7 @@ func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransac } for _, tx := range txs { - msg, err := evmtypes.UnwrapEthereumMsg(tx) + msg, err := evmtypes.UnwrapEthereumMsg(tx, txHash) if err != nil { // not ethereum tx continue @@ -696,16 +683,18 @@ func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransac return nil, errors.New("invalid ethereum tx") } + msgIndex, attrs := types.FindTxAttributes(res.TxResult.Events, hexTx) + if msgIndex < 0 { + return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) + } + tx, err := e.clientCtx.TxConfig.TxDecoder()(res.Tx) if err != nil { return nil, err } - if len(tx.GetMsgs()) != 1 { - return nil, errors.New("invalid ethereum tx") - } - - msg, ok := tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx) + // the `msgIndex` is inferred from tx events, should be within the bound. + msg, ok := tx.GetMsgs()[msgIndex].(*evmtypes.MsgEthereumTx) if !ok { return nil, errors.New("invalid ethereum tx") } @@ -718,7 +707,7 @@ func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransac // Try to find txIndex from events found := false - txIndex, err := types.TxIndexFromEvents(res.TxResult.Events) + txIndex, err := types.GetUint64Attribute(attrs, evmtypes.AttributeKeyTxIndex) if err == nil { found = true } else { @@ -1033,7 +1022,6 @@ func (e *EVMBackend) BaseFee(height int64) (*big.Int, error) { // GetEthereumMsgsFromTendermintBlock returns all real MsgEthereumTxs from a Tendermint block. // It also ensures consistency over the correct txs indexes across RPC endpoints func (e *EVMBackend) GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evmtypes.MsgEthereumTx { - // nolint: prealloc var result []*evmtypes.MsgEthereumTx txResults := blockRes.TxsResults @@ -1050,16 +1038,15 @@ func (e *EVMBackend) GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.Result e.logger.Debug("failed to decode transaction in block", "height", block.Block.Height, "error", err.Error()) continue } - if len(tx.GetMsgs()) != 1 { - continue - } - ethMsg, ok := tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx) - if !ok { - continue - } + for _, msg := range tx.GetMsgs() { + ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + continue + } - result = append(result, ethMsg) + result = append(result, ethMsg) + } } return result diff --git a/rpc/ethereum/backend/utils.go b/rpc/ethereum/backend/utils.go index c572c63a..dc8884eb 100644 --- a/rpc/ethereum/backend/utils.go +++ b/rpc/ethereum/backend/utils.go @@ -182,44 +182,76 @@ func (e *EVMBackend) getAccountNonce(accAddr common.Address, pending bool, heigh // add the uncommitted txs to the nonce counter // only supports `MsgEthereumTx` style tx for _, tx := range pendingTxs { - msg, err := evmtypes.UnwrapEthereumMsg(tx) - if err != nil { - // not ethereum tx - continue - } + for _, msg := range (*tx).GetMsgs() { + ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + // not ethereum tx + break + } - sender, err := msg.GetSender(e.chainID) - if err != nil { - continue - } - if sender == accAddr { - nonce++ + sender, err := ethMsg.GetSender(e.chainID) + if err != nil { + continue + } + if sender == accAddr { + nonce++ + } } } return nonce, nil } -// TxLogsFromEvents parses ethereum logs from cosmos events -func TxLogsFromEvents(events []abci.Event) ([]*ethtypes.Log, error) { - logs := make([]*evmtypes.Log, 0) +// AllTxLogsFromEvents parses all ethereum logs from cosmos events +func AllTxLogsFromEvents(events []abci.Event) ([][]*ethtypes.Log, error) { + allLogs := make([][]*ethtypes.Log, 0, 4) for _, event := range events { if event.Type != evmtypes.EventTypeTxLog { continue } - for _, attr := range event.Attributes { - if !bytes.Equal(attr.Key, []byte(evmtypes.AttributeKeyTxLog)) { - continue - } - - var log evmtypes.Log - if err := json.Unmarshal(attr.Value, &log); err != nil { - return nil, err - } - - logs = append(logs, &log) + logs, err := ParseTxLogsFromEvent(event) + if err != nil { + return nil, err } + + allLogs = append(allLogs, logs) + } + return allLogs, nil +} + +// TxLogsFromEvents parses ethereum logs from cosmos events for specific msg index +func TxLogsFromEvents(events []abci.Event, msgIndex int) ([]*ethtypes.Log, error) { + for _, event := range events { + if event.Type != evmtypes.EventTypeTxLog { + continue + } + + if msgIndex > 0 { + // not the eth tx we want + msgIndex-- + continue + } + + return ParseTxLogsFromEvent(event) + } + return nil, fmt.Errorf("eth tx logs not found for message index %d", msgIndex) +} + +// ParseTxLogsFromEvent parse tx logs from one event +func ParseTxLogsFromEvent(event abci.Event) ([]*ethtypes.Log, error) { + logs := make([]*evmtypes.Log, 0, len(event.Attributes)) + for _, attr := range event.Attributes { + if !bytes.Equal(attr.Key, []byte(evmtypes.AttributeKeyTxLog)) { + continue + } + + var log evmtypes.Log + if err := json.Unmarshal(attr.Value, &log); err != nil { + return nil, err + } + + logs = append(logs, &log) } return evmtypes.LogsToEthereum(logs), nil } diff --git a/rpc/ethereum/namespaces/eth/api.go b/rpc/ethereum/namespaces/eth/api.go index 6608883b..084929f6 100644 --- a/rpc/ethereum/namespaces/eth/api.go +++ b/rpc/ethereum/namespaces/eth/api.go @@ -6,7 +6,6 @@ import ( "fmt" "math" "math/big" - "strings" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" @@ -389,7 +388,20 @@ func (e *PublicAPI) GetCode(address common.Address, blockNrOrHash rpctypes.Block // GetTransactionLogs returns the logs given a transaction hash. func (e *PublicAPI) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) { e.logger.Debug("eth_getTransactionLogs", "hash", txHash) - return e.backend.GetTransactionLogs(txHash) + + hexTx := txHash.Hex() + res, err := e.backend.GetTxByEthHash(txHash) + if err != nil { + e.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) + return nil, nil + } + + msgIndex, _ := rpctypes.FindTxAttributes(res.TxResult.Events, hexTx) + if msgIndex < 0 { + return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) + } + // parse tx logs from events + return backend.TxLogsFromEvents(res.TxResult.Events, msgIndex) } // Sign signs the provided data using the private key of address via Geth's signature standard. @@ -556,7 +568,8 @@ func (e *PublicAPI) Resend(ctx context.Context, args evmtypes.TransactionArgs, g } for _, tx := range pending { - p, err := evmtypes.UnwrapEthereumMsg(tx) + // FIXME does Resend api possible at all? https://github.com/tharsis/ethermint/issues/905 + p, err := evmtypes.UnwrapEthereumMsg(tx, common.Hash{}) if err != nil { // not valid ethereum tx continue @@ -768,6 +781,11 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac return nil, nil } + msgIndex, attrs := rpctypes.FindTxAttributes(res.TxResult.Events, hexTx) + if msgIndex < 0 { + return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) + } + resBlock, err := e.clientCtx.Client.Block(e.ctx, &res.Height) if err != nil { e.logger.Debug("block not found", "height", res.Height, "error", err.Error()) @@ -780,13 +798,15 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac return nil, fmt.Errorf("failed to decode tx: %w", err) } - msg, err := evmtypes.UnwrapEthereumMsg(&tx) - if err != nil { - e.logger.Debug("invalid tx", "error", err.Error()) - return nil, 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) } - txData, err := evmtypes.UnpackTxData(msg.Data) + txData, err := evmtypes.UnpackTxData(ethMsg.Data) if err != nil { e.logger.Error("failed to unpack tx data", "error", err.Error()) return nil, err @@ -799,37 +819,61 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac return nil, nil } - for i := 0; i <= int(res.Index) && i < len(blockRes.TxsResults); i++ { + 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 + } + } // Get the transaction result from the log + _, found := attrs[evmtypes.AttributeKeyEthereumTxFailed] var status hexutil.Uint - if strings.Contains(res.TxResult.GetLog(), evmtypes.AttributeKeyEthereumTxFailed) { + if found { status = hexutil.Uint(ethtypes.ReceiptStatusFailed) } else { status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful) } - from, err := msg.GetSender(e.chainIDEpoch) + from, err := ethMsg.GetSender(e.chainIDEpoch) if err != nil { return nil, err } - logs, err := e.backend.GetTransactionLogs(hash) + // parse tx logs from events + logs, err := backend.TxLogsFromEvents(res.TxResult.Events, msgIndex) if err != nil { e.logger.Debug("logs not found", "hash", hexTx, "error", err.Error()) } - // get eth index based on block's txs - var txIndex uint64 - msgs := e.backend.GetEthereumMsgsFromTendermintBlock(resBlock, blockRes) - for i := range msgs { - if msgs[i].Hash == hexTx { - txIndex = uint64(i) - break + // Try to find txIndex from events + found = false + txIndex, err := rpctypes.GetUint64Attribute(attrs, evmtypes.AttributeKeyTxIndex) + if err == nil { + found = true + } else { + // 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 + break + } } } + if !found { + return nil, errors.New("can't find index of ethereum tx") + } receipt := map[string]interface{}{ // Consensus fields: These fields are defined by the Yellow Paper @@ -842,7 +886,7 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac // They are stored in the chain database. "transactionHash": hash, "contractAddress": nil, - "gasUsed": hexutil.Uint64(res.TxResult.GasUsed), + "gasUsed": hexutil.Uint64(gasUsed), "type": hexutil.Uint(txData.TxType()), // Inclusion information: These fields provide information about the inclusion of the @@ -888,24 +932,26 @@ func (e *PublicAPI) GetPendingTransactions() ([]*rpctypes.RPCTransaction, error) result := make([]*rpctypes.RPCTransaction, 0, len(txs)) for _, tx := range txs { - msg, err := evmtypes.UnwrapEthereumMsg(tx) - if err != nil { - // not valid ethereum tx - continue - } + for _, msg := range (*tx).GetMsgs() { + ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + // not valid ethereum tx + break + } - rpctx, err := rpctypes.NewTransactionFromMsg( - msg, - common.Hash{}, - uint64(0), - uint64(0), - e.chainIDEpoch, - ) - if err != nil { - return nil, err - } + rpctx, err := rpctypes.NewTransactionFromMsg( + ethMsg, + common.Hash{}, + uint64(0), + uint64(0), + e.chainIDEpoch, + ) + if err != nil { + return nil, err + } - result = append(result, rpctx) + result = append(result, rpctx) + } } return result, nil diff --git a/rpc/ethereum/namespaces/eth/filters/api.go b/rpc/ethereum/namespaces/eth/filters/api.go index 6488b6a6..5d6a788a 100644 --- a/rpc/ethereum/namespaces/eth/filters/api.go +++ b/rpc/ethereum/namespaces/eth/filters/api.go @@ -31,7 +31,6 @@ type Backend interface { GetLogsByNumber(blockNum types.BlockNumber) ([][]*ethtypes.Log, error) BlockBloom(height *int64) (ethtypes.Bloom, error) - GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) BloomStatus() (uint64, uint64) RPCFilterCap() int32 diff --git a/rpc/ethereum/types/utils.go b/rpc/ethereum/types/utils.go index 6e8990d7..81d84d57 100644 --- a/rpc/ethereum/types/utils.go +++ b/rpc/ethereum/types/utils.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/hex" - "errors" "fmt" "math/big" "strconv" @@ -25,21 +24,21 @@ import ( ) // RawTxToEthTx returns a evm MsgEthereum transaction from raw tx bytes. -func RawTxToEthTx(clientCtx client.Context, txBz tmtypes.Tx) (*evmtypes.MsgEthereumTx, error) { +func RawTxToEthTx(clientCtx client.Context, txBz tmtypes.Tx) ([]*evmtypes.MsgEthereumTx, error) { tx, err := clientCtx.TxConfig.TxDecoder()(txBz) if err != nil { return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) } - if len(tx.GetMsgs()) != 1 { - return nil, errors.New("not ethereum tx") + ethTxs := make([]*evmtypes.MsgEthereumTx, len(tx.GetMsgs())) + for i, msg := range tx.GetMsgs() { + ethTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return nil, fmt.Errorf("invalid message type %T, expected %T", msg, &evmtypes.MsgEthereumTx{}) + } + ethTxs[i] = ethTx } - - ethTx, ok := tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx) - if !ok { - return nil, fmt.Errorf("invalid msg type %T, expected %T", tx, evmtypes.MsgEthereumTx{}) - } - return ethTx, nil + return ethTxs, nil } // EthHeaderFromTendermint is an util function that returns an Ethereum Header @@ -256,25 +255,80 @@ func BaseFeeFromEvents(events []abci.Event) *big.Int { return nil } -// TxIndexFromEvents parses the tx index from cosmos events -func TxIndexFromEvents(events []abci.Event) (uint64, error) { +// 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 } - for _, attr := range event.Attributes { - if bytes.Equal(attr.Key, []byte(evmtypes.AttributeKeyTxIndex)) { - result, err := strconv.ParseInt(string(attr.Value), 10, 64) - if err != nil { - return 0, err - } - if result < 0 { - return 0, errors.New("negative tx index") - } - return uint64(result), nil - } + 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 } - return 0, errors.New("not found") + // not found + return -1, nil +} + +// 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 } diff --git a/rpc/websockets.go b/rpc/websockets.go index 1a6ef78b..86629e34 100644 --- a/rpc/websockets.go +++ b/rpc/websockets.go @@ -683,44 +683,46 @@ func (api *pubSubAPI) subscribePendingTransactions(wsConn *wsConn) (rpc.ID, erro select { case ev := <-txsCh: data, _ := ev.Data.(tmtypes.EventDataTx) - ethTx, err := types.RawTxToEthTx(api.clientCtx, data.Tx) + ethTxs, err := types.RawTxToEthTx(api.clientCtx, data.Tx) if err != nil { // not ethereum tx - panic("debug") + continue } api.filtersMu.RLock() - for subID, wsSub := range api.filters { - subID := subID - wsSub := wsSub - if wsSub.query != query { - continue - } - // write to ws conn - res := &SubscriptionNotification{ - Jsonrpc: "2.0", - Method: "eth_subscription", - Params: &SubscriptionResult{ - Subscription: subID, - Result: ethTx.Hash, - }, - } + for _, ethTx := range ethTxs { + for subID, wsSub := range api.filters { + subID := subID + wsSub := wsSub + if wsSub.query != query { + continue + } + // write to ws conn + res := &SubscriptionNotification{ + Jsonrpc: "2.0", + Method: "eth_subscription", + Params: &SubscriptionResult{ + Subscription: subID, + Result: ethTx.Hash, + }, + } - err = wsSub.wsConn.WriteJSON(res) - if err != nil { - api.logger.Debug("error writing header, will drop peer", "error", err.Error()) + err = wsSub.wsConn.WriteJSON(res) + if err != nil { + api.logger.Debug("error writing header, will drop peer", "error", err.Error()) - try(func() { - api.filtersMu.Lock() - defer api.filtersMu.Unlock() + try(func() { + api.filtersMu.Lock() + defer api.filtersMu.Unlock() - if err != websocket.ErrCloseSent { - _ = wsSub.wsConn.Close() - } + if err != websocket.ErrCloseSent { + _ = wsSub.wsConn.Close() + } - delete(api.filters, subID) - close(wsSub.unsubscribed) - }, api.logger, "closing websocket peer sub") + delete(api.filters, subID) + close(wsSub.unsubscribed) + }, api.logger, "closing websocket peer sub") + } } } api.filtersMu.RUnlock() diff --git a/x/evm/client/rest/rest.go b/x/evm/client/rest/rest.go index 81368e29..73e410fb 100644 --- a/x/evm/client/rest/rest.go +++ b/x/evm/client/rest/rest.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" "math/big" "net/http" "strings" @@ -94,17 +95,22 @@ func getEthTransactionByHash(clientCtx client.Context, hashHex string) ([]byte, blockHash := common.BytesToHash(block.Block.Header.Hash()) - ethTx, err := rpctypes.RawTxToEthTx(clientCtx, tx.Tx) + ethTxs, err := rpctypes.RawTxToEthTx(clientCtx, tx.Tx) if err != nil { return nil, err } height := uint64(tx.Height) - rpcTx, err := rpctypes.NewRPCTransaction(ethTx.AsTransaction(), blockHash, height, uint64(tx.Index), baseFee) - if err != nil { - return nil, err + for _, ethTx := range ethTxs { + if common.HexToHash(ethTx.Hash) == common.BytesToHash(hash) { + rpcTx, err := rpctypes.NewRPCTransaction(ethTx.AsTransaction(), blockHash, height, uint64(tx.Index), baseFee) + if err != nil { + return nil, err + } + return json.Marshal(rpcTx) + } } - return json.Marshal(rpcTx) + return nil, errors.New("eth tx not found") } diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index c9ebd8ab..c6903eec 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -6,6 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -301,3 +302,36 @@ func (k Keeper) BaseFee(ctx sdk.Context, ethCfg *params.ChainConfig) *big.Int { } return baseFee } + +// ResetTransientGasUsed reset gas used to prepare for execution of current cosmos tx, called in ante handler. +func (k Keeper) ResetTransientGasUsed(ctx sdk.Context) { + store := ctx.TransientStore(k.transientKey) + store.Delete(types.KeyPrefixTransientGasUsed) +} + +// GetTransientGasUsed returns the gas used by current cosmos tx. +func (k Keeper) GetTransientGasUsed(ctx sdk.Context) uint64 { + store := ctx.TransientStore(k.transientKey) + bz := store.Get(types.KeyPrefixTransientGasUsed) + if len(bz) == 0 { + return 0 + } + return sdk.BigEndianToUint64(bz) +} + +// SetTransientGasUsed sets the gas used by current cosmos tx. +func (k Keeper) SetTransientGasUsed(ctx sdk.Context, gasUsed uint64) { + store := ctx.TransientStore(k.transientKey) + bz := sdk.Uint64ToBigEndian(gasUsed) + store.Set(types.KeyPrefixTransientGasUsed, bz) +} + +// AddTransientGasUsed accumulate gas used by each eth msgs included in current cosmos tx. +func (k Keeper) AddTransientGasUsed(ctx sdk.Context, gasUsed uint64) (uint64, error) { + result := k.GetTransientGasUsed(ctx) + gasUsed + if result < gasUsed { + return 0, sdkerrors.Wrap(types.ErrGasOverflow, "transient gas used") + } + k.SetTransientGasUsed(ctx, result) + return result, nil +} diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index f3cfe68e..50c49afc 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -38,7 +38,9 @@ func (k *Keeper) EthereumTx(goCtx context.Context, msg *types.MsgEthereumTx) (*t // add event for ethereum transaction hash format sdk.NewAttribute(types.AttributeKeyEthereumTxHash, response.Hash), // add event for index of valid ethereum tx - sdk.NewAttribute(types.AttributeKeyTxIndex, strconv.FormatInt(int64(txIndex), 10)), + sdk.NewAttribute(types.AttributeKeyTxIndex, strconv.FormatUint(txIndex, 10)), + // add event for eth tx gas used, we can't get it from cosmos tx result when it contains multiple eth tx msgs. + sdk.NewAttribute(types.AttributeKeyTxGasUsed, strconv.FormatUint(response.GasUsed, 10)), } if len(ctx.TxBytes()) > 0 { diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go index 1560b616..10b66aac 100644 --- a/x/evm/keeper/state_transition.go +++ b/x/evm/keeper/state_transition.go @@ -287,8 +287,13 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*t k.SetTxIndexTransient(ctx, uint64(txConfig.TxIndex)+1) - // update the gas used after refund - k.ResetGasMeterAndConsumeGas(ctx, res.GasUsed) + totalGasUsed, err := k.AddTransientGasUsed(ctx, res.GasUsed) + if err != nil { + return nil, sdkerrors.Wrap(err, "failed to add transient gas used") + } + + // reset the gas meter for current cosmos transaction + k.ResetGasMeterAndConsumeGas(ctx, totalGasUsed) return res, nil } diff --git a/x/evm/spec/02_state.md b/x/evm/spec/02_state.md index 800917f7..7b6d7197 100644 --- a/x/evm/spec/02_state.md +++ b/x/evm/spec/02_state.md @@ -19,6 +19,7 @@ The `x/evm` module keeps the following objects in state: | Block Bloom | Block bloom filter, used to accumulate the bloom filter of current block, emitted to events at end blocker. | `[]byte{1} + []byte(tx.Hash)` | `protobuf([]Log)` | Transient | | Tx Index | Index of current transaction in current block. | `[]byte{2}` | `BigEndian(uint64)` | Transient | | Log Size | Number of the logs emitted so far in current block. Used to decide the log index of following logs. | `[]byte{3}` | `BigEndian(uint64)` | Transient | +| Gas Used | Amount of gas used by ethereum messages of current cosmos-sdk tx, it's necessary when cosmos-sdk tx contains multiple ethereum messages. | `[]byte{4}` | `BigEndian(uint64)` | Transient | ## StateDB @@ -161,7 +162,7 @@ With `AddLog()` you can append the given ethereum `Log` to the list of Logs asso ## Keeper -The EVM module `Keeper` grants access to the EVM module state and implements `statedb.Keeper` interface to support the `StateDB` implementation. The Keeper contains a store key that allows the DB to write to a concrete subtree of the multistore that is only accessible to the EVM module. Instead of using a trie and database for querying and persistence (the `StateDB` implementation on Ethermint), use the Cosmos `KVStore` (key-value store) and Cosmos SDK `Keeper` to facilitate state transitions. +The EVM module `Keeper` grants access to the EVM module state and implements `statedb.Keeper` interface to support the `StateDB` implementation. The Keeper contains a store key that allows the DB to write to a concrete subtree of the multistore that is only accessible to the EVM module. Instead of using a trie and database for querying and persistence (the `StateDB` implementation on Ethermint), use the Cosmos `KVStore` (key-value store) and Cosmos SDK `Keeper` to facilitate state transitions. To support the interface functionality, it imports 4 module Keepers: diff --git a/x/evm/spec/07_events.md b/x/evm/spec/07_events.md index dc3e6a0a..0a749899 100644 --- a/x/evm/spec/07_events.md +++ b/x/evm/spec/07_events.md @@ -16,6 +16,7 @@ The `x/evm` module emits the Cosmos SDK events after a state execution. The EVM | ethereum_tx | `"txHash"` | `{tendermint_hex_hash}` | | ethereum_tx | `"ethereumTxHash"` | `{hex_hash}` | | ethereum_tx | `"txIndex"` | `{tx_index}` | +| ethereum_tx | `"txGasUsed"` | `{gas_used}` | | tx_log | `"txLog"` | `{tx_log}` | | message | `"sender"` | `{eth_address}` | | message | `"action"` | `"ethereum"` | diff --git a/x/evm/types/events.go b/x/evm/types/events.go index 0b8ca69a..d9e85928 100644 --- a/x/evm/types/events.go +++ b/x/evm/types/events.go @@ -11,6 +11,7 @@ const ( AttributeKeyTxHash = "txHash" AttributeKeyEthereumTxHash = "ethereumTxHash" AttributeKeyTxIndex = "txIndex" + AttributeKeyTxGasUsed = "txGasUsed" AttributeKeyTxType = "txType" AttributeKeyTxLog = "txLog" // tx failed in eth vm execution diff --git a/x/evm/types/key.go b/x/evm/types/key.go index 36e5a8fc..ade14eb2 100644 --- a/x/evm/types/key.go +++ b/x/evm/types/key.go @@ -32,6 +32,7 @@ const ( prefixTransientBloom = iota + 1 prefixTransientTxIndex prefixTransientLogSize + prefixTransientGasUsed ) // KVStore key prefixes @@ -45,6 +46,7 @@ var ( KeyPrefixTransientBloom = []byte{prefixTransientBloom} KeyPrefixTransientTxIndex = []byte{prefixTransientTxIndex} KeyPrefixTransientLogSize = []byte{prefixTransientLogSize} + KeyPrefixTransientGasUsed = []byte{prefixTransientGasUsed} ) // AddressStoragePrefix returns a prefix to iterate over a given account storage. diff --git a/x/evm/types/utils.go b/x/evm/types/utils.go index 94b7192f..16b1c780 100644 --- a/x/evm/types/utils.go +++ b/x/evm/types/utils.go @@ -9,6 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) @@ -54,20 +55,22 @@ func DecodeTransactionLogs(data []byte) (TransactionLogs, error) { } // UnwrapEthereumMsg extract MsgEthereumTx from wrapping sdk.Tx -func UnwrapEthereumMsg(tx *sdk.Tx) (*MsgEthereumTx, error) { +func UnwrapEthereumMsg(tx *sdk.Tx, ethHash common.Hash) (*MsgEthereumTx, error) { if tx == nil { return nil, fmt.Errorf("invalid tx: nil") } - if len((*tx).GetMsgs()) != 1 { - return nil, fmt.Errorf("invalid tx type: %T", tx) - } - msg, ok := (*tx).GetMsgs()[0].(*MsgEthereumTx) - if !ok { - return nil, fmt.Errorf("invalid tx type: %T", tx) + for _, msg := range (*tx).GetMsgs() { + ethMsg, ok := msg.(*MsgEthereumTx) + if !ok { + return nil, fmt.Errorf("invalid tx type: %T", tx) + } + if ethMsg.AsTransaction().Hash() == ethHash { + return ethMsg, nil + } } - return msg, nil + return nil, fmt.Errorf("eth tx not found: %s", ethHash) } // BinSearch execute the binary search and hone in on an executable gas limit diff --git a/x/evm/types/utils_test.go b/x/evm/types/utils_test.go index 21aac532..d3ec00e5 100644 --- a/x/evm/types/utils_test.go +++ b/x/evm/types/utils_test.go @@ -48,7 +48,7 @@ func TestEvmDataEncoding(t *testing.T) { } func TestUnwrapEthererumMsg(t *testing.T) { - _, err := evmtypes.UnwrapEthereumMsg(nil) + _, err := evmtypes.UnwrapEthereumMsg(nil, common.Hash{}) require.NotNil(t, err) encodingConfig := encoding.MakeConfig(app.ModuleBasics) @@ -56,14 +56,14 @@ func TestUnwrapEthererumMsg(t *testing.T) { builder, _ := clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) tx := builder.GetTx().(sdk.Tx) - _, err = evmtypes.UnwrapEthereumMsg(&tx) + _, err = evmtypes.UnwrapEthereumMsg(&tx, common.Hash{}) require.NotNil(t, err) msg := evmtypes.NewTx(big.NewInt(1), 0, &common.Address{}, big.NewInt(0), 0, big.NewInt(0), nil, nil, []byte{}, nil) err = builder.SetMsgs(msg) tx = builder.GetTx().(sdk.Tx) - msg_, err := evmtypes.UnwrapEthereumMsg(&tx) + msg_, err := evmtypes.UnwrapEthereumMsg(&tx, msg.AsTransaction().Hash()) require.Nil(t, err) require.Equal(t, msg_, msg) }