From b77aab43bb20bac6d183fdb2a09256db3706a94d Mon Sep 17 00:00:00 2001 From: Federico Kunze <31522760+fedekunze@users.noreply.github.com> Date: Wed, 2 Jun 2021 04:52:53 -0400 Subject: [PATCH] evm: refactor state transition (#41) * evm: keeper statedb refactor * keeper: implement stateDB account, balance, nonce and suicide functions * keeper: implement stateDB code and iterator functions * keeper: implement stateDB log and preimage functions * update code to use CommitStateDB * tests updates * journal changes (wip) * cache fields * journal and logs * minor cleanup * evm: state transition refactor * evm: unpack revert errors * evm: update state transition (wip) * evm: remove journal related changes * evm: delete empty account code and storage state * update gas limit * evm: header hash to/from context * evm: minor params and state transition changes * ante: state transition changes * ante: refactor default sig gas consumer * ante: ignore gas costs from ops other than intrinsic gas * ante: CanTransferDecorator * evm: refund gas * update comments * state transition comments * ante: CanTransfer and AccessList decorator tests * evm: cleanup state transition * ignore nonce increment during ante handler on contract creation * fix ante tests * more test fixes --- app/ante/ante.go | 2 + app/ante/ante_test.go | 10 +- app/ante/eth.go | 186 +++++++++++++++++++--- app/ante/eth_test.go | 168 +++++++++++++++++++- ethereum/rpc/eth_api.go | 49 ++---- types/errors.go | 3 - x/evm/keeper/state_transition.go | 264 +++++++++++++++++++++++++++++++ 7 files changed, 612 insertions(+), 70 deletions(-) create mode 100644 x/evm/keeper/state_transition.go diff --git a/app/ante/ante.go b/app/ante/ante.go index 201de1ac..5d659a93 100644 --- a/app/ante/ante.go +++ b/app/ante/ante.go @@ -67,6 +67,8 @@ func NewAnteHandler( NewEthAccountVerificationDecorator(ak, bankKeeper, evmKeeper), NewEthNonceVerificationDecorator(ak), NewEthGasConsumeDecorator(ak, bankKeeper, evmKeeper), + NewCanTransferDecorator(evmKeeper), + NewAccessListDecorator(evmKeeper), NewEthIncrementSenderSequenceDecorator(ak), // innermost AnteDecorator. ) diff --git a/app/ante/ante_test.go b/app/ante/ante_test.go index 93adbbf9..13311ffd 100644 --- a/app/ante/ante_test.go +++ b/app/ante/ante_test.go @@ -40,7 +40,7 @@ func (suite AnteTestSuite) TestAnteHandler() { { "success - CheckTx (contract)", func() sdk.Tx { - signedContractTx := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 2, big.NewInt(10), 100000, big.NewInt(1), nil, nil) + signedContractTx := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 100000, big.NewInt(1), nil, nil) signedContractTx.From = addr.Hex() tx := suite.CreateTestTx(signedContractTx, privKey, 1) @@ -51,7 +51,7 @@ func (suite AnteTestSuite) TestAnteHandler() { { "success - ReCheckTx (contract)", func() sdk.Tx { - signedContractTx := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 3, big.NewInt(10), 100000, big.NewInt(1), nil, nil) + signedContractTx := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 100000, big.NewInt(1), nil, nil) signedContractTx.From = addr.Hex() tx := suite.CreateTestTx(signedContractTx, privKey, 1) @@ -62,7 +62,7 @@ func (suite AnteTestSuite) TestAnteHandler() { { "success - DeliverTx", func() sdk.Tx { - signedTx := evmtypes.NewMsgEthereumTx(suite.app.EvmKeeper.ChainID(), 4, &to, big.NewInt(10), 100000, big.NewInt(1), nil, nil) + signedTx := evmtypes.NewMsgEthereumTx(suite.app.EvmKeeper.ChainID(), 1, &to, big.NewInt(10), 100000, big.NewInt(1), nil, nil) signedTx.From = addr.Hex() tx := suite.CreateTestTx(signedTx, privKey, 1) @@ -73,7 +73,7 @@ func (suite AnteTestSuite) TestAnteHandler() { { "success - CheckTx", func() sdk.Tx { - signedTx := evmtypes.NewMsgEthereumTx(suite.app.EvmKeeper.ChainID(), 5, &to, big.NewInt(10), 100000, big.NewInt(1), nil, nil) + signedTx := evmtypes.NewMsgEthereumTx(suite.app.EvmKeeper.ChainID(), 2, &to, big.NewInt(10), 100000, big.NewInt(1), nil, nil) signedTx.From = addr.Hex() tx := suite.CreateTestTx(signedTx, privKey, 1) @@ -84,7 +84,7 @@ func (suite AnteTestSuite) TestAnteHandler() { { "success - ReCheckTx", func() sdk.Tx { - signedTx := evmtypes.NewMsgEthereumTx(suite.app.EvmKeeper.ChainID(), 2, &to, big.NewInt(10), 100000, big.NewInt(1), nil, nil) + signedTx := evmtypes.NewMsgEthereumTx(suite.app.EvmKeeper.ChainID(), 3, &to, big.NewInt(10), 100000, big.NewInt(1), nil, nil) signedTx.From = addr.Hex() tx := suite.CreateTestTx(signedTx, privKey, 1) diff --git a/app/ante/eth.go b/app/ante/eth.go index 2495c527..ad7abbef 100644 --- a/app/ante/eth.go +++ b/app/ante/eth.go @@ -12,18 +12,23 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" ) // EVMKeeper defines the expected keeper interface used on the Eth AnteHandler type EVMKeeper interface { + vm.StateDB + ChainID() *big.Int GetParams(ctx sdk.Context) evmtypes.Params GetChainConfig(ctx sdk.Context) (evmtypes.ChainConfig, bool) WithContext(ctx sdk.Context) ResetRefundTransient(ctx sdk.Context) + NewEVM(msg core.Message, config *params.ChainConfig) *vm.EVM } -// EthSigVerificationDecorator validates an ethereum signature +// EthSigVerificationDecorator validates an ethereum signatures type EthSigVerificationDecorator struct { evmKeeper EVMKeeper } @@ -88,7 +93,12 @@ func NewEthAccountVerificationDecorator(ak AccountKeeper, bankKeeper BankKeeper, } } -// AnteHandle validates the signature and returns sender address +// AnteHandle validates checks that the sender balance is greater than the total transaction cost. +// The account will be set to store if it doesn't exis, i.e cannot be found on store. +// This AnteHandler decorator will fail if: +// - any of the msgs is not a MsgEthereumTx +// - from address is empty +// - account balance is lower than the transaction cost func (avd EthAccountVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { if !ctx.IsCheckTx() { return next(ctx, tx, simulate) @@ -114,7 +124,8 @@ func (avd EthAccountVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx acc := avd.ak.GetAccount(infCtx, from) if acc == nil { - _ = avd.ak.NewAccountWithAddress(infCtx, from) + acc = avd.ak.NewAccountWithAddress(infCtx, from) + avd.ak.SetAccount(infCtx, acc) } // validate sender has enough funds to pay for gas cost @@ -143,8 +154,8 @@ func NewEthNonceVerificationDecorator(ak AccountKeeper) EthNonceVerificationDeco } } -// AnteHandle validates that the transaction nonce is valid (equivalent to the sender account’s -// current nonce). +// 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() { @@ -201,10 +212,17 @@ func NewEthGasConsumeDecorator(ak AccountKeeper, bankKeeper BankKeeper, ek EVMKe // AnteHandle validates that the Ethereum tx message has enough to cover intrinsic gas // (during CheckTx only) and that the sender has enough balance to pay for the gas cost. // -// Intrinsic gas for a transaction is the amount of gas -// that the transaction uses before the transaction is executed. The gas is a -// constant value of 21000 plus any cost inccured by additional bytes of data -// supplied with the transaction. +// Intrinsic gas for a transaction is the amount of gas that the transaction uses before the +// transaction is executed. The gas is a constant value plus any cost inccured by additional bytes +// of data supplied with the transaction. +// +// This AnteHandler decorator will fail if: +// - the transaction contains more than one message +// - the message is not a MsgEthereumTx +// - sender account cannot be found +// - transaction's gas limit is lower than the intrinsic gas +// - user doesn't have enough balance to deduct the transaction fees (gas_limit * gas_price) +// - transaction or block gas meter runs out of gas func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { // get and set account must be called with an infinite gas meter in order to prevent // additional gas from being deducted. @@ -214,7 +232,7 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "only 1 ethereum msg supported per tx, got %d", len(tx.GetMsgs())) } - // reset the refund gas value for the current transaction + // reset the refund gas value in the keeper for the current transaction egcd.evmKeeper.ResetRefundTransient(infCtx) config, found := egcd.evmKeeper.GetChainConfig(infCtx) @@ -259,14 +277,14 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula return ctx, sdkerrors.Wrapf(sdkerrors.ErrOutOfGas, "gas limit too low: %d (gas limit) < %d (intrinsic gas)", gasLimit, intrinsicGas) } - // Cost calculates the fees paid to validators based on gas limit and price - cost := msgEthTx.Fee() // fee = gas limit * gas price + // calculate the fees paid to validators based on gas limit and price + feeAmt := msgEthTx.Fee() // fee = gas limit * gas price evmDenom := egcd.evmKeeper.GetParams(infCtx).EvmDenom - feeAmt := sdk.Coins{sdk.NewCoin(evmDenom, sdk.NewIntFromBigInt(cost))} + fees := sdk.Coins{sdk.NewCoin(evmDenom, sdk.NewIntFromBigInt(feeAmt))} // deduct the full gas cost from the user balance - if err := authante.DeductFees(egcd.bankKeeper, infCtx, signerAcc, feeAmt); err != nil { + if err := authante.DeductFees(egcd.bankKeeper, infCtx, signerAcc, fees); err != nil { return ctx, err } @@ -286,11 +304,125 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula return next(ctx, tx, simulate) } -// EthIncrementSenderSequenceDecorator increments the sequence of the signers. The -// main difference with the SDK's IncrementSequenceDecorator is that the MsgEthereumTx -// doesn't implement the SigVerifiableTx interface. +// CanTransferDecorator checks if the sender is allowed to transfer funds according to the EVM block +// context rules. +type CanTransferDecorator struct { + evmKeeper EVMKeeper +} + +// NewCanTransferDecorator creates a new CanTransferDecorator instance. +func NewCanTransferDecorator(evmKeeper EVMKeeper) CanTransferDecorator { + return CanTransferDecorator{ + evmKeeper: evmKeeper, + } +} + +// AnteHandle creates an EVM from the message and calls the BlockContext CanTransfer function to +// see if the address can execute the transaction. +func (ctd CanTransferDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + // get and set account must be called with an infinite gas meter in order to prevent + // additional gas from being deducted. + infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + ctd.evmKeeper.WithContext(infCtx) + + config, found := ctd.evmKeeper.GetChainConfig(infCtx) + if !found { + return ctx, evmtypes.ErrChainConfigNotFound + } + + ethCfg := config.EthereumConfig(ctd.evmKeeper.ChainID()) + + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", msg) + } + + coreMsg, err := msgEthTx.AsMessage() + if err != nil { + return ctx, err + } + + evm := ctd.evmKeeper.NewEVM(coreMsg, ethCfg) + + // 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(ctd.evmKeeper, coreMsg.From(), coreMsg.Value()) { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "address %s", coreMsg.From().Hex()) + } + } + + ctd.evmKeeper.WithContext(ctx) + + // set the original gas meter + return next(ctx, tx, simulate) +} + +// AccessListDecorator prepare an access list for the sender if Yolov3/Berlin/EIPs 2929 and 2930 are +// applicable at the current block number. +type AccessListDecorator struct { + evmKeeper EVMKeeper +} + +// NewAccessListDecorator creates a new AccessListDecorator. +func NewAccessListDecorator(evmKeeper EVMKeeper) AccessListDecorator { + return AccessListDecorator{ + evmKeeper: evmKeeper, + } +} + +// AnteHandle handles the preparatory steps for executing an EVM state transition with +// regards to both EIP-2929 and EIP-2930: // -// CONTRACT: must be called after msg.VerifySig in order to cache the sender address. +// - Add sender to access list (2929) +// - Add destination to access list (2929) +// - Add precompiles to access list (2929) +// - Add the contents of the optional tx access list (2930) +// +// The AnteHandler will only prepare the access list if Yolov3/Berlin/EIPs 2929 and 2930 are applicable at the current number. +func (ald AccessListDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + // get and set account must be called with an infinite gas meter in order to prevent + // additional gas from being deducted. + + infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + + config, found := ald.evmKeeper.GetChainConfig(infCtx) + if !found { + return ctx, evmtypes.ErrChainConfigNotFound + } + + ethCfg := config.EthereumConfig(ald.evmKeeper.ChainID()) + + rules := ethCfg.Rules(big.NewInt(ctx.BlockHeight())) + + // we don't need to prepare the access list if the chain is not currently on the Berlin upgrade + if !rules.IsBerlin { + return next(ctx, tx, simulate) + } + + // setup the keeper context before setting the access list + ald.evmKeeper.WithContext(infCtx) + + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", msg) + } + + coreMsg, err := msgEthTx.AsMessage() + if err != nil { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "tx cannot be expressed as core.Message: %s", err.Error()) + } + + ald.evmKeeper.PrepareAccessList(coreMsg.From(), coreMsg.To(), vm.ActivePrecompiles(rules), coreMsg.AccessList()) + } + + // set the original gas meter + ald.evmKeeper.WithContext(ctx) + return next(ctx, tx, simulate) +} + +// EthIncrementSenderSequenceDecorator increments the sequence of the signers. type EthIncrementSenderSequenceDecorator struct { ak AccountKeeper } @@ -302,16 +434,32 @@ func NewEthIncrementSenderSequenceDecorator(ak AccountKeeper) EthIncrementSender } } -// AnteHandle handles incrementing the sequence of the sender. +// AnteHandle handles incrementing the sequence of the signer (i.e sender). If the transaction is a +// contract creation, the nonce will be incremented during the transaction execution and not within +// this AnteHandler decorator. func (issd EthIncrementSenderSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { // get and set account must be called with an infinite gas meter in order to prevent // additional gas from being deducted. infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", msg) + } + + // NOTE: on contract creation, the nonce is incremented within the EVM Create function during tx execution + // and not previous to the state transition ¯\_(ツ)_/¯ + if msgEthTx.To() == nil { + // contract creation, don't increment sequence on AnteHandler but on tx execution + // continue to the next item + continue + } + // increment sequence of all signers for _, addr := range msg.GetSigners() { acc := issd.ak.GetAccount(infCtx, addr) + if acc == nil { return ctx, sdkerrors.Wrapf( sdkerrors.ErrUnknownAddress, diff --git a/app/ante/eth_test.go b/app/ante/eth_test.go index 5d2a5839..1b6678b5 100644 --- a/app/ante/eth_test.go +++ b/app/ante/eth_test.go @@ -9,6 +9,7 @@ import ( "github.com/cosmos/ethermint/tests" evmtypes "github.com/cosmos/ethermint/x/evm/types" + "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" ) @@ -306,13 +307,147 @@ func (suite AnteTestSuite) TestEthGasConsumeDecorator() { } } +func (suite AnteTestSuite) TestCanTransferDecorator() { + dec := ante.NewCanTransferDecorator(suite.app.EvmKeeper) + + addr, _ := newTestAddrKey() + + tx := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil) + tx2 := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil) + + tx.From = addr.Hex() + + testCases := []struct { + name string + tx sdk.Tx + malleate func() + expPass bool + }{ + {"invalid transaction type", &invalidTx{}, func() {}, false}, + {"AsMessage failed", tx2, func() {}, false}, + { + "evm CanTransfer failed", + tx, + func() { + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + }, + false, + }, + { + "success", + tx, + func() { + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + + err := suite.app.BankKeeper.SetBalance(suite.ctx, addr.Bytes(), sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(10000000000))) + suite.Require().NoError(err) + }, + true, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + + tc.malleate() + + consumed := suite.ctx.GasMeter().GasConsumed() + ctx, err := dec.AnteHandle(suite.ctx.WithIsCheckTx(true), tc.tx, false, nextFn) + suite.Require().Equal(consumed, ctx.GasMeter().GasConsumed()) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + +func (suite AnteTestSuite) TestAccessListDecorator() { + dec := ante.NewAccessListDecorator(suite.app.EvmKeeper) + + addr, _ := newTestAddrKey() + al := ðtypes.AccessList{ + {Address: addr, StorageKeys: []common.Hash{{}}}, + } + + tx := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil) + tx2 := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, al) + tx3 := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil) + + tx.From = addr.Hex() + tx2.From = addr.Hex() + + testCases := []struct { + name string + tx sdk.Tx + malleate func() + expPass bool + }{ + {"invalid transaction type", &invalidTx{}, func() {}, false}, + {"AsMessage failed", tx3, func() {}, false}, + { + "success - no access list", + tx, + func() { + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + + err := suite.app.BankKeeper.SetBalance(suite.ctx, addr.Bytes(), sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(10000000000))) + suite.Require().NoError(err) + }, + true, + }, + { + "success - with access list", + tx2, + func() { + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + + err := suite.app.BankKeeper.SetBalance(suite.ctx, addr.Bytes(), sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(10000000000))) + suite.Require().NoError(err) + }, + true, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + + tc.malleate() + + consumed := suite.ctx.GasMeter().GasConsumed() + ctx, err := dec.AnteHandle(suite.ctx.WithIsCheckTx(true), tc.tx, false, nextFn) + suite.Require().Equal(consumed, ctx.GasMeter().GasConsumed()) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + func (suite AnteTestSuite) TestEthIncrementSenderSequenceDecorator() { dec := ante.NewEthIncrementSenderSequenceDecorator(suite.app.AccountKeeper) addr, privKey := newTestAddrKey() - signedTx := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil) - signedTx.From = addr.Hex() - err := signedTx.Sign(suite.ethSigner, tests.NewSigner(privKey)) + contract := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 0, big.NewInt(10), 1000, big.NewInt(1), nil, nil) + contract.From = addr.Hex() + + to := tests.GenerateAddress() + tx := evmtypes.NewMsgEthereumTx(suite.app.EvmKeeper.ChainID(), 0, &to, big.NewInt(10), 1000, big.NewInt(1), nil, nil) + tx.From = addr.Hex() + + err := contract.Sign(suite.ethSigner, tests.NewSigner(privKey)) + suite.Require().NoError(err) + + err = tx.Sign(suite.ethSigner, tests.NewSigner(privKey)) suite.Require().NoError(err) testCases := []struct { @@ -322,27 +457,39 @@ func (suite AnteTestSuite) TestEthIncrementSenderSequenceDecorator() { expPass bool expPanic bool }{ + { + "invalid transaction type", + &invalidTx{}, + func() {}, + false, false, + }, { "no signers", - evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil), + evmtypes.NewMsgEthereumTx(suite.app.EvmKeeper.ChainID(), 1, &to, big.NewInt(10), 1000, big.NewInt(1), nil, nil), func() {}, false, true, }, { "account not set to store", - signedTx, + tx, func() {}, false, false, }, { - "success", - signedTx, + "success - create contract", + contract, func() { acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) suite.app.AccountKeeper.SetAccount(suite.ctx, acc) }, true, false, }, + { + "success - call", + tx, + func() {}, + true, false, + }, } for _, tc := range testCases { @@ -363,6 +510,13 @@ func (suite AnteTestSuite) TestEthIncrementSenderSequenceDecorator() { if tc.expPass { suite.Require().NoError(err) + msg := tc.tx.(*evmtypes.MsgEthereumTx) + nonce := suite.app.EvmKeeper.GetNonce(addr) + if msg.To() == nil { + suite.Require().Equal(msg.Data.Nonce, nonce) + } else { + suite.Require().Equal(msg.Data.Nonce+1, nonce) + } } else { suite.Require().Error(err) } diff --git a/ethereum/rpc/eth_api.go b/ethereum/rpc/eth_api.go index 75bf544d..50e5d15e 100644 --- a/ethereum/rpc/eth_api.go +++ b/ethereum/rpc/eth_api.go @@ -3,7 +3,6 @@ package rpc import ( "bytes" "context" - "encoding/json" "fmt" "math/big" "strings" @@ -470,30 +469,16 @@ func (e *PublicEthAPI) Call(args rpctypes.CallArgs, blockNr rpctypes.BlockNumber return []byte{}, err } - if len(simRes.Result.Log) > 0 { - var logs []rpctypes.SDKTxLogs - - if err := json.Unmarshal([]byte(simRes.Result.Log), &logs); err != nil { - e.logger.WithError(err).Errorln("failed to unmarshal simRes.Result.Log") - } - - if len(logs) > 0 && logs[0].Log == rpctypes.LogRevertedFlag { - data, err := evmtypes.DecodeTxResponse(simRes.Result.Data) - if err != nil { - e.logger.WithError(err).Warningln("call result decoding failed") - return []byte{}, err - } - - return []byte{}, rpctypes.ErrRevertedWith(data.Ret) - } - } - data, err := evmtypes.DecodeTxResponse(simRes.Result.Data) if err != nil { e.logger.WithError(err).Warningln("call result decoding failed") return []byte{}, err } + if data.Reverted { + return []byte{}, rpctypes.ErrRevertedWith(data.Ret) + } + return (hexutil.Bytes)(data.Ret), nil } @@ -620,25 +605,17 @@ func (e *PublicEthAPI) EstimateGas(args rpctypes.CallArgs) (hexutil.Uint64, erro return 0, err } - if len(simRes.Result.Log) > 0 { - var logs []rpctypes.SDKTxLogs - if err := json.Unmarshal([]byte(simRes.Result.Log), &logs); err != nil { - e.logger.WithError(err).Errorln("failed to unmarshal simRes.Result.Log") - return 0, err - } - - if len(logs) > 0 && logs[0].Log == rpctypes.LogRevertedFlag { - data, err := evmtypes.DecodeTxResponse(simRes.Result.Data) - if err != nil { - e.logger.WithError(err).Warningln("call result decoding failed") - return 0, err - } - - return 0, rpctypes.ErrRevertedWith(data.Ret) - } + data, err := evmtypes.DecodeTxResponse(simRes.Result.Data) + if err != nil { + e.logger.WithError(err).Warningln("call result decoding failed") + return 0, err } - // TODO: change 1000 buffer for more accurate buffer (eg: SDK's gasAdjusted) + if data.Reverted { + return 0, rpctypes.ErrRevertedWith(data.Ret) + } + + // TODO: Add Gas Info from state transition to MsgEthereumTxResponse fields and return that instead estimatedGas := simRes.GasInfo.GasUsed gas := estimatedGas + 200000 diff --git a/types/errors.go b/types/errors.go index 2aaa505a..a80e02e7 100644 --- a/types/errors.go +++ b/types/errors.go @@ -18,9 +18,6 @@ var ( // ErrInvalidChainID returns an error resulting from an invalid chain ID. ErrInvalidChainID = sdkerrors.Register(RootCodespace, 3, "invalid chain ID") - // ErrVMExecution returns an error resulting from an error in EVM execution. - ErrVMExecution = sdkerrors.Register(RootCodespace, 4, "error while executing evm transaction") - // ErrMarshalBigInt returns an error resulting from marshaling a big.Int to a string. ErrMarshalBigInt = sdkerrors.Register(RootCodespace, 5, "cannot marshal big.Int to string") diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go new file mode 100644 index 00000000..ed915017 --- /dev/null +++ b/x/evm/keeper/state_transition.go @@ -0,0 +1,264 @@ +package keeper + +import ( + "errors" + "fmt" + "math/big" + "os" + "time" + + "github.com/cosmos/cosmos-sdk/telemetry" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/cosmos/ethermint/x/evm/types" + + "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" +) + +// NewEVM generates an ethereum VM from the provided Message fields and the ChainConfig. +func (k *Keeper) NewEVM(msg core.Message, config *params.ChainConfig) *vm.EVM { + blockCtx := vm.BlockContext{ + CanTransfer: core.CanTransfer, + Transfer: core.Transfer, + GetHash: k.GetHashFn(), + Coinbase: common.Address{}, // there's no beneficiary since we're not mining + GasLimit: k.ctx.BlockGasMeter().Limit(), + BlockNumber: big.NewInt(k.ctx.BlockHeight()), + Time: big.NewInt(k.ctx.BlockHeader().Time.Unix()), + Difficulty: big.NewInt(0), // unused. Only required in PoW context + } + + txCtx := core.NewEVMTxContext(msg) + vmConfig := k.VMConfig() + + return vm.NewEVM(blockCtx, txCtx, k, config, vmConfig) +} + +// VMConfig creates an EVM configuration from the module parameters and the debug setting. +// The config generated uses the default JumpTable from the EVM. +func (k Keeper) VMConfig() vm.Config { + params := k.GetParams(k.ctx) + + return vm.Config{ + Debug: k.debug, + Tracer: vm.NewJSONLogger(&vm.LogConfig{Debug: k.debug}, os.Stderr), // TODO: consider using the Struct Logger too + NoRecursion: false, // TODO: consider disabling recursion though params + ExtraEips: params.EIPs(), + } +} + +// GetHashFn implements vm.GetHashFunc for Ethermint. It handles 3 cases: +// 1. The requested height matches the current height from context (and thus same epoch number) +// 2. The requested height is from an previous height from the same chain epoch +// 3. The requested height is from a height greater than the latest one +func (k Keeper) GetHashFn() vm.GetHashFunc { + return func(height uint64) common.Hash { + switch { + case k.ctx.BlockHeight() == int64(height): + // Case 1: The requested height matches the one from the context so we can retrieve the header + // hash directly from the context. + // TODO: deprecate field from the keeper on next SDK release + return k.headerHash + + case k.ctx.BlockHeight() > int64(height): + // Case 2: if the chain is not the current height we need to retrieve the hash from the store for the + // current chain epoch. This only applies if the current height is greater than the requested height. + return k.GetHeightHash(k.ctx, height) + + default: + // Case 3: heights greater than the current one returns an empty hash. + return common.Hash{} + } + } +} + +// TransitionDb runs and attempts to perform a state transition with the given transaction (i.e Message), that will +// only be persisted to the underlying KVStore if the transaction does not error. +// +// Gas tracking +// +// Ethereum consumes gas according to the EVM opcodes instead of general reads and writes to store. Because of this, the +// state transition needs to ignore the SDK gas consumption mechanism defined by the GasKVStore and instead consume the +// amount of gas used by the VM execution. The amount of gas used is tracked by the EVM and returned in the execution +// result. +// +// Prior to the execution, the starting tx gas meter is saved and replaced with an infinite gas meter in a new context +// in order to ignore the SDK gas consumption config values (read, write, has, delete). +// After the execution, the gas used from the message execution will be added to the starting gas consumed, taking into +// consideration the amount of gas returned. Finally, the context is updated with the EVM gas consumed value prior to +// returning. +// +// For relevant discussion see: https://github.com/cosmos/cosmos-sdk/discussions/9072 +func (k *Keeper) TransitionDb(msg core.Message) (*types.ExecutionResult, error) { + defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), types.MetricKeyTransitionDB) + + cfg, found := k.GetChainConfig(k.ctx) + if !found { + return nil, types.ErrChainConfigNotFound + } + + evm := k.NewEVM(msg, cfg.EthereumConfig(k.eip155ChainID)) + + // create an ethereum StateTransition instance and run TransitionDb + result, err := k.ApplyMessage(evm, msg) + if err != nil { + return nil, err + } + + return result, nil +} + +// Gas consumption notes (write doc from this) + +// gas = remaining gas = limit - consumed + +// Gas consumption in ethereum: +// 0. Buy gas -> deduct gasLimit * gasPrice from user account +// 0.1 leftover gas = gas limit +// 1. consume intrinsic gas +// 1.1 leftover gas = leftover gas - intrinsic gas +// 2. Exec vm functions by passing the gas (i.e remaining gas) +// 2.1 final leftover gas returned after spending gas from the opcodes jump tables +// 3. Refund amount = max(gasConsumed / 2, gas refund), where gas refund is a local variable + +// TODO: (@fedekunze) currently we consume the entire gas limit in the ante handler, so if a transaction fails +// the amount spent will be grater than the gas spent in an Ethereum tx (i.e here the leftover gas won't be refunded). + +func (k *Keeper) ApplyMessage(evm *vm.EVM, msg core.Message) (*types.ExecutionResult, error) { + var ( + ret []byte // return bytes from evm execution + contract common.Address + contractAddr string + vmErr, err error // vm errors do not effect consensus and are therefore not assigned to err + ) + + sender := vm.AccountRef(msg.From()) + contractCreation := msg.To() == nil + + // transaction gas meter (tracks limit and usage) + gasConsumed := k.ctx.GasMeter().GasConsumed() + leftoverGas := k.ctx.GasMeter().Limit() - k.ctx.GasMeter().GasConsumedToLimit() + + // NOTE: Since CRUD operations on the SDK store consume gasm we need to set up an infinite gas meter so that we only consume + // the gas used by the Ethereum message execution. + // Not setting the infinite gas meter here would mean that we are incurring in additional gas costs + k.WithContext(k.ctx.WithGasMeter(sdk.NewInfiniteGasMeter())) + + // NOTE: gas limit is the GasLimit defied in the message minus the Intrinsic Gas that has already been + // consumed on the AnteHandler. + + // ensure gas is consistent during CheckTx + if k.ctx.IsCheckTx() { + if err := k.checkGasConsumption(msg, gasConsumed, contractCreation); err != nil { + return nil, err + } + } + + if contractCreation { + ret, contract, leftoverGas, vmErr = evm.Create(sender, msg.Data(), leftoverGas, msg.Value()) + contractAddr = contract.Hex() + } else { + ret, leftoverGas, vmErr = evm.Call(sender, *msg.To(), msg.Data(), leftoverGas, msg.Value()) + } + + // refund gas prior to handling the vm error in order to set the updated gas meter + gasConsumed, leftoverGas, err = k.refundGas(msg, leftoverGas) + if err != nil { + return nil, err + } + + if vmErr != nil { + if errors.Is(vmErr, vm.ErrExecutionReverted) { + // unpack the return data bytes from the err if the execution has been reverted on the VM + return nil, types.NewExecErrorWithReson(ret) + } + + // wrap the VM error + return nil, sdkerrors.Wrap(types.ErrVMExecution, vmErr.Error()) + } + + return &types.ExecutionResult{ + Response: &types.MsgEthereumTxResponse{ + ContractAddress: contractAddr, + Ret: ret, + }, + GasInfo: types.GasInfo{ + GasLimit: k.ctx.GasMeter().Limit(), + GasConsumed: gasConsumed, + GasRefunded: leftoverGas, + }, + }, nil +} + +// checkGasConsumption verifies that the amount of gas consumed so far matches the intrinsic gas value. +func (k *Keeper) checkGasConsumption(msg core.Message, gasConsumed uint64, isContractCreation bool) error { + cfg, _ := k.GetChainConfig(k.ctx) + ethCfg := cfg.EthereumConfig(k.eip155ChainID) + + height := big.NewInt(k.ctx.BlockHeight()) + homestead := ethCfg.IsHomestead(height) + istanbul := ethCfg.IsIstanbul(height) + + intrinsicGas, err := core.IntrinsicGas(msg.Data(), msg.AccessList(), isContractCreation, homestead, istanbul) + if err != nil { + // should have already been checked on Ante Handler + return err + } + + if intrinsicGas != gasConsumed { + return fmt.Errorf("inconsistent gas. Expected gas consumption to be %d (intrinsic gas only), got %d", intrinsicGas, gasConsumed) + } + + return nil +} + +// refundGas transfers the leftover gas to the sender of the message, caped to half of the total gas +// consumed in the transaction. Additionally, the function sets the total gas consumed to the value +// returned by the EVM execution, thus ignoring the previous intrinsic gas inconsumed during in the +// AnteHandler. +func (k *Keeper) refundGas(msg core.Message, leftoverGas uint64) (consumed, leftover uint64, err error) { + gasConsumed := msg.Gas() - leftoverGas + + // Apply refund counter, capped to half of the used gas. + refund := gasConsumed / 2 + if refund > k.GetRefund() { + refund = k.GetRefund() + } + + leftoverGas += refund + gasConsumed = msg.Gas() - leftoverGas + + // Return EVM tokens for remaining gas, exchanged at the original rate. + remaining := new(big.Int).Mul(new(big.Int).SetUint64(leftoverGas), msg.GasPrice()) + + switch remaining.Sign() { + case -1: + // negative refund errors + return 0, 0, fmt.Errorf("refunded amount value cannot be negative %d", remaining.Int64()) + case 1: + // positive amount refund + params := k.GetParams(k.ctx) + refundedCoins := sdk.Coins{sdk.NewCoin(params.EvmDenom, sdk.NewIntFromBigInt(remaining))} + + // refund to sender from the fee collector module account, which is the escrow account in charge of collecting tx fees + if err := k.bankKeeper.SendCoinsFromModuleToAccount(k.ctx, authtypes.FeeCollectorName, msg.From().Bytes(), refundedCoins); err != nil { + return 0, 0, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "fee collector account failed to refund fees: %s", err.Error()) + } + default: + // no refund, consume gas and update the tx gas meter + } + + // set the gas consumed into the context with the new gas meter. This gas meter will have the + // original gas limit defined in the msg and will consume the gas now that the amount has been + // refunded + gasMeter := sdk.NewGasMeter(msg.Gas()) + gasMeter.ConsumeGas(gasConsumed, "update gas consumption after refund") + k.WithContext(k.ctx.WithGasMeter(gasMeter)) + + return gasConsumed, leftoverGas, nil +}