From 9802cbc98ee344d5327de9a929c8bc93ec7bf34a Mon Sep 17 00:00:00 2001 From: Austin Abell Date: Sat, 19 Oct 2019 08:14:38 +0900 Subject: [PATCH] Gas usage implementation (#123) * Set up gas consumption based on gas limit * Convert evm gas meter to be infinite since being ignored * Remove unnecessary declaration * Update fees paid to validators to be function of gas limit and price instead of just gas * added nonce check for node tx execution * Increment account nonce after mempool check * Remove unnecessary nonce increment --- app/ante.go | 141 ++++++++++++++++++++++++-------- x/evm/module.go | 6 +- x/evm/types/state_transition.go | 48 ++++------- 3 files changed, 131 insertions(+), 64 deletions(-) diff --git a/app/ante.go b/app/ante.go index 7a4be48f..fe4e00e8 100644 --- a/app/ante.go +++ b/app/ante.go @@ -13,7 +13,6 @@ import ( emint "github.com/cosmos/ethermint/types" evmtypes "github.com/cosmos/ethermint/x/evm/types" - ethcmn "github.com/ethereum/go-ethereum/common" ethcore "github.com/ethereum/go-ethereum/core" tmcrypto "github.com/tendermint/tendermint/crypto" @@ -41,7 +40,7 @@ func NewAnteHandler(ak auth.AccountKeeper, sk types.SupplyKeeper) sdk.AnteHandle return sdkAnteHandler(ctx, ak, sk, castTx, sim) case *evmtypes.EthereumTxMsg: - return ethAnteHandler(ctx, castTx, ak) + return ethAnteHandler(ctx, ak, sk, castTx, sim) default: return ctx, sdk.ErrInternal(fmt.Sprintf("transaction type invalid: %T", tx)).Result(), true @@ -106,7 +105,6 @@ func sdkAnteHandler( // the first signer pays the transaction fees if !stdTx.Fee.Amount.IsZero() { - // Testing error is in DeductFees res = auth.DeductFees(sk, newCtx, signerAccs[0], stdTx.Fee.Amount) if !res.IsOK() { return newCtx, res, true @@ -193,53 +191,120 @@ func consumeSigGas(meter sdk.GasMeter, pubkey tmcrypto.PubKey) { // perform the same series of checks. The distinction is made in CheckTx to // prevent spam and DoS attacks. func ethAnteHandler( - ctx sdk.Context, ethTxMsg *evmtypes.EthereumTxMsg, ak auth.AccountKeeper, + ctx sdk.Context, ak auth.AccountKeeper, sk types.SupplyKeeper, + ethTxMsg *evmtypes.EthereumTxMsg, sim bool, ) (newCtx sdk.Context, res sdk.Result, abort bool) { + var senderAddr sdk.AccAddress + + // This is done to ignore costs in Ante handler checks + ctx = ctx.WithBlockGasMeter(sdk.NewInfiniteGasMeter()) + if ctx.IsCheckTx() { // Only perform pre-message (Ethereum transaction) execution validation // during CheckTx. Otherwise, during DeliverTx the EVM will handle them. - if res := validateEthTxCheckTx(ctx, ak, ethTxMsg); !res.IsOK() { - return newCtx, res, true + if senderAddr, res = validateEthTxCheckTx(ctx, ak, ethTxMsg); !res.IsOK() { + return ctx, res, true + } + } else { + // This is still currently needed to retrieve the sender address + if senderAddr, res = validateSignature(ctx, ethTxMsg); !res.IsOK() { + return ctx, res, true + } + + // Explicit nonce check is also needed in case of multiple txs with same nonce not being handled + if res := checkNonce(ctx, ak, ethTxMsg, senderAddr); !res.IsOK() { + return ctx, res, true } } - return ctx, sdk.Result{}, false + // Recover and catch out of gas error + defer func() { + if r := recover(); r != nil { + switch rType := r.(type) { + case sdk.ErrorOutOfGas: + log := fmt.Sprintf("out of gas in location: %v", rType.Descriptor) + res = sdk.ErrOutOfGas(log).Result() + res.GasWanted = ethTxMsg.Data.GasLimit + res.GasUsed = ctx.GasMeter().GasConsumed() + abort = true + default: + panic(r) + } + } + }() + + // Fetch sender account from signature + senderAcc, res := auth.GetSignerAcc(ctx, ak, senderAddr) + if !res.IsOK() { + return ctx, res, true + } + + // Charge sender for gas up to limit + if ethTxMsg.Data.GasLimit != 0 { + // Cost calculates the fees paid to validators based on gas limit and price + cost := new(big.Int).Mul(ethTxMsg.Data.Price, new(big.Int).SetUint64(ethTxMsg.Data.GasLimit)) + + res = auth.DeductFees(sk, ctx, senderAcc, sdk.Coins{ + sdk.NewCoin(emint.DenomDefault, sdk.NewIntFromBigInt(cost)), + }) + + if !res.IsOK() { + return ctx, res, true + } + } + + // Set gas meter after ante handler to ignore gaskv costs + newCtx = auth.SetGasMeter(sim, ctx, ethTxMsg.Data.GasLimit) + + gas, _ := ethcore.IntrinsicGas(ethTxMsg.Data.Payload, ethTxMsg.To() == nil, false) + newCtx.GasMeter().ConsumeGas(gas, "eth intrinsic gas") + + return newCtx, sdk.Result{}, false } func validateEthTxCheckTx( ctx sdk.Context, ak auth.AccountKeeper, ethTxMsg *evmtypes.EthereumTxMsg, -) sdk.Result { - - // parse the chainID from a string to a base-10 integer - chainID, ok := new(big.Int).SetString(ctx.ChainID(), 10) - if !ok { - return emint.ErrInvalidChainID(fmt.Sprintf("invalid chainID: %s", ctx.ChainID())).Result() - } - +) (sdk.AccAddress, sdk.Result) { // Validate sufficient fees have been provided that meet a minimum threshold // defined by the proposer (for mempool purposes during CheckTx). if res := ensureSufficientMempoolFees(ctx, ethTxMsg); !res.IsOK() { - return res + return nil, res } // validate enough intrinsic gas if res := validateIntrinsicGas(ethTxMsg); !res.IsOK() { - return res + return nil, res + } + + signer, res := validateSignature(ctx, ethTxMsg) + if !res.IsOK() { + return nil, res + } + + // validate account (nonce and balance checks) + if res := validateAccount(ctx, ak, ethTxMsg, signer); !res.IsOK() { + return nil, res + } + + return sdk.AccAddress(signer.Bytes()), sdk.Result{} +} + +// Validates signature and returns sender address +func validateSignature(ctx sdk.Context, ethTxMsg *evmtypes.EthereumTxMsg) (sdk.AccAddress, sdk.Result) { + // parse the chainID from a string to a base-10 integer + chainID, ok := new(big.Int).SetString(ctx.ChainID(), 10) + if !ok { + return nil, emint.ErrInvalidChainID(fmt.Sprintf("invalid chainID: %s", ctx.ChainID())).Result() } // validate sender/signature signer, err := ethTxMsg.VerifySig(chainID) if err != nil { - return sdk.ErrUnauthorized(fmt.Sprintf("signature verification failed: %s", err)).Result() + return nil, sdk.ErrUnauthorized(fmt.Sprintf("signature verification failed: %s", err)).Result() } - // validate account (nonce and balance checks) - if res := validateAccount(ctx, ak, ethTxMsg, signer); !res.IsOK() { - return res - } - - return sdk.Result{} + return sdk.AccAddress(signer.Bytes()), sdk.Result{} } // validateIntrinsicGas validates that the Ethereum tx message has enough to @@ -265,10 +330,10 @@ func validateIntrinsicGas(ethTxMsg *evmtypes.EthereumTxMsg) sdk.Result { // validateAccount validates the account nonce and that the account has enough // funds to cover the tx cost. func validateAccount( - ctx sdk.Context, ak auth.AccountKeeper, ethTxMsg *evmtypes.EthereumTxMsg, signer ethcmn.Address, + ctx sdk.Context, ak auth.AccountKeeper, ethTxMsg *evmtypes.EthereumTxMsg, signer sdk.AccAddress, ) sdk.Result { - acc := ak.GetAccount(ctx, sdk.AccAddress(signer.Bytes())) + acc := ak.GetAccount(ctx, signer) // on InitChain make sure account number == 0 if ctx.BlockHeight() == 0 && acc.GetAccountNumber() != 0 { @@ -278,12 +343,9 @@ func validateAccount( )).Result() } - // Validate the transaction nonce is valid (equivalent to the sender account’s - // current nonce). - seq := acc.GetSequence() - if ethTxMsg.Data.AccountNonce != seq { - return sdk.ErrInvalidSequence( - fmt.Sprintf("nonce too low; got %d, expected %d", ethTxMsg.Data.AccountNonce, seq)).Result() + // Validate nonce is correct + if res := checkNonce(ctx, ak, ethTxMsg, signer); !res.IsOK() { + return res } // validate sender has enough funds @@ -297,6 +359,21 @@ func validateAccount( return sdk.Result{} } +func checkNonce( + ctx sdk.Context, ak auth.AccountKeeper, ethTxMsg *evmtypes.EthereumTxMsg, signer sdk.AccAddress, +) sdk.Result { + acc := ak.GetAccount(ctx, signer) + // Validate the transaction nonce is valid (equivalent to the sender account’s + // current nonce). + seq := acc.GetSequence() + if ethTxMsg.Data.AccountNonce != seq { + return sdk.ErrInvalidSequence( + fmt.Sprintf("invalid nonce; got %d, expected %d", ethTxMsg.Data.AccountNonce, seq)).Result() + } + + return sdk.Result{} +} + // ensureSufficientMempoolFees verifies that enough fees have been provided by the // Ethereum transaction that meet the minimum threshold set by the block // proposer. diff --git a/x/evm/module.go b/x/evm/module.go index df6f754d..ce8d138a 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -117,7 +117,11 @@ func (am AppModule) BeginBlock(ctx sdk.Context, bl abci.RequestBeginBlock) { // EndBlock function for module at end of block func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { - _, err := am.keeper.csdb.Commit(true) + // Gas costs are handled within msg handler so costs should be ignored + ebCtx := ctx.WithBlockGasMeter(sdk.NewInfiniteGasMeter()) + + // Commit state objects to KV store + _, err := am.keeper.csdb.WithContext(ebCtx).Commit(true) if err != nil { panic(err) } diff --git a/x/evm/types/state_transition.go b/x/evm/types/state_transition.go index 17c2024a..c245a60a 100644 --- a/x/evm/types/state_transition.go +++ b/x/evm/types/state_transition.go @@ -31,6 +31,9 @@ type StateTransition struct { func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int) { contractCreation := st.Recipient == nil + // This gas limit the the transaction gas limit with intrinsic gas subtracted + gasLimit := ctx.GasMeter().Limit() + // Create context for evm context := vm.Context{ CanTransfer: core.CanTransfer, @@ -40,11 +43,17 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int) BlockNumber: big.NewInt(ctx.BlockHeight()), Time: big.NewInt(time.Now().Unix()), Difficulty: big.NewInt(0x30000), // unused - GasLimit: ctx.GasMeter().Limit(), + GasLimit: gasLimit, GasPrice: ctx.MinGasPrices().AmountOf(emint.DenomDefault).Int, } - vmenv := vm.NewEVM(context, st.Csdb.WithContext(ctx), GenerateChainConfig(st.ChainID), vm.Config{}) + // This gas meter is set up to consume gas from gaskv during evm execution and be ignored + evmGasMeter := sdk.NewInfiniteGasMeter() + + vmenv := vm.NewEVM( + context, st.Csdb.WithContext(ctx.WithGasMeter(evmGasMeter)), + GenerateChainConfig(st.ChainID), vm.Config{}, + ) var ( leftOverGas uint64 @@ -54,11 +63,11 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int) ) if contractCreation { - _, addr, leftOverGas, vmerr = vmenv.Create(senderRef, st.Payload, st.GasLimit, st.Amount) + _, addr, leftOverGas, vmerr = vmenv.Create(senderRef, st.Payload, gasLimit, st.Amount) } else { // Increment the nonce for the next transaction st.Csdb.SetNonce(st.Sender, st.Csdb.GetNonce(st.Sender)+1) - _, leftOverGas, vmerr = vmenv.Call(senderRef, *st.Recipient, st.Payload, st.GasLimit, st.Amount) + _, leftOverGas, vmerr = vmenv.Call(senderRef, *st.Recipient, st.Payload, gasLimit, st.Amount) } // handle errors @@ -66,15 +75,13 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int) return emint.ErrVMExecution(vmerr.Error()).Result(), nil } - // Refund remaining gas from tx (Check these values and ensure gas is being consumed correctly) - refundGas(st.Csdb, &leftOverGas, st.GasLimit, context.GasPrice, st.Sender) - - // add balance for the processor of the tx (determine who rewards are being processed to) - // TODO: Double check nothing needs to be done here + // Refunds would happen here, if intended in future st.Csdb.Finalise(true) // Change to depend on config - // TODO: Consume gas from sender + // Consume gas from evm execution + // Out of gas check does not need to be done here since it is done within the EVM execution + ctx.GasMeter().ConsumeGas(gasLimit-leftOverGas, "EVM execution consumption") // Generate bloom filter to be saved in tx receipt data bloomInt := big.NewInt(0) @@ -90,24 +97,3 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int) return sdk.Result{Data: returnData, GasUsed: st.GasLimit - leftOverGas}, bloomInt } - -func refundGas( - st vm.StateDB, gasRemaining *uint64, initialGas uint64, gasPrice *big.Int, - from common.Address, -) { - // Apply refund counter, capped to half of the used gas. - refund := (initialGas - *gasRemaining) / 2 - if refund > st.GetRefund() { - refund = st.GetRefund() - } - *gasRemaining += refund - - // // Return ETH for remaining gas, exchanged at the original rate. - // remaining := new(big.Int).Mul(new(big.Int).SetUint64(*gasRemaining), gasPrice) - // st.AddBalance(from, remaining) - - // // Also return remaining gas to the block gas counter so it is - // // available for the next transaction. - // TODO: Return gas to block gas meter? - // st.gp.AddGas(st.gas) -}