From 92791d5f9daf2fa2f81303dd68cf77fb01adcdcd Mon Sep 17 00:00:00 2001 From: Sai Kumar Date: Wed, 20 Apr 2022 17:20:55 +0530 Subject: [PATCH] WIP: trying to migrate auth handlers to middlewares --- app/app.go | 57 +-- app/middleware/eip792.go | 288 +++++++++++++++ app/middleware/eth.go | 697 +++++++++++++++++++++++++++++++++++ app/middleware/interfaces.go | 33 ++ app/middleware/middleware.go | 32 ++ app/middleware/options.go | 175 +++++++++ app/middleware/reject.go | 52 +++ types/gasmeter.go | 8 + x/evm/types/interfaces.go | 1 + 9 files changed, 1321 insertions(+), 22 deletions(-) create mode 100644 app/middleware/eip792.go create mode 100644 app/middleware/eth.go create mode 100644 app/middleware/interfaces.go create mode 100644 app/middleware/middleware.go create mode 100644 app/middleware/options.go create mode 100644 app/middleware/reject.go diff --git a/app/app.go b/app/app.go index c5b84bd0..1183bd5b 100644 --- a/app/app.go +++ b/app/app.go @@ -11,12 +11,16 @@ import ( "github.com/rakyll/statik/fs" "github.com/spf13/cast" + "github.com/tharsis/ethermint/app/middleware" + evmtypes "github.com/tharsis/ethermint/x/evm/types" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/db" + "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/server/api" "github.com/cosmos/cosmos-sdk/server/config" servertypes "github.com/cosmos/cosmos-sdk/server/types" @@ -111,7 +115,6 @@ import ( // evmrest "github.com/tharsis/ethermint/x/evm/client/rest" storetypes "github.com/cosmos/cosmos-sdk/store/v2alpha1" evmkeeper "github.com/tharsis/ethermint/x/evm/keeper" - evmtypes "github.com/tharsis/ethermint/x/evm/types" "github.com/tharsis/ethermint/x/feemarket" feemarketkeeper "github.com/tharsis/ethermint/x/feemarket/keeper" feemarkettypes "github.com/tharsis/ethermint/x/feemarket/types" @@ -608,35 +611,45 @@ func NewEthermintApp( // initialize BaseApp app.SetInitChainer(app.InitChainer) app.SetBeginBlocker(app.BeginBlocker) - - // use Ethermint's custom AnteHandler - - // maxGasWanted := cast.ToUint64(appOpts.Get(srvflags.EVMMaxTxGasWanted)) - // options := ante.HandlerOptions{ - // AccountKeeper: app.AccountKeeper, - // BankKeeper: app.BankKeeper, - // EvmKeeper: app.EvmKeeper, - // FeegrantKeeper: app.FeeGrantKeeper, - // IBCKeeper: app.IBCKeeper, - // FeeMarketKeeper: app.FeeMarketKeeper, - // SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), - // SigGasConsumer: ante.DefaultSigVerificationGasConsumer, - // MaxTxGasWanted: maxGasWanted, - // } - - // if err := options.Validate(); err != nil { - // panic(err) - // } - - // app.SetAnteHandler(ante.NewAnteHandler(options)) app.SetEndBlocker(app.EndBlocker) // app.ScopedIBCKeeper = scopedIBCKeeper // app.ScopedTransferKeeper = scopedTransferKeeper + maxGasWanted := cast.ToUint64(appOpts.Get(srvflags.EVMMaxTxGasWanted)) + options := middleware.HandlerOptions{ + Debug: app.Trace(), + LegacyRouter: app.legacyRouter, + MsgServiceRouter: app.msgSvcRouter, + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + EvmKeeper: app.EvmKeeper, + FeegrantKeeper: app.FeeGrantKeeper, + FeeMarketKeeper: app.FeeMarketKeeper, + SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), + SigGasConsumer: authmiddleware.DefaultSigVerificationGasConsumer, + MaxTxGasWanted: maxGasWanted, + TxDecoder: encodingConfig.TxConfig.TxDecoder(), + } + + app.setTxHandler(options, encodingConfig.TxConfig, cast.ToStringSlice(appOpts.Get(server.FlagIndexEvents))) return app } +func (app *EthermintApp) setTxHandler(options middleware.HandlerOptions, txConfig client.TxConfig, indexEventsStr []string) { + indexEvents := map[string]struct{}{} + for _, e := range indexEventsStr { + indexEvents[e] = struct{}{} + } + + txHandler, err := middleware.NewMiddleware(indexEventsStr, options) + if err != nil { + panic(err) + } + + app.SetTxHandler(txHandler) +} + // Name returns the name of the App func (app *EthermintApp) Name() string { return app.BaseApp.Name() } diff --git a/app/middleware/eip792.go b/app/middleware/eip792.go new file mode 100644 index 00000000..98560c25 --- /dev/null +++ b/app/middleware/eip792.go @@ -0,0 +1,288 @@ +package middleware + +import ( + context "context" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/cosmos/cosmos-sdk/x/auth/middleware" + "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/secp256k1" + "github.com/tharsis/ethermint/crypto/ethsecp256k1" + "github.com/tharsis/ethermint/ethereum/eip712" + ethermint "github.com/tharsis/ethermint/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + evmtypes "github.com/tharsis/ethermint/x/evm/types" +) + +var ethermintCodec codec.ProtoCodecMarshaler + +func init() { + registry := codectypes.NewInterfaceRegistry() + ethermint.RegisterInterfaces(registry) + ethermintCodec = codec.NewProtoCodec(registry) +} + +// Eip712SigVerificationDecorator Verify all signatures for a tx and return an error if any are invalid. Note, +// the Eip712SigVerificationDecorator decorator will not get executed on ReCheck. +// +// CONTRACT: Pubkeys are set in context for all signers before this decorator runs +// CONTRACT: Tx must implement SigVerifiableTx interface +type Eip712SigVerificationDecorator struct { + next tx.Handler + ak evmtypes.AccountKeeper + signModeHandler authsigning.SignModeHandler +} + +var _ tx.Handler = Eip712SigVerificationDecorator{} + +// NewEip712SigVerificationDecorator creates a new Eip712SigVerificationDecorator +func NewEip712SigVerificationDecorator(ak evmtypes.AccountKeeper, signModeHandler authsigning.SignModeHandler) tx.Middleware { + return func(h tx.Handler) tx.Handler { + return Eip712SigVerificationDecorator{ + next: h, + ak: ak, + signModeHandler: signModeHandler, + } + } +} + +// CheckTx implements tx.Handler +func (svd Eip712SigVerificationDecorator) CheckTx(cx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + ctx := sdk.UnwrapSDKContext(cx) + reqTx := req.Tx + + // no need to verify signatures on recheck tx + if ctx.IsReCheckTx() { + return svd.next.CheckTx(ctx, req, checkReq) + } + + sigTx, ok := reqTx.(authsigning.SigVerifiableTx) + if !ok { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "tx %T doesn't implement authsigning.SigVerifiableTx", reqTx) + } + + authSignTx, ok := reqTx.(authsigning.Tx) + if !ok { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "tx %T doesn't implement the authsigning.Tx interface", reqTx) + } + + // stdSigs contains the sequence number, account number, and signatures. + // When simulating, this would just be a 0-length slice. + sigs, err := sigTx.GetSignaturesV2() + if err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, err + } + + signerAddrs := sigTx.GetSigners() + + // EIP712 allows just one signature + if len(sigs) != 1 { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "invalid number of signers (%d); EIP712 signatures allows just one signature", len(sigs)) + } + + // check that signer length and signature length are the same + if len(sigs) != len(signerAddrs) { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "invalid number of signer; expected: %d, got %d", len(signerAddrs), len(sigs)) + } + + // EIP712 has just one signature, avoid looping here and only read index 0 + i := 0 + sig := sigs[i] + + acc, err := middleware.GetSignerAcc(ctx, svd.ak, signerAddrs[i]) + if err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, err + } + + // retrieve pubkey + pubKey := acc.GetPubKey() + if pubKey == nil { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(sdkerrors.ErrInvalidPubKey, "pubkey on account is not set") + } + + // Check account sequence number. + if sig.Sequence != acc.GetSequence() { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf( + sdkerrors.ErrWrongSequence, + "account sequence mismatch, expected %d, got %d", acc.GetSequence(), sig.Sequence, + ) + } + + // retrieve signer data + genesis := ctx.BlockHeight() == 0 + chainID := ctx.ChainID() + + var accNum uint64 + if !genesis { + accNum = acc.GetAccountNumber() + } + + signerData := authsigning.SignerData{ + ChainID: chainID, + AccountNumber: accNum, + Sequence: acc.GetSequence(), + } + + if err := VerifySignature(pubKey, signerData, sig.Data, svd.signModeHandler, authSignTx); err != nil { + errMsg := fmt.Errorf("signature verification failed; please verify account number (%d) and chain-id (%s): %w", accNum, chainID, err) + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, errMsg.Error()) + } + + return svd.next.CheckTx(ctx, req, checkReq) +} + +// DeliverTx implements tx.Handler +func (svd Eip712SigVerificationDecorator) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return svd.next.DeliverTx(ctx, req) +} + +// SimulateTx implements tx.Handler +func (svd Eip712SigVerificationDecorator) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return svd.next.SimulateTx(ctx, req) +} + +// VerifySignature verifies a transaction signature contained in SignatureData abstracting over different signing modes +// and single vs multi-signatures. +func VerifySignature( + pubKey cryptotypes.PubKey, + signerData authsigning.SignerData, + sigData signing.SignatureData, + _ authsigning.SignModeHandler, + tx authsigning.Tx, +) error { + switch data := sigData.(type) { + case *signing.SingleSignatureData: + if data.SignMode != signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON { + return sdkerrors.Wrapf(sdkerrors.ErrNotSupported, "unexpected SignatureData %T: wrong SignMode", sigData) + } + + // Note: this prevents the user from sending thrash data in the signature field + if len(data.Signature) != 0 { + return sdkerrors.Wrap(sdkerrors.ErrTooManySignatures, "invalid signature value; EIP712 must have the cosmos transaction signature empty") + } + + // @contract: this code is reached only when Msg has Web3Tx extension (so this custom Ante handler flow), + // and the signature is SIGN_MODE_LEGACY_AMINO_JSON which is supported for EIP712 for now + + msgs := tx.GetMsgs() + if len(msgs) == 0 { + return sdkerrors.Wrap(sdkerrors.ErrNoSignatures, "tx doesn't contain any msgs to verify signature") + } + + txBytes := legacytx.StdSignBytes( + signerData.ChainID, + signerData.AccountNumber, + signerData.Sequence, + tx.GetTimeoutHeight(), + legacytx.StdFee{ + Amount: tx.GetFee(), + Gas: tx.GetGas(), + }, + msgs, tx.GetMemo(), tx.GetTip(), + ) + + signerChainID, err := ethermint.ParseChainID(signerData.ChainID) + if err != nil { + return sdkerrors.Wrapf(err, "failed to parse chainID: %s", signerData.ChainID) + } + + txWithExtensions, ok := tx.(middleware.HasExtensionOptionsTx) + if !ok { + return sdkerrors.Wrap(sdkerrors.ErrUnknownExtensionOptions, "tx doesnt contain any extensions") + } + opts := txWithExtensions.GetExtensionOptions() + if len(opts) != 1 { + return sdkerrors.Wrap(sdkerrors.ErrUnknownExtensionOptions, "tx doesnt contain expected amount of extension options") + } + + var optIface ethermint.ExtensionOptionsWeb3TxI + + if err := ethermintCodec.UnpackAny(opts[0], &optIface); err != nil { + return sdkerrors.Wrap(err, "failed to proto-unpack ExtensionOptionsWeb3Tx") + } + + extOpt, ok := optIface.(*ethermint.ExtensionOptionsWeb3Tx) + if !ok { + return sdkerrors.Wrap(sdkerrors.ErrInvalidChainID, "unknown extension option") + } + + if extOpt.TypedDataChainID != signerChainID.Uint64() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidChainID, "invalid chainID") + } + + if len(extOpt.FeePayer) == 0 { + return sdkerrors.Wrap(sdkerrors.ErrUnknownExtensionOptions, "no feePayer on ExtensionOptionsWeb3Tx") + } + feePayer, err := sdk.AccAddressFromBech32(extOpt.FeePayer) + if err != nil { + return sdkerrors.Wrap(err, "failed to parse feePayer from ExtensionOptionsWeb3Tx") + } + + feeDelegation := &eip712.FeeDelegationOptions{ + FeePayer: feePayer, + } + + typedData, err := eip712.WrapTxToTypedData(ethermintCodec, extOpt.TypedDataChainID, msgs[0], txBytes, feeDelegation) + if err != nil { + return sdkerrors.Wrap(err, "failed to pack tx data in EIP712 object") + } + + sigHash, err := eip712.ComputeTypedDataHash(typedData) + if err != nil { + return err + } + + feePayerSig := extOpt.FeePayerSig + if len(feePayerSig) != ethcrypto.SignatureLength { + return sdkerrors.Wrap(sdkerrors.ErrorInvalidSigner, "signature length doesn't match typical [R||S||V] signature 65 bytes") + } + + // Remove the recovery offset if needed (ie. Metamask eip712 signature) + if feePayerSig[ethcrypto.RecoveryIDOffset] == 27 || feePayerSig[ethcrypto.RecoveryIDOffset] == 28 { + feePayerSig[ethcrypto.RecoveryIDOffset] -= 27 + } + + feePayerPubkey, err := secp256k1.RecoverPubkey(sigHash, feePayerSig) + if err != nil { + return sdkerrors.Wrap(err, "failed to recover delegated fee payer from sig") + } + + ecPubKey, err := ethcrypto.UnmarshalPubkey(feePayerPubkey) + if err != nil { + return sdkerrors.Wrap(err, "failed to unmarshal recovered fee payer pubkey") + } + + pk := ðsecp256k1.PubKey{ + Key: ethcrypto.CompressPubkey(ecPubKey), + } + + if !pubKey.Equals(pk) { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidPubKey, "feePayer pubkey %s is different from transaction pubkey %s", pubKey, pk) + } + + recoveredFeePayerAcc := sdk.AccAddress(pk.Address().Bytes()) + + if !recoveredFeePayerAcc.Equals(feePayer) { + return sdkerrors.Wrapf(sdkerrors.ErrorInvalidSigner, "failed to verify delegated fee payer %s signature", recoveredFeePayerAcc) + } + + // VerifySignature of ethsecp256k1 accepts 64 byte signature [R||S] + // WARNING! Under NO CIRCUMSTANCES try to use pubKey.VerifySignature there + if !secp256k1.VerifySignature(pubKey.Bytes(), sigHash, feePayerSig[:len(feePayerSig)-1]) { + return sdkerrors.Wrap(sdkerrors.ErrorInvalidSigner, "unable to verify signer signature of EIP712 typed data") + } + + return nil + default: + return sdkerrors.Wrapf(sdkerrors.ErrTooManySignatures, "unexpected SignatureData %T", sigData) + } +} diff --git a/app/middleware/eth.go b/app/middleware/eth.go new file mode 100644 index 00000000..86199d61 --- /dev/null +++ b/app/middleware/eth.go @@ -0,0 +1,697 @@ +package middleware + +import ( + context "context" + "errors" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/x/auth/middleware" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + ethermint "github.com/tharsis/ethermint/types" + evmkeeper "github.com/tharsis/ethermint/x/evm/keeper" + "github.com/tharsis/ethermint/x/evm/statedb" + evmtypes "github.com/tharsis/ethermint/x/evm/types" +) + +// EthSetupContextDecorator is adapted from SetUpContextDecorator from cosmos-sdk, it ignores gas consumption +// by setting the gas meter to infinite +type EthSetupContextDecorator struct { + next tx.Handler + evmKeeper EVMKeeper +} + +// CheckTx implements tx.Handler +func (esc EthSetupContextDecorator) CheckTx(ctx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + sdkCtx, err := gasContext(sdk.UnwrapSDKContext(ctx), req.Tx, false) + if err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, err + } + + // 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(sdkCtx) + return esc.next.CheckTx(ctx, req, checkReq) +} + +// gasContext returns a new context with a gas meter set from a given context. +func gasContext(ctx sdk.Context, tx sdk.Tx, isSimulate bool) (sdk.Context, error) { + // all transactions must implement GasTx + gasTx, ok := tx.(middleware.GasTx) + if !ok { + // Set a gas meter with limit 0 as to prevent an infinite gas meter attack + // during runTx. + newCtx := setGasMeter(ctx, 0, isSimulate) + return newCtx, sdkerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be GasTx") + } + + return setGasMeter(ctx, gasTx.GetGas(), isSimulate), nil +} + +// setGasMeter returns a new context with a gas meter set from a given context. +func setGasMeter(ctx sdk.Context, gasLimit uint64, simulate bool) sdk.Context { + // In various cases such as simulation and during the genesis block, we do not + // meter any gas utilization. + if simulate || ctx.BlockHeight() == 0 { + return ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + } + + return ctx.WithGasMeter(sdk.NewGasMeter(gasLimit)) +} + +// populateGas returns a new tx.Response with gas fields populated. +func populateGas(res tx.Response, sdkCtx sdk.Context) tx.Response { + res.GasWanted = sdkCtx.GasMeter().Limit() + res.GasUsed = sdkCtx.GasMeter().GasConsumed() + + return res +} + +// DeliverTx implements tx.Handler +func (esc EthSetupContextDecorator) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + sdkCtx, err := gasContext(sdk.UnwrapSDKContext(ctx), req.Tx, false) + if err != nil { + return tx.Response{}, err + } + + // 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(sdkCtx) + return esc.next.DeliverTx(ctx, req) +} + +// SimulateTx implements tx.Handler +func (esc EthSetupContextDecorator) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + sdkCtx, err := gasContext(sdk.UnwrapSDKContext(ctx), req.Tx, false) + if err != nil { + return tx.Response{}, err + } + + // 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(sdkCtx) + return esc.next.SimulateTx(ctx, req) +} + +var _ tx.Handler = EthSetupContextDecorator{} + +func NewEthSetUpContextDecorator(evmKeeper EVMKeeper) tx.Middleware { + return func(txh tx.Handler) tx.Handler { + return EthSetupContextDecorator{ + next: txh, + evmKeeper: evmKeeper, + } + } +} + +// 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 { + next tx.Handler + evmKeeper EVMKeeper +} + +// CheckTx implements tx.Handler +func (mfd EthMempoolFeeDecorator) CheckTx(ctx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + + sdkCtx := sdk.UnwrapSDKContext(ctx) + params := mfd.evmKeeper.GetParams(sdkCtx) + ethCfg := params.ChainConfig.EthereumConfig(mfd.evmKeeper.ChainID()) + baseFee := mfd.evmKeeper.BaseFee(sdkCtx, ethCfg) + if baseFee == nil { + for _, msg := range req.Tx.GetMsgs() { + ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + + evmDenom := params.EvmDenom + feeAmt := ethMsg.GetFee() + glDec := sdk.NewDec(int64(ethMsg.GetGas())) + requiredFee := sdkCtx.MinGasPrices().AmountOf(evmDenom).Mul(glDec) + if sdk.NewDecFromBigInt(feeAmt).LT(requiredFee) { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeAmt, requiredFee) + } + } + } + + return mfd.next.CheckTx(ctx, req, checkReq) +} + +// DeliverTx implements tx.Handler +func (mfd EthMempoolFeeDecorator) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return mfd.next.DeliverTx(ctx, req) +} + +// SimulateTx implements tx.Handler +func (mfd EthMempoolFeeDecorator) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return mfd.next.SimulateTx(ctx, req) +} + +var _ tx.Handler = EthMempoolFeeDecorator{} + +func NewEthMempoolFeeDecorator(ek EVMKeeper) tx.Middleware { + return func(txh tx.Handler) tx.Handler { + return EthMempoolFeeDecorator{ + next: txh, + evmKeeper: ek, + } + } +} + +// EthValidateBasicDecorator is adapted from ValidateBasicDecorator from cosmos-sdk, it ignores ErrNoSignatures +type EthValidateBasicDecorator struct { + next tx.Handler + evmKeeper EVMKeeper +} + +// CheckTx implements tx.Handler +func (vbd EthValidateBasicDecorator) CheckTx(cx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + ctx := sdk.UnwrapSDKContext(cx) + reqTx := req.Tx + + // no need to validate basic on recheck tx, call next antehandler + if ctx.IsReCheckTx() { + return vbd.CheckTx(ctx, req, checkReq) + } + + err := reqTx.ValidateBasic() + // ErrNoSignatures is fine with eth tx + if err != nil && !errors.Is(err, sdkerrors.ErrNoSignatures) { + return tx.Response{}, tx.ResponseCheckTx{}, 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 + if wrapperTx, ok := reqTx.(protoTxProvider); ok { + protoTx := wrapperTx.GetProtoTx() + body := protoTx.Body + if body.Memo != "" || body.TimeoutHeight != uint64(0) || len(body.NonCriticalExtensionOptions) > 0 { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, + "for eth tx body Memo TimeoutHeight NonCriticalExtensionOptions should be empty") + } + + if len(body.ExtensionOptions) != 1 { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx length of ExtensionOptions should be 1") + } + + txFee := sdk.Coins{} + txGasLimit := uint64(0) + + for _, msg := range protoTx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + txGasLimit += msgEthTx.GetGas() + + txData, err := evmtypes.UnpackTxData(msgEthTx.Data) + if err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(err, "failed to unpack MsgEthereumTx Data") + } + + 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 tx.Response{}, tx.ResponseCheckTx{}, 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 { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx AuthInfo SignerInfos should be empty") + } + + if authInfo.Fee.Payer != "" || authInfo.Fee.Granter != "" { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx AuthInfo Fee payer and granter should be empty") + } + + if !authInfo.Fee.Amount.IsEqual(txFee) { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid AuthInfo Fee Amount (%s != %s)", authInfo.Fee.Amount, txFee) + } + + if authInfo.Fee.GasLimit != txGasLimit { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid AuthInfo Fee GasLimit (%d != %d)", authInfo.Fee.GasLimit, txGasLimit) + } + + sigs := protoTx.Signatures + if len(sigs) > 0 { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "for eth tx Signatures should be empty") + } + } + + return vbd.next.CheckTx(ctx, req, checkReq) +} + +// DeliverTx implements tx.Handler +func (vbd EthValidateBasicDecorator) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return vbd.next.DeliverTx(ctx, req) +} + +// SimulateTx implements tx.Handler +func (vbd EthValidateBasicDecorator) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return vbd.next.SimulateTx(ctx, req) +} + +var _ tx.Handler = EthValidateBasicDecorator{} + +// NewEthValidateBasicDecorator creates a new EthValidateBasicDecorator +func NewEthValidateBasicDecorator(ek EVMKeeper) tx.Middleware { + return func(h tx.Handler) tx.Handler { + return EthValidateBasicDecorator{ + next: h, + evmKeeper: ek, + } + } + +} + +// EthSigVerificationDecorator validates an ethereum signatures +type EthSigVerificationDecorator struct { + next tx.Handler + evmKeeper EVMKeeper +} + +// CheckTx implements tx.Handler +func (esvd EthSigVerificationDecorator) CheckTx(cx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + chainID := esvd.evmKeeper.ChainID() + ctx := sdk.UnwrapSDKContext(cx) + reqTx := req.Tx + + params := esvd.evmKeeper.GetParams(ctx) + + ethCfg := params.ChainConfig.EthereumConfig(chainID) + blockNum := big.NewInt(ctx.BlockHeight()) + signer := ethtypes.MakeSigner(ethCfg, blockNum) + + for _, msg := range reqTx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + + sender, err := signer.Sender(msgEthTx.AsTransaction()) + if err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, 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 esvd.next.CheckTx(ctx, req, checkReq) +} + +// DeliverTx implements tx.Handler +func (esvd EthSigVerificationDecorator) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return esvd.next.DeliverTx(ctx, req) +} + +// SimulateTx implements tx.Handler +func (esvd EthSigVerificationDecorator) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return esvd.next.SimulateTx(ctx, req) +} + +var _ tx.Handler = EthSigVerificationDecorator{} + +// NewEthSigVerificationDecorator creates a new EthSigVerificationDecorator +func NewEthSigVerificationDecorator(ek EVMKeeper) tx.Middleware { + return func(h tx.Handler) tx.Handler { + return EthSigVerificationDecorator{ + next: h, + evmKeeper: ek, + } + } +} + +// EthAccountVerificationDecorator validates an account balance checks +type EthAccountVerificationDecorator struct { + next tx.Handler + ak evmtypes.AccountKeeper + bankKeeper evmtypes.BankKeeper + evmKeeper EVMKeeper +} + +// CheckTx implements tx.Handler +func (avd EthAccountVerificationDecorator) CheckTx(cx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + reqTx := req.Tx + ctx := sdk.UnwrapSDKContext(cx) + if !ctx.IsCheckTx() { + return avd.next.CheckTx(cx, req, checkReq) + } + + for i, msg := range reqTx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + + txData, err := evmtypes.UnpackTxData(msgEthTx.Data) + if err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, 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 tx.Response{}, tx.ResponseCheckTx{}, 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 tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, + "the sender is not EOA: address %s, codeHash <%s>", fromAddr, acct.CodeHash) + } + + if err := evmkeeper.CheckSenderBalance(sdk.NewIntFromBigInt(acct.Balance), txData); err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(err, "failed to check sender balance") + } + } + return avd.next.CheckTx(ctx, req, checkReq) +} + +// DeliverTx implements tx.Handler +func (avd EthAccountVerificationDecorator) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return avd.next.DeliverTx(ctx, req) +} + +// SimulateTx implements tx.Handler +func (avd EthAccountVerificationDecorator) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return avd.next.SimulateTx(ctx, req) +} + +var _ tx.Handler = EthAccountVerificationDecorator{} + +// NewEthAccountVerificationDecorator creates a new EthAccountVerificationDecorator +func NewEthAccountVerificationDecorator(ak evmtypes.AccountKeeper, bankKeeper evmtypes.BankKeeper, ek EVMKeeper) tx.Middleware { + return func(h tx.Handler) tx.Handler { + return EthAccountVerificationDecorator{ + next: h, + ak: ak, + bankKeeper: bankKeeper, + evmKeeper: ek, + } + } +} + +// EthGasConsumeDecorator validates enough intrinsic gas for the transaction and +// gas consumption. +type EthGasConsumeDecorator struct { + next tx.Handler + evmKeeper EVMKeeper + maxGasWanted uint64 +} + +// CheckTx implements tx.Handler +func (egcd EthGasConsumeDecorator) CheckTx(cx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + ctx := sdk.UnwrapSDKContext(cx) + reqTx := req.Tx + + params := egcd.evmKeeper.GetParams(ctx) + + ethCfg := params.ChainConfig.EthereumConfig(egcd.evmKeeper.ChainID()) + + blockHeight := big.NewInt(ctx.BlockHeight()) + homestead := ethCfg.IsHomestead(blockHeight) + istanbul := ethCfg.IsIstanbul(blockHeight) + london := ethCfg.IsLondon(blockHeight) + evmDenom := params.EvmDenom + gasWanted := uint64(0) + var events sdk.Events + + for _, msg := range reqTx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + + txData, err := evmtypes.UnpackTxData(msgEthTx.Data) + if err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(err, "failed to unpack tx data") + } + + if ctx.IsCheckTx() { + // 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() + } + + fees, err := egcd.evmKeeper.DeductTxCostsFromUserBalance( + ctx, + *msgEthTx, + txData, + evmDenom, + homestead, + istanbul, + london, + ) + if err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(err, "failed to deduct transaction costs from user balance") + } + + events = append(events, sdk.NewEvent(sdk.EventTypeTx, sdk.NewAttribute(sdk.AttributeKeyFee, fees.String()))) + } + + // TODO: change to typed events + ctx.EventManager().EmitEvents(events) + + // TODO: deprecate after https://github.com/cosmos/cosmos-sdk/issues/9514 is fixed on SDK + blockGasLimit := ethermint.BlockGasLimit(ctx) + + // NOTE: safety check + if blockGasLimit > 0 { + // generate a copy of the gas pool (i.e block gas meter) to see if we've run out of gas for this block + // if current gas consumed is greater than the limit, this funcion panics and the error is recovered on the Baseapp + gasPool := sdk.NewGasMeter(blockGasLimit) + gasPool.ConsumeGas(ctx.GasMeter().GasConsumedToLimit(), "gas pool check") + } + + // Set ctx.GasMeter with a limit of GasWanted (gasLimit) + gasConsumed := ctx.GasMeter().GasConsumed() + ctx = ctx.WithGasMeter(ethermint.NewInfiniteGasMeterWithLimit(gasWanted)) + ctx.GasMeter().ConsumeGas(gasConsumed, "copy gas consumed") + return egcd.next.CheckTx(ctx, req, checkReq) +} + +// DeliverTx implements tx.Handler +func (egcd EthGasConsumeDecorator) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return egcd.next.DeliverTx(ctx, req) +} + +// SimulateTx implements tx.Handler +func (egcd EthGasConsumeDecorator) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return egcd.next.SimulateTx(ctx, req) +} + +var _ tx.Handler = EthGasConsumeDecorator{} + +// NewEthGasConsumeDecorator creates a new EthGasConsumeDecorator +func NewEthGasConsumeDecorator( + evmKeeper EVMKeeper, + maxGasWanted uint64, +) tx.Middleware { + return func(h tx.Handler) tx.Handler { + return EthGasConsumeDecorator{ + h, + evmKeeper, + maxGasWanted, + } + } +} + +// CanTransferDecorator checks if the sender is allowed to transfer funds according to the EVM block +// context rules. +type CanTransferDecorator struct { + next tx.Handler + evmKeeper EVMKeeper +} + +// CheckTx implements tx.Handler +func (ctd CanTransferDecorator) CheckTx(cx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + ctx := sdk.UnwrapSDKContext(cx) + reqTx := req.Tx + params := ctd.evmKeeper.GetParams(ctx) + ethCfg := params.ChainConfig.EthereumConfig(ctd.evmKeeper.ChainID()) + signer := ethtypes.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight())) + + for _, msg := range reqTx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + + baseFee := ctd.evmKeeper.BaseFee(ctx, ethCfg) + + coreMsg, err := msgEthTx.AsMessage(signer, baseFee) + if err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf( + err, + "failed to create an ethereum core.Message from signer %T", signer, + ) + } + + // 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 tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf( + sdkerrors.ErrInsufficientFunds, + "failed to transfer %s from address %s using the EVM block context transfer function", + coreMsg.Value(), + coreMsg.From(), + ) + } + + if evmtypes.IsLondon(ethCfg, ctx.BlockHeight()) { + if baseFee == nil { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap( + evmtypes.ErrInvalidBaseFee, + "base fee is supported but evm block context value is nil", + ) + } + if coreMsg.GasFeeCap().Cmp(baseFee) < 0 { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf( + sdkerrors.ErrInsufficientFee, + "max fee per gas less than block base fee (%s < %s)", + coreMsg.GasFeeCap(), baseFee, + ) + } + } + } + + return ctd.next.CheckTx(ctx, req, checkReq) +} + +// DeliverTx implements tx.Handler +func (ctd CanTransferDecorator) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return ctd.next.DeliverTx(ctx, req) +} + +// SimulateTx implements tx.Handler +func (ctd CanTransferDecorator) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return ctd.next.SimulateTx(ctx, req) +} + +var _ tx.Handler = CanTransferDecorator{} + +// NewCanTransferDecorator creates a new CanTransferDecorator instance. +func NewCanTransferDecorator(evmKeeper EVMKeeper) tx.Middleware { + return func(h tx.Handler) tx.Handler { + return CanTransferDecorator{ + next: h, + evmKeeper: evmKeeper, + } + } +} + +// EthIncrementSenderSequenceDecorator increments the sequence of the signers. +type EthIncrementSenderSequenceDecorator struct { + next tx.Handler + ak evmtypes.AccountKeeper +} + +// CheckTx implements tx.Handler +func (issd EthIncrementSenderSequenceDecorator) CheckTx(cx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + ctx := sdk.UnwrapSDKContext(cx) + reqTx := req.Tx + + for _, msg := range reqTx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + + txData, err := evmtypes.UnpackTxData(msgEthTx.Data) + if err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrap(err, "failed to unpack tx data") + } + + // increase sequence of sender + acc := issd.ak.GetAccount(ctx, msgEthTx.GetFrom()) + if acc == nil { + return tx.Response{}, tx.ResponseCheckTx{}, 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 tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf( + sdkerrors.ErrInvalidSequence, + "invalid nonce; got %d, expected %d", txData.GetNonce(), nonce, + ) + } + + if err := acc.SetSequence(nonce + 1); err != nil { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf(err, "failed to set sequence to %d", acc.GetSequence()+1) + } + + issd.ak.SetAccount(ctx, acc) + } + + return issd.next.CheckTx(ctx, req, checkReq) +} + +// DeliverTx implements tx.Handler +func (issd EthIncrementSenderSequenceDecorator) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return issd.next.DeliverTx(ctx, req) + +} + +// SimulateTx implements tx.Handler +func (issd EthIncrementSenderSequenceDecorator) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return issd.next.SimulateTx(ctx, req) +} + +var _ tx.Handler = EthIncrementSenderSequenceDecorator{} + +// NewEthIncrementSenderSequenceDecorator creates a new EthIncrementSenderSequenceDecorator. +func NewEthIncrementSenderSequenceDecorator(ak evmtypes.AccountKeeper) tx.Middleware { + return func(h tx.Handler) tx.Handler { + return EthIncrementSenderSequenceDecorator{ + next: h, + ak: ak, + } + } + +} diff --git a/app/middleware/interfaces.go b/app/middleware/interfaces.go new file mode 100644 index 00000000..b0ff7a76 --- /dev/null +++ b/app/middleware/interfaces.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + tx "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/tharsis/ethermint/x/evm/statedb" + evmtypes "github.com/tharsis/ethermint/x/evm/types" +) + +// EVMKeeper defines the expected keeper interface used on the Eth AnteHandler +type EVMKeeper interface { + statedb.Keeper + + ChainID() *big.Int + GetParams(ctx sdk.Context) evmtypes.Params + NewEVM(ctx sdk.Context, msg core.Message, cfg *evmtypes.EVMConfig, tracer vm.EVMLogger, stateDB vm.StateDB) *vm.EVM + DeductTxCostsFromUserBalance( + ctx sdk.Context, msgEthTx evmtypes.MsgEthereumTx, txData evmtypes.TxData, denom string, homestead, istanbul, london bool, + ) (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 { + GetProtoTx() *tx.Tx +} diff --git a/app/middleware/middleware.go b/app/middleware/middleware.go new file mode 100644 index 00000000..e9a0d883 --- /dev/null +++ b/app/middleware/middleware.go @@ -0,0 +1,32 @@ +package middleware + +import ( + context "context" + + "github.com/cosmos/cosmos-sdk/types/tx" +) + +type MD struct { + next tx.Handler +} + +var _ tx.Handler = MD{} + +func NewMiddleware(indexEventsStr []string, options HandlerOptions) (tx.Handler, error) { + return newEthAuthMiddleware(options) +} + +// CheckTx implements tx.Handler +func (md MD) CheckTx(ctx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + return md.next.CheckTx(ctx, req, checkReq) +} + +// DeliverTx implements tx.Handler +func (md MD) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return md.next.DeliverTx(ctx, req) +} + +// SimulateTx implements tx.Handler +func (md MD) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return md.next.SimulateTx(ctx, req) +} diff --git a/app/middleware/options.go b/app/middleware/options.go new file mode 100644 index 00000000..b67a966a --- /dev/null +++ b/app/middleware/options.go @@ -0,0 +1,175 @@ +package middleware + +import ( + "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + evmtypes "github.com/tharsis/ethermint/x/evm/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authmiddleware "github.com/cosmos/cosmos-sdk/x/auth/middleware" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// HandlerOptions extend the SDK's AnteHandler options by requiring the IBC +// channel keeper, EVM Keeper and Fee Market Keeper. +type HandlerOptions struct { + Debug bool + + // TxDecoder is used to decode the raw tx bytes into a sdk.Tx. + TxDecoder sdk.TxDecoder + + // IndexEvents defines the set of events in the form {eventType}.{attributeKey}, + // which informs Tendermint what to index. If empty, all events will be indexed. + IndexEvents map[string]struct{} + + LegacyRouter sdk.Router + MsgServiceRouter *authmiddleware.MsgServiceRouter + + ExtensionOptionChecker authmiddleware.ExtensionOptionChecker + TxFeeChecker authmiddleware.TxFeeChecker + + AccountKeeper evmtypes.AccountKeeper + BankKeeper evmtypes.BankKeeper + FeeMarketKeeper evmtypes.FeeMarketKeeper + EvmKeeper EVMKeeper + FeegrantKeeper authmiddleware.FeegrantKeeper + SignModeHandler authsigning.SignModeHandler + SigGasConsumer func(meter sdk.GasMeter, sig signing.SignatureV2, params authtypes.Params) error + MaxTxGasWanted uint64 +} + +func (options HandlerOptions) Validate() error { + if options.TxDecoder == nil { + return sdkerrors.Wrap(sdkerrors.ErrLogic, "txDecoder is required for middlewares") + } + if options.SignModeHandler == nil { + return sdkerrors.Wrap(sdkerrors.ErrLogic, "sign mode handler is required for middlewares") + } + if options.AccountKeeper == nil { + return sdkerrors.Wrap(sdkerrors.ErrLogic, "account keeper is required for AnteHandler") + } + if options.BankKeeper == nil { + return sdkerrors.Wrap(sdkerrors.ErrLogic, "bank keeper is required for AnteHandler") + } + if options.SignModeHandler == nil { + return sdkerrors.Wrap(sdkerrors.ErrLogic, "sign mode handler is required for ante builder") + } + if options.FeeMarketKeeper == nil { + return sdkerrors.Wrap(sdkerrors.ErrLogic, "fee market keeper is required for AnteHandler") + } + if options.EvmKeeper == nil { + return sdkerrors.Wrap(sdkerrors.ErrLogic, "evm keeper is required for AnteHandler") + } + return nil +} + +func newEthAuthMiddleware(options HandlerOptions) (tx.Handler, error) { + return authmiddleware.ComposeMiddlewares( + authmiddleware.NewRunMsgsTxHandler(options.MsgServiceRouter, options.LegacyRouter), + NewEthSetUpContextDecorator(options.EvmKeeper), + NewEthMempoolFeeDecorator(options.EvmKeeper), + NewEthValidateBasicDecorator(options.EvmKeeper), + NewEthSigVerificationDecorator(options.EvmKeeper), + NewEthAccountVerificationDecorator(options.AccountKeeper, options.BankKeeper, options.EvmKeeper), + NewEthGasConsumeDecorator(options.EvmKeeper, options.MaxTxGasWanted), + NewCanTransferDecorator(options.EvmKeeper), + NewEthIncrementSenderSequenceDecorator(options.AccountKeeper), + ), nil +} + +func newCosmosAuthMiddleware(options HandlerOptions) (tx.Handler, error) { + + return authmiddleware.ComposeMiddlewares( + authmiddleware.NewRunMsgsTxHandler(options.MsgServiceRouter, options.LegacyRouter), + // reject MsgEthereumTxs + NewRejectMessagesDecorator(), + authmiddleware.NewTxDecoderMiddleware(options.TxDecoder), + // Set a new GasMeter on sdk.Context. + // + // Make sure the Gas middleware is outside of all other middlewares + // that reads the GasMeter. In our case, the Recovery middleware reads + // the GasMeter to populate GasInfo. + authmiddleware.GasTxMiddleware, + // Recover from panics. Panics outside of this middleware won't be + // caught, be careful! + authmiddleware.RecoveryTxMiddleware, + // Choose which events to index in Tendermint. Make sure no events are + // emitted outside of this middleware. + authmiddleware.NewIndexEventsTxMiddleware(options.IndexEvents), + // Reject all extension options other than the ones needed by the feemarket. + authmiddleware.NewExtensionOptionsMiddleware(options.ExtensionOptionChecker), + authmiddleware.ValidateBasicMiddleware, + authmiddleware.TxTimeoutHeightMiddleware, + authmiddleware.ValidateMemoMiddleware(options.AccountKeeper), + authmiddleware.ConsumeTxSizeGasMiddleware(options.AccountKeeper), + // No gas should be consumed in any middleware above in a "post" handler part. See + // ComposeMiddlewares godoc for details. + // `DeductFeeMiddleware` and `IncrementSequenceMiddleware` should be put outside of `WithBranchedStore` middleware, + // so their storage writes are not discarded when tx fails. + authmiddleware.DeductFeeMiddleware(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), + authmiddleware.SetPubKeyMiddleware(options.AccountKeeper), + authmiddleware.ValidateSigCountMiddleware(options.AccountKeeper), + authmiddleware.SigGasConsumeMiddleware(options.AccountKeeper, options.SigGasConsumer), + authmiddleware.SigVerificationMiddleware(options.AccountKeeper, options.SignModeHandler), + authmiddleware.IncrementSequenceMiddleware(options.AccountKeeper), + // Creates a new MultiStore branch, discards downstream writes if the downstream returns error. + // These kinds of middlewares should be put under this: + // - Could return error after messages executed succesfully. + // - Storage writes should be discarded together when tx failed. + authmiddleware.WithBranchedStore, + // Consume block gas. All middlewares whose gas consumption after their `next` handler + // should be accounted for, should go below this middleware. + authmiddleware.ConsumeBlockGasMiddleware, + authmiddleware.NewTipMiddleware(options.BankKeeper), + ), nil +} + +func newCosmosAnteHandlerEip712(options HandlerOptions) (tx.Handler, error) { + + return authmiddleware.ComposeMiddlewares( + authmiddleware.NewRunMsgsTxHandler(options.MsgServiceRouter, options.LegacyRouter), + // reject MsgEthereumTxs + NewRejectMessagesDecorator(), + authmiddleware.NewTxDecoderMiddleware(options.TxDecoder), + // Set a new GasMeter on sdk.Context. + // + // Make sure the Gas middleware is outside of all other middlewares + // that reads the GasMeter. In our case, the Recovery middleware reads + // the GasMeter to populate GasInfo. + authmiddleware.GasTxMiddleware, + // Recover from panics. Panics outside of this middleware won't be + // caught, be careful! + authmiddleware.RecoveryTxMiddleware, + // Choose which events to index in Tendermint. Make sure no events are + // emitted outside of this middleware. + authmiddleware.NewIndexEventsTxMiddleware(options.IndexEvents), + // Reject all extension options other than the ones needed by the feemarket. + authmiddleware.NewExtensionOptionsMiddleware(options.ExtensionOptionChecker), + authmiddleware.ValidateBasicMiddleware, + authmiddleware.TxTimeoutHeightMiddleware, + authmiddleware.ValidateMemoMiddleware(options.AccountKeeper), + authmiddleware.ConsumeTxSizeGasMiddleware(options.AccountKeeper), + // No gas should be consumed in any middleware above in a "post" handler part. See + // ComposeMiddlewares godoc for details. + // `DeductFeeMiddleware` and `IncrementSequenceMiddleware` should be put outside of `WithBranchedStore` middleware, + // so their storage writes are not discarded when tx fails. + authmiddleware.DeductFeeMiddleware(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), + authmiddleware.SetPubKeyMiddleware(options.AccountKeeper), + authmiddleware.ValidateSigCountMiddleware(options.AccountKeeper), + authmiddleware.SigGasConsumeMiddleware(options.AccountKeeper, options.SigGasConsumer), + // Note: signature verification uses EIP instead of the cosmos signature validator + NewEip712SigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), + authmiddleware.IncrementSequenceMiddleware(options.AccountKeeper), + // Creates a new MultiStore branch, discards downstream writes if the downstream returns error. + // These kinds of middlewares should be put under this: + // - Could return error after messages executed succesfully. + // - Storage writes should be discarded together when tx failed. + authmiddleware.WithBranchedStore, + // Consume block gas. All middlewares whose gas consumption after their `next` handler + // should be accounted for, should go below this middleware. + authmiddleware.ConsumeBlockGasMiddleware, + authmiddleware.NewTipMiddleware(options.BankKeeper), + ), nil +} diff --git a/app/middleware/reject.go b/app/middleware/reject.go new file mode 100644 index 00000000..e59e92c3 --- /dev/null +++ b/app/middleware/reject.go @@ -0,0 +1,52 @@ +package middleware + +import ( + context "context" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx" + evmtypes "github.com/tharsis/ethermint/x/evm/types" +) + +// RejectMessagesDecorator prevents invalid msg types from being executed +type RejectMessagesDecorator struct { + next tx.Handler +} + +func NewRejectMessagesDecorator() tx.Middleware { + return func(h tx.Handler) tx.Handler { + return RejectMessagesDecorator{ + next: h, + } + } +} + +// Middleware rejects messages that requires ethereum-specific authentication. +// For example `MsgEthereumTx` requires fee to be deducted in the antehandler in +// order to perform the refund. + +// CheckTx implements tx.Handler +func (rmd RejectMessagesDecorator) CheckTx(ctx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) { + reqTx := req.Tx + for _, msg := range reqTx.GetMsgs() { + if _, ok := msg.(*evmtypes.MsgEthereumTx); ok { + return tx.Response{}, tx.ResponseCheckTx{}, sdkerrors.Wrapf( + sdkerrors.ErrInvalidType, + "MsgEthereumTx needs to be contained within a tx with 'ExtensionOptionsEthereumTx' option", + ) + } + } + return rmd.next.CheckTx(ctx, req, checkReq) +} + +// DeliverTx implements tx.Handler +func (rmd RejectMessagesDecorator) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return rmd.next.DeliverTx(ctx, req) +} + +// SimulateTx implements tx.Handler +func (rmd RejectMessagesDecorator) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) { + return rmd.next.SimulateTx(ctx, req) +} + +var _ tx.Handler = RejectMessagesDecorator{} diff --git a/types/gasmeter.go b/types/gasmeter.go index b9d35edf..751de082 100644 --- a/types/gasmeter.go +++ b/types/gasmeter.go @@ -45,6 +45,14 @@ func (g *infiniteGasMeterWithLimit) Limit() sdk.Gas { return g.limit } +// GasRemaining returns the gas left in the GasMeter. +func (g *infiniteGasMeterWithLimit) GasRemaining() sdk.Gas { + if g.IsPastLimit() { + return 0 + } + return g.limit - g.consumed +} + // addUint64Overflow performs the addition operation on two uint64 integers and // returns a boolean on whether or not the result overflows. func addUint64Overflow(a, b uint64) (uint64, bool) { diff --git a/x/evm/types/interfaces.go b/x/evm/types/interfaces.go index 9664cbf4..e36feaf7 100644 --- a/x/evm/types/interfaces.go +++ b/x/evm/types/interfaces.go @@ -33,6 +33,7 @@ type BankKeeper interface { SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amount sdk.Coins) error } // StakingKeeper returns the historical headers kept in store.