diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b64ab2e..5a762732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (evm) Reject invalid `MsgEthereumTx` wrapping tx * (evm) Fix `SelfDestruct` opcode by deleting account code and state. * (feemarket) [tharsis#855](https://github.com/tharsis/ethermint/pull/855) consistent `BaseFee` check logic. +* (evm) [tharsis#729](https://github.com/tharsis/ethermint/pull/729) Refactor EVM StateDB implementation. ### Improvements diff --git a/app/ante/ante_test.go b/app/ante/ante_test.go index 77bc3bc8..5ca53093 100644 --- a/app/ante/ante_test.go +++ b/app/ante/ante_test.go @@ -23,7 +23,7 @@ func (suite AnteTestSuite) TestAnteHandler() { suite.Require().NoError(acc.SetSequence(1)) suite.app.AccountKeeper.SetAccount(suite.ctx, acc) - suite.app.EvmKeeper.AddBalance(addr, big.NewInt(10000000000)) + suite.app.EvmKeeper.SetBalance(suite.ctx, addr, big.NewInt(10000000000)) suite.app.FeeMarketKeeper.SetBaseFee(suite.ctx, big.NewInt(100)) @@ -577,7 +577,7 @@ func (suite AnteTestSuite) TestAnteHandlerWithDynamicTxFee() { suite.app.AccountKeeper.SetAccount(suite.ctx, acc) suite.ctx = suite.ctx.WithIsCheckTx(tc.checkTx).WithIsReCheckTx(tc.reCheckTx) - suite.app.EvmKeeper.AddBalance(addr, big.NewInt((ethparams.InitialBaseFee+10)*100000)) + suite.app.EvmKeeper.SetBalance(suite.ctx, addr, big.NewInt((ethparams.InitialBaseFee+10)*100000)) _, err := suite.anteHandler(suite.ctx, tc.txFn(), false) if tc.expPass { suite.Require().NoError(err) diff --git a/app/ante/eth.go b/app/ante/eth.go index 75ea445f..4e8a0420 100644 --- a/app/ante/eth.go +++ b/app/ante/eth.go @@ -10,6 +10,7 @@ import ( 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" "github.com/ethereum/go-ethereum/common" @@ -95,9 +96,6 @@ func (avd EthAccountVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx return next(ctx, tx, simulate) } - avd.evmKeeper.WithContext(ctx) - evmDenom := avd.evmKeeper.GetParams(ctx).EvmDenom - for i, msg := range tx.GetMsgs() { msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) if !ok { @@ -117,25 +115,25 @@ func (avd EthAccountVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx // check whether the sender address is EOA fromAddr := common.BytesToAddress(from) - codeHash := avd.evmKeeper.GetCodeHash(fromAddr) - if codeHash != common.BytesToHash(evmtypes.EmptyCodeHash) { + acct, err := avd.evmKeeper.GetAccount(ctx, fromAddr) + if err != nil { return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, - "the sender is not EOA: address <%v>, codeHash <%s>", fromAddr, codeHash) + "the sender is not EthAccount: address %s", fromAddr) } - - acc := avd.ak.GetAccount(ctx, from) - if acc == nil { - acc = avd.ak.NewAccountWithAddress(ctx, from) + if acct == nil { + acc := avd.ak.NewAccountWithAddress(ctx, from) avd.ak.SetAccount(ctx, acc) + acct = statedb.NewEmptyAccount() + } else if acct.IsContract() { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, + "the sender is not EOA: address %s, codeHash <%s>", fromAddr, acct.CodeHash) } - if err := evmkeeper.CheckSenderBalance(ctx, avd.bankKeeper, from, txData, evmDenom); err != nil { + if err := evmkeeper.CheckSenderBalance(sdk.NewIntFromBigInt(acct.Balance), txData); err != nil { return ctx, sdkerrors.Wrap(err, "failed to check sender balance") } } - // recover the original gas meter - avd.evmKeeper.WithContext(ctx) return next(ctx, tx, simulate) } @@ -221,9 +219,6 @@ func NewEthGasConsumeDecorator( // - 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) { - // reset the refund gas value in the keeper for the current transaction - egcd.evmKeeper.ResetRefundTransient(ctx) - params := egcd.evmKeeper.GetParams(ctx) ethCfg := params.ChainConfig.EthereumConfig(egcd.evmKeeper.ChainID()) @@ -278,8 +273,6 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula } // we know that we have enough gas on the pool to cover the intrinsic gas - // set up the updated context to the evm Keeper - egcd.evmKeeper.WithContext(ctx) return next(ctx, tx, simulate) } @@ -301,8 +294,6 @@ func NewCanTransferDecorator(evmKeeper EVMKeeper, fmk evmtypes.FeeMarketKeeper) // 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) { - ctd.evmKeeper.WithContext(ctx) - params := ctd.evmKeeper.GetParams(ctx) ethCfg := params.ChainConfig.EthereumConfig(ctd.evmKeeper.ChainID()) signer := ethtypes.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight())) @@ -330,11 +321,12 @@ func (ctd CanTransferDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate CoinBase: common.Address{}, BaseFee: baseFee, } - evm := ctd.evmKeeper.NewEVM(coreMsg, cfg, evmtypes.NewNoOpTracer()) + 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(ctd.evmKeeper, coreMsg.From(), coreMsg.Value()) { + if coreMsg.Value().Sign() > 0 && !evm.Context.CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) { return ctx, sdkerrors.Wrapf( sdkerrors.ErrInsufficientFunds, "failed to transfer %s from address %s using the EVM block context transfer function", @@ -360,7 +352,6 @@ func (ctd CanTransferDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate } } - // set the original gas meter return next(ctx, tx, simulate) } @@ -422,8 +413,6 @@ func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simu return next(ctx, tx, simulate) } - vbd.evmKeeper.WithContext(ctx) - err := tx.ValidateBasic() // ErrNoSignatures is fine with eth tx if err != nil && !errors.Is(err, sdkerrors.ErrNoSignatures) { diff --git a/app/ante/eth_test.go b/app/ante/eth_test.go index 69f10d82..f8f75fbc 100644 --- a/app/ante/eth_test.go +++ b/app/ante/eth_test.go @@ -7,6 +7,7 @@ import ( "github.com/tharsis/ethermint/app/ante" "github.com/tharsis/ethermint/tests" + "github.com/tharsis/ethermint/x/evm/statedb" evmtypes "github.com/tharsis/ethermint/x/evm/types" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -65,6 +66,8 @@ func (suite AnteTestSuite) TestNewEthAccountVerificationDecorator() { tx := evmtypes.NewTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil, nil, nil) tx.From = addr.Hex() + var vmdb *statedb.StateDB + testCases := []struct { name string tx sdk.Tx @@ -86,7 +89,7 @@ func (suite AnteTestSuite) TestNewEthAccountVerificationDecorator() { tx, func() { // set not as an EOA - suite.app.EvmKeeper.SetCode(addr, []byte("1")) + vmdb.SetCode(addr, []byte("1")) }, true, false, @@ -96,7 +99,7 @@ func (suite AnteTestSuite) TestNewEthAccountVerificationDecorator() { tx, func() { // reset back to EOA - suite.app.EvmKeeper.SetCode(addr, nil) + vmdb.SetCode(addr, nil) }, true, false, @@ -105,7 +108,7 @@ func (suite AnteTestSuite) TestNewEthAccountVerificationDecorator() { "success new account", tx, func() { - suite.app.EvmKeeper.AddBalance(addr, big.NewInt(1000000)) + vmdb.AddBalance(addr, big.NewInt(1000000)) }, true, true, @@ -117,7 +120,7 @@ func (suite AnteTestSuite) TestNewEthAccountVerificationDecorator() { acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) suite.app.AccountKeeper.SetAccount(suite.ctx, acc) - suite.app.EvmKeeper.AddBalance(addr, big.NewInt(1000000)) + vmdb.AddBalance(addr, big.NewInt(1000000)) }, true, true, @@ -126,7 +129,10 @@ func (suite AnteTestSuite) TestNewEthAccountVerificationDecorator() { for _, tc := range testCases { suite.Run(tc.name, func() { + vmdb = suite.StateDB() tc.malleate() + suite.Require().NoError(vmdb.Commit()) + _, err := dec.AnteHandle(suite.ctx.WithIsCheckTx(tc.checkTx), tc.tx, false, nextFn) if tc.expPass { @@ -204,6 +210,8 @@ func (suite AnteTestSuite) TestEthGasConsumeDecorator() { tx2 := evmtypes.NewTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000000, big.NewInt(1), nil, nil, nil, ðtypes.AccessList{{Address: addr, StorageKeys: nil}}) tx2.From = addr.Hex() + var vmdb *statedb.StateDB + testCases := []struct { name string tx sdk.Tx @@ -221,29 +229,20 @@ func (suite AnteTestSuite) TestEthGasConsumeDecorator() { { "gas limit too low", tx, - func() { - acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) - suite.app.AccountKeeper.SetAccount(suite.ctx, acc) - }, + func() {}, false, false, }, { "not enough balance for fees", tx2, - func() { - acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) - suite.app.AccountKeeper.SetAccount(suite.ctx, acc) - }, + func() {}, false, false, }, { "not enough tx gas", tx2, func() { - acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) - suite.app.AccountKeeper.SetAccount(suite.ctx, acc) - - suite.app.EvmKeeper.AddBalance(addr, big.NewInt(1000000)) + vmdb.AddBalance(addr, big.NewInt(1000000)) }, false, true, }, @@ -251,10 +250,7 @@ func (suite AnteTestSuite) TestEthGasConsumeDecorator() { "not enough block gas", tx2, func() { - acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) - suite.app.AccountKeeper.SetAccount(suite.ctx, acc) - - suite.app.EvmKeeper.AddBalance(addr, big.NewInt(1000000)) + vmdb.AddBalance(addr, big.NewInt(1000000)) suite.ctx = suite.ctx.WithBlockGasMeter(sdk.NewGasMeter(1)) }, @@ -264,10 +260,7 @@ func (suite AnteTestSuite) TestEthGasConsumeDecorator() { "success", tx2, func() { - acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) - suite.app.AccountKeeper.SetAccount(suite.ctx, acc) - - suite.app.EvmKeeper.AddBalance(addr, big.NewInt(1000000)) + vmdb.AddBalance(addr, big.NewInt(1000000)) suite.ctx = suite.ctx.WithBlockGasMeter(sdk.NewGasMeter(10000000000000000000)) }, @@ -277,7 +270,9 @@ func (suite AnteTestSuite) TestEthGasConsumeDecorator() { for _, tc := range testCases { suite.Run(tc.name, func() { + vmdb = suite.StateDB() tc.malleate() + suite.Require().NoError(vmdb.Commit()) if tc.expPanic { suite.Require().Panics(func() { @@ -331,6 +326,8 @@ func (suite AnteTestSuite) TestCanTransferDecorator() { err := tx.Sign(suite.ethSigner, tests.NewSigner(privKey)) suite.Require().NoError(err) + var vmdb *statedb.StateDB + testCases := []struct { name string tx sdk.Tx @@ -355,7 +352,7 @@ func (suite AnteTestSuite) TestCanTransferDecorator() { acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) suite.app.AccountKeeper.SetAccount(suite.ctx, acc) - suite.app.EvmKeeper.AddBalance(addr, big.NewInt(1000000)) + vmdb.AddBalance(addr, big.NewInt(1000000)) }, true, }, @@ -363,7 +360,9 @@ func (suite AnteTestSuite) TestCanTransferDecorator() { for _, tc := range testCases { suite.Run(tc.name, func() { + vmdb = suite.StateDB() tc.malleate() + suite.Require().NoError(vmdb.Commit()) _, err := dec.AnteHandle(suite.ctx.WithIsCheckTx(true), tc.tx, false, nextFn) @@ -458,7 +457,7 @@ func (suite AnteTestSuite) TestEthIncrementSenderSequenceDecorator() { txData, err := evmtypes.UnpackTxData(msg.Data) suite.Require().NoError(err) - nonce := suite.app.EvmKeeper.GetNonce(addr) + nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, addr) suite.Require().Equal(txData.GetNonce()+1, nonce) } else { suite.Require().Error(err) diff --git a/app/ante/interfaces.go b/app/ante/interfaces.go index 08bf9c72..f2ab4b7f 100644 --- a/app/ante/interfaces.go +++ b/app/ante/interfaces.go @@ -5,23 +5,20 @@ import ( 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 { - vm.StateDB + statedb.Keeper ChainID() *big.Int GetParams(ctx sdk.Context) evmtypes.Params - WithContext(ctx sdk.Context) - ResetRefundTransient(ctx sdk.Context) - NewEVM(msg core.Message, cfg *evmtypes.EVMConfig, tracer vm.Tracer) *vm.EVM - GetCodeHash(addr common.Address) common.Hash + NewEVM(ctx sdk.Context, msg core.Message, cfg *evmtypes.EVMConfig, tracer vm.Tracer, stateDB vm.StateDB) *vm.EVM DeductTxCostsFromUserBalance( ctx sdk.Context, msgEthTx evmtypes.MsgEthereumTx, txData evmtypes.TxData, denom string, homestead, istanbul, london bool, ) (sdk.Coins, error) diff --git a/app/ante/sigs_test.go b/app/ante/sigs_test.go index 5c685746..f07d0986 100644 --- a/app/ante/sigs_test.go +++ b/app/ante/sigs_test.go @@ -4,6 +4,7 @@ import ( "math/big" "github.com/tharsis/ethermint/tests" + "github.com/tharsis/ethermint/x/evm/statedb" evmtypes "github.com/tharsis/ethermint/x/evm/types" ) @@ -14,11 +15,11 @@ func (suite AnteTestSuite) TestSignatures() { addr, privKey := tests.NewAddrKey() to := tests.GenerateAddress() - acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr.Bytes()) - suite.Require().NoError(acc.SetSequence(1)) - suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + acc := statedb.NewEmptyAccount() + acc.Nonce = 1 + acc.Balance = big.NewInt(10000000000) - suite.app.EvmKeeper.AddBalance(addr, big.NewInt(10000000000)) + suite.app.EvmKeeper.SetAccount(suite.ctx, addr, *acc) msgEthereumTx := evmtypes.NewTx(suite.app.EvmKeeper.ChainID(), 1, &to, big.NewInt(10), 100000, big.NewInt(1), nil, nil, nil, nil) msgEthereumTx.From = addr.Hex() diff --git a/app/ante/utils_test.go b/app/ante/utils_test.go index 3a94e8a2..4a474cd1 100644 --- a/app/ante/utils_test.go +++ b/app/ante/utils_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/suite" @@ -25,6 +26,7 @@ import ( ante "github.com/tharsis/ethermint/app/ante" "github.com/tharsis/ethermint/encoding" "github.com/tharsis/ethermint/tests" + "github.com/tharsis/ethermint/x/evm/statedb" evmtypes "github.com/tharsis/ethermint/x/evm/types" feemarkettypes "github.com/tharsis/ethermint/x/feemarket/types" @@ -43,6 +45,10 @@ type AnteTestSuite struct { enableLondonHF bool } +func (suite *AnteTestSuite) StateDB() *statedb.StateDB { + return statedb.New(suite.ctx, suite.app.EvmKeeper, statedb.NewEmptyTxConfig(common.BytesToHash(suite.ctx.HeaderHash().Bytes()))) +} + func (suite *AnteTestSuite) SetupTest() { checkTx := false diff --git a/tests/importer/importer_test.go b/tests/importer/importer_test.go index f7d74436..e141f40a 100644 --- a/tests/importer/importer_test.go +++ b/tests/importer/importer_test.go @@ -17,6 +17,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" evmkeeper "github.com/tharsis/ethermint/x/evm/keeper" + "github.com/tharsis/ethermint/x/evm/statedb" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/ethash" @@ -139,23 +140,23 @@ func (suite *ImporterTestSuite) TestImportBlocks() { }) ctx := suite.app.NewContext(false, tmheader) ctx = ctx.WithBlockHeight(tmheader.Height) - suite.app.EvmKeeper.WithContext(ctx) + vmdb := statedb.New(ctx, suite.app.EvmKeeper, statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash().Bytes()))) if chainConfig.DAOForkSupport && chainConfig.DAOForkBlock != nil && chainConfig.DAOForkBlock.Cmp(block.Number()) == 0 { - applyDAOHardFork(suite.app.EvmKeeper) + applyDAOHardFork(vmdb) } for _, tx := range block.Transactions() { receipt, gas, err := applyTransaction( - chainConfig, chainContext, nil, gp, suite.app.EvmKeeper, header, tx, usedGas, vmConfig, + ctx, chainConfig, chainContext, nil, gp, suite.app.EvmKeeper, vmdb, header, tx, usedGas, vmConfig, ) suite.Require().NoError(err, "failed to apply tx at block %d; tx: %X; gas %d; receipt:%v", block.NumberU64(), tx.Hash(), gas, receipt) suite.Require().NotNil(receipt) } // apply mining rewards - accumulateRewards(chainConfig, suite.app.EvmKeeper, header, block.Uncles()) + accumulateRewards(chainConfig, vmdb, header, block.Uncles()) // simulate BaseApp EndBlocker commitment endBR := types.RequestEndBlock{Height: tmheader.Height} @@ -173,7 +174,7 @@ func (suite *ImporterTestSuite) TestImportBlocks() { // reward. The total reward consists of the static block reward and rewards for // included uncles. The coinbase of each uncle block is also rewarded. func accumulateRewards( - config *ethparams.ChainConfig, evmKeeper *evmkeeper.Keeper, + config *ethparams.ChainConfig, vmdb ethvm.StateDB, header *ethtypes.Header, uncles []*ethtypes.Header, ) { // select the correct block reward based on chain progression @@ -191,12 +192,12 @@ func accumulateRewards( r.Sub(r, header.Number) r.Mul(r, blockReward) r.Div(r, rewardBig8) - evmKeeper.AddBalance(uncle.Coinbase, r) + vmdb.AddBalance(uncle.Coinbase, r) r.Div(blockReward, rewardBig32) reward.Add(reward, r) } - evmKeeper.AddBalance(header.Coinbase, reward) + vmdb.AddBalance(header.Coinbase, reward) } // ApplyDAOHardFork modifies the state database according to the DAO hard-fork @@ -205,15 +206,15 @@ func accumulateRewards( // Code is pulled from go-ethereum 1.9 because the StateDB interface does not include the // SetBalance function implementation // Ref: https://github.com/ethereum/go-ethereum/blob/52f2461774bcb8cdd310f86b4bc501df5b783852/consensus/misc/dao.go#L74 -func applyDAOHardFork(evmKeeper *evmkeeper.Keeper) { +func applyDAOHardFork(vmdb ethvm.StateDB) { // Retrieve the contract to refund balances into - if !evmKeeper.Exist(ethparams.DAORefundContract) { - evmKeeper.CreateAccount(ethparams.DAORefundContract) + if !vmdb.Exist(ethparams.DAORefundContract) { + vmdb.CreateAccount(ethparams.DAORefundContract) } // Move every DAO account and extra-balance account funds into the refund contract for _, addr := range ethparams.DAODrainList() { - evmKeeper.AddBalance(ethparams.DAORefundContract, evmKeeper.GetBalance(addr)) + vmdb.AddBalance(ethparams.DAORefundContract, vmdb.GetBalance(addr)) } } @@ -224,8 +225,8 @@ func applyDAOHardFork(evmKeeper *evmkeeper.Keeper) { // Function is also pulled from go-ethereum 1.9 because of the incompatible usage // Ref: https://github.com/ethereum/go-ethereum/blob/52f2461774bcb8cdd310f86b4bc501df5b783852/core/state_processor.go#L88 func applyTransaction( - config *ethparams.ChainConfig, bc ethcore.ChainContext, author *common.Address, - gp *ethcore.GasPool, evmKeeper *evmkeeper.Keeper, header *ethtypes.Header, + ctx sdk.Context, config *ethparams.ChainConfig, bc ethcore.ChainContext, author *common.Address, + gp *ethcore.GasPool, evmKeeper *evmkeeper.Keeper, vmdb *statedb.StateDB, header *ethtypes.Header, tx *ethtypes.Transaction, usedGas *uint64, cfg ethvm.Config, ) (*ethtypes.Receipt, uint64, error) { msg, err := tx.AsMessage(ethtypes.MakeSigner(config, header.Number), sdk.ZeroInt().BigInt()) @@ -239,7 +240,7 @@ func applyTransaction( // Create a new environment which holds all relevant information // about the transaction and calling mechanisms. - vmenv := ethvm.NewEVM(blockCtx, txCtx, evmKeeper, config, cfg) + vmenv := ethvm.NewEVM(blockCtx, txCtx, vmdb, config, cfg) // Apply the transaction to the current state (included in the env) execResult, err := ethcore.ApplyMessage(vmenv, msg, gp) @@ -263,11 +264,11 @@ func applyTransaction( } // Set the receipt logs and create a bloom for filtering - receipt.Logs = evmKeeper.GetTxLogsTransient(tx.Hash()) + receipt.Logs = vmdb.Logs() receipt.Bloom = ethtypes.CreateBloom(ethtypes.Receipts{receipt}) receipt.BlockHash = header.Hash() receipt.BlockNumber = header.Number - receipt.TransactionIndex = uint(evmKeeper.GetTxIndexTransient()) + receipt.TransactionIndex = uint(evmKeeper.GetTxIndexTransient(ctx)) return receipt, execResult.UsedGas, err } diff --git a/x/evm/genesis.go b/x/evm/genesis.go index a54fd794..557e3d6f 100644 --- a/x/evm/genesis.go +++ b/x/evm/genesis.go @@ -22,7 +22,6 @@ func InitGenesis( accountKeeper types.AccountKeeper, data types.GenesisState, ) []abci.ValidatorUpdate { - k.WithContext(ctx) k.WithChainID(ctx) k.SetParams(ctx, data.Params) @@ -55,10 +54,10 @@ func InitGenesis( if !bytes.Equal(common.HexToHash(ethAcct.CodeHash).Bytes(), codeHash.Bytes()) { panic("code don't match codeHash") } - k.SetCode(address, code) + k.SetCode(ctx, codeHash.Bytes(), code) for _, storage := range account.Storage { - k.SetState(address, common.HexToHash(storage.Key), common.HexToHash(storage.Value)) + k.SetState(ctx, address, common.HexToHash(storage.Key), common.HexToHash(storage.Value).Bytes()) } } @@ -67,8 +66,6 @@ func InitGenesis( // ExportGenesis exports genesis state of the EVM module func ExportGenesis(ctx sdk.Context, k *keeper.Keeper, ak types.AccountKeeper) *types.GenesisState { - k.WithContext(ctx) - var ethGenAccounts []types.GenesisAccount ak.IterateAccounts(ctx, func(account authtypes.AccountI) bool { ethAccount, ok := account.(*ethermint.EthAccount) @@ -79,14 +76,11 @@ func ExportGenesis(ctx sdk.Context, k *keeper.Keeper, ak types.AccountKeeper) *t addr := ethAccount.EthAddress() - storage, err := k.GetAccountStorage(ctx, addr) - if err != nil { - panic(err) - } + storage := k.GetAccountStorage(ctx, addr) genAccount := types.GenesisAccount{ Address: addr.String(), - Code: common.Bytes2Hex(k.GetCode(addr)), + Code: common.Bytes2Hex(k.GetCode(ctx, ethAccount.GetCodeHash())), Storage: storage, } diff --git a/x/evm/genesis_test.go b/x/evm/genesis_test.go index e4506552..9a35390b 100644 --- a/x/evm/genesis_test.go +++ b/x/evm/genesis_test.go @@ -8,6 +8,7 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/tharsis/ethermint/crypto/ethsecp256k1" "github.com/tharsis/ethermint/x/evm" + "github.com/tharsis/ethermint/x/evm/statedb" "github.com/tharsis/ethermint/x/evm/types" ) @@ -17,6 +18,8 @@ func (suite *EvmTestSuite) TestInitGenesis() { address := common.HexToAddress(privkey.PubKey().Address().String()) + var vmdb *statedb.StateDB + testCases := []struct { name string malleate func() @@ -32,11 +35,7 @@ func (suite *EvmTestSuite) TestInitGenesis() { { "valid account", func() { - acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, address.Bytes()) - suite.Require().NotNil(acc) - - suite.app.EvmKeeper.AddBalance(address, big.NewInt(1)) - suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + vmdb.AddBalance(address, big.NewInt(1)) }, &types.GenesisState{ Params: types.DefaultParams(), @@ -102,8 +101,10 @@ func (suite *EvmTestSuite) TestInitGenesis() { for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() // reset values + vmdb = suite.StateDB() tc.malleate() + vmdb.Commit() if tc.expPanic { suite.Require().Panics( diff --git a/x/evm/handler_test.go b/x/evm/handler_test.go index b8f96ac2..e80791be 100644 --- a/x/evm/handler_test.go +++ b/x/evm/handler_test.go @@ -33,6 +33,7 @@ import ( "github.com/tharsis/ethermint/tests" ethermint "github.com/tharsis/ethermint/types" "github.com/tharsis/ethermint/x/evm" + "github.com/tharsis/ethermint/x/evm/statedb" "github.com/tharsis/ethermint/x/evm/types" "github.com/tendermint/tendermint/crypto/tmhash" @@ -137,7 +138,6 @@ func (suite *EvmTestSuite) DoSetupTest(t require.TestingT) { ConsensusHash: tmhash.Sum([]byte("consensus")), LastResultsHash: tmhash.Sum([]byte("last_result")), }) - suite.app.EvmKeeper.WithContext(suite.ctx) queryHelper := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry()) types.RegisterQueryServer(queryHelper, suite.app.EvmKeeper) @@ -173,6 +173,10 @@ func (suite *EvmTestSuite) SignTx(tx *types.MsgEthereumTx) { suite.Require().NoError(err) } +func (suite *EvmTestSuite) StateDB() *statedb.StateDB { + return statedb.New(suite.ctx, suite.app.EvmKeeper, statedb.NewEmptyTxConfig(common.BytesToHash(suite.ctx.HeaderHash().Bytes()))) +} + func TestEvmTestSuite(t *testing.T) { suite.Run(t, new(EvmTestSuite)) } @@ -230,7 +234,6 @@ func (suite *EvmTestSuite) TestHandleMsgEthereumTx() { suite.SetupTest() // reset //nolint tc.malleate() - suite.app.EvmKeeper.Snapshot() res, err := suite.handler(suite.ctx, tx) //nolint @@ -282,16 +285,6 @@ func (suite *EvmTestSuite) TestHandlerLogs() { suite.Require().Equal(len(txResponse.Logs), 1) suite.Require().Equal(len(txResponse.Logs[0].Topics), 2) - - tlogs := types.LogsToEthereum(txResponse.Logs) - for _, log := range tlogs { - suite.app.EvmKeeper.AddLogTransient(log) - } - suite.Require().NoError(err) - - logs := suite.app.EvmKeeper.GetTxLogsTransient(tlogs[0].TxHash) - - suite.Require().Equal(logs, tlogs) } func (suite *EvmTestSuite) TestDeployAndCallContract() { @@ -510,7 +503,7 @@ func (suite *EvmTestSuite) TestErrorWhenDeployContract() { func (suite *EvmTestSuite) deployERC20Contract() common.Address { k := suite.app.EvmKeeper - nonce := k.GetNonce(suite.from) + nonce := k.GetNonce(suite.ctx, suite.from) ctorArgs, err := types.ERC20Contract.ABI.Pack("", suite.from, big.NewInt(10000000000)) suite.Require().NoError(err) msg := ethtypes.NewMessage( @@ -526,7 +519,7 @@ func (suite *EvmTestSuite) deployERC20Contract() common.Address { nil, true, ) - rsp, err := k.ApplyMessage(msg, nil, true) + rsp, err := k.ApplyMessage(suite.ctx, msg, nil, true) suite.Require().NoError(err) suite.Require().False(rsp.Failed()) return crypto.CreateAddress(suite.from, nonce) @@ -571,14 +564,14 @@ func (suite *EvmTestSuite) TestERC20TransferReverted() { k.SetHooks(tc.hooks) // add some fund to pay gas fee - k.AddBalance(suite.from, big.NewInt(10000000000)) + k.SetBalance(suite.ctx, suite.from, big.NewInt(10000000000)) contract := suite.deployERC20Contract() data, err := types.ERC20Contract.ABI.Pack("transfer", suite.from, big.NewInt(10)) suite.Require().NoError(err) - nonce := k.GetNonce(suite.from) + nonce := k.GetNonce(suite.ctx, suite.from) tx := types.NewTx( suite.chainID, nonce, @@ -593,7 +586,7 @@ func (suite *EvmTestSuite) TestERC20TransferReverted() { ) suite.SignTx(tx) - before := k.GetBalance(suite.from) + before := k.GetBalance(suite.ctx, suite.from) txData, err := types.UnpackTxData(tx.Data) suite.Require().NoError(err) @@ -606,7 +599,7 @@ func (suite *EvmTestSuite) TestERC20TransferReverted() { suite.Require().True(res.Failed()) suite.Require().Equal(tc.expErr, res.VmError) - after := k.GetBalance(suite.from) + after := k.GetBalance(suite.ctx, suite.from) if tc.expErr == "out of gas" { suite.Require().Equal(tc.gasLimit, res.GasUsed) @@ -618,7 +611,8 @@ func (suite *EvmTestSuite) TestERC20TransferReverted() { suite.Require().Equal(big.NewInt(int64(res.GasUsed)), new(big.Int).Sub(before, after)) // nonce should not be increased. - suite.Require().Equal(nonce, k.GetNonce(suite.from)) + nonce2 := k.GetNonce(suite.ctx, suite.from) + suite.Require().Equal(nonce, nonce2) }) } } @@ -650,7 +644,7 @@ func (suite *EvmTestSuite) TestContractDeploymentRevert() { // test with different hooks scenarios k.SetHooks(tc.hooks) - nonce := k.GetNonce(suite.from) + nonce := k.GetNonce(suite.ctx, suite.from) ctorArgs, err := types.ERC20Contract.ABI.Pack("", suite.from, big.NewInt(0)) suite.Require().NoError(err) @@ -667,14 +661,17 @@ func (suite *EvmTestSuite) TestContractDeploymentRevert() { suite.SignTx(tx) // simulate nonce increment in ante handler - k.SetNonce(suite.from, nonce+1) + db := suite.StateDB() + db.SetNonce(suite.from, nonce+1) + suite.Require().NoError(db.Commit()) rsp, err := k.EthereumTx(sdk.WrapSDKContext(suite.ctx), tx) suite.Require().NoError(err) suite.Require().True(rsp.Failed()) // nonce don't change - suite.Require().Equal(nonce+1, k.GetNonce(suite.from)) + nonce2 := k.GetNonce(suite.ctx, suite.from) + suite.Require().Equal(nonce+1, nonce2) }) } } diff --git a/x/evm/keeper/abci.go b/x/evm/keeper/abci.go index 939680c6..e619b626 100644 --- a/x/evm/keeper/abci.go +++ b/x/evm/keeper/abci.go @@ -10,7 +10,6 @@ import ( // BeginBlock sets the sdk Context and EIP155 chain id to the Keeper. func (k *Keeper) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { - k.WithContext(ctx) k.WithChainID(ctx) } @@ -20,12 +19,9 @@ func (k *Keeper) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { func (k *Keeper) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.ValidatorUpdate { // Gas costs are handled within msg handler so costs should be ignored infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) - k.WithContext(infCtx) - bloom := ethtypes.BytesToBloom(k.GetBlockBloomTransient().Bytes()) + bloom := ethtypes.BytesToBloom(k.GetBlockBloomTransient(infCtx).Bytes()) k.EmitBlockBloomEvent(infCtx, bloom) - k.WithContext(ctx) - return []abci.ValidatorUpdate{} } diff --git a/x/evm/keeper/benchmark_test.go b/x/evm/keeper/benchmark_test.go index c3126815..c4461f86 100644 --- a/x/evm/keeper/benchmark_test.go +++ b/x/evm/keeper/benchmark_test.go @@ -12,7 +12,6 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" ethermint "github.com/tharsis/ethermint/types" - "github.com/tharsis/ethermint/x/evm/keeper" "github.com/tharsis/ethermint/x/evm/types" ) @@ -81,7 +80,7 @@ func BenchmarkTokenTransfer(b *testing.B) { DoBenchmark(b, func(suite *KeeperTestSuite, contract common.Address) *types.MsgEthereumTx { input, err := types.ERC20Contract.ABI.Pack("transfer", common.HexToAddress("0x378c50D9264C63F3F92B806d4ee56E9D86FfB3Ec"), big.NewInt(1000)) require.NoError(b, err) - nonce := suite.app.EvmKeeper.GetNonce(suite.address) + nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) return types.NewTx(suite.app.EvmKeeper.ChainID(), nonce, &contract, big.NewInt(0), 410000, big.NewInt(1), nil, nil, input, nil) }) } @@ -90,7 +89,7 @@ func BenchmarkEmitLogs(b *testing.B) { DoBenchmark(b, func(suite *KeeperTestSuite, contract common.Address) *types.MsgEthereumTx { input, err := types.ERC20Contract.ABI.Pack("benchmarkLogs", big.NewInt(1000)) require.NoError(b, err) - nonce := suite.app.EvmKeeper.GetNonce(suite.address) + nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) return types.NewTx(suite.app.EvmKeeper.ChainID(), nonce, &contract, big.NewInt(0), 4100000, big.NewInt(1), nil, nil, input, nil) }) } @@ -99,7 +98,7 @@ func BenchmarkTokenTransferFrom(b *testing.B) { DoBenchmark(b, func(suite *KeeperTestSuite, contract common.Address) *types.MsgEthereumTx { input, err := types.ERC20Contract.ABI.Pack("transferFrom", suite.address, common.HexToAddress("0x378c50D9264C63F3F92B806d4ee56E9D86FfB3Ec"), big.NewInt(0)) require.NoError(b, err) - nonce := suite.app.EvmKeeper.GetNonce(suite.address) + nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) return types.NewTx(suite.app.EvmKeeper.ChainID(), nonce, &contract, big.NewInt(0), 410000, big.NewInt(1), nil, nil, input, nil) }) } @@ -108,7 +107,7 @@ func BenchmarkTokenMint(b *testing.B) { DoBenchmark(b, func(suite *KeeperTestSuite, contract common.Address) *types.MsgEthereumTx { input, err := types.ERC20Contract.ABI.Pack("mint", common.HexToAddress("0x378c50D9264C63F3F92B806d4ee56E9D86FfB3Ec"), big.NewInt(1000)) require.NoError(b, err) - nonce := suite.app.EvmKeeper.GetNonce(suite.address) + nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) return types.NewTx(suite.app.EvmKeeper.ChainID(), nonce, &contract, big.NewInt(0), 410000, big.NewInt(1), nil, nil, input, nil) }) } @@ -118,7 +117,7 @@ func BenchmarkMessageCall(b *testing.B) { input, err := types.TestMessageCall.ABI.Pack("benchmarkMessageCall", big.NewInt(10000)) require.NoError(b, err) - nonce := suite.app.EvmKeeper.GetNonce(suite.address) + nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) msg := types.NewTx(suite.app.EvmKeeper.ChainID(), nonce, &contract, big.NewInt(0), 25000000, big.NewInt(1), nil, nil, input, nil) msg.From = suite.address.Hex() @@ -143,40 +142,3 @@ func BenchmarkMessageCall(b *testing.B) { require.False(b, rsp.Failed()) } } - -func DoBenchmarkDeepContextStack(b *testing.B, depth int) { - begin := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - end := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} - - suite := KeeperTestSuite{} - suite.DoSetupTest(b) - - transientKey := suite.app.GetTKey(types.TransientKey) - - var stack keeper.ContextStack - stack.Reset(suite.ctx) - - for i := 0; i < depth; i++ { - stack.Snapshot() - - store := stack.CurrentContext().TransientStore(transientKey) - store.Set(begin, []byte("value")) - } - - store := stack.CurrentContext().TransientStore(transientKey) - for i := 0; i < b.N; i++ { - store.Iterator(begin, end) - } -} - -func BenchmarkDeepContextStack1(b *testing.B) { - DoBenchmarkDeepContextStack(b, 1) -} - -func BenchmarkDeepContextStack10(b *testing.B) { - DoBenchmarkDeepContextStack(b, 10) -} - -func BenchmarkDeepContextStack13(b *testing.B) { - DoBenchmarkDeepContextStack(b, 13) -} diff --git a/x/evm/keeper/context_stack.go b/x/evm/keeper/context_stack.go deleted file mode 100644 index f832bd1e..00000000 --- a/x/evm/keeper/context_stack.go +++ /dev/null @@ -1,109 +0,0 @@ -package keeper - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// cachedContext is a pair of cache context and its corresponding commit method. -// They are obtained from the return value of `context.CacheContext()`. -type cachedContext struct { - ctx sdk.Context - commit func() -} - -// ContextStack manages the initial context and a stack of cached contexts, -// to support the `StateDB.Snapshot` and `StateDB.RevertToSnapshot` methods. -type ContextStack struct { - // Context of the initial state before transaction execution. - // It's the context used by `StateDB.CommitedState`. - initialCtx sdk.Context - cachedContexts []cachedContext -} - -// CurrentContext returns the top context of cached stack, -// if the stack is empty, returns the initial context. -func (cs *ContextStack) CurrentContext() sdk.Context { - l := len(cs.cachedContexts) - if l == 0 { - return cs.initialCtx - } - return cs.cachedContexts[l-1].ctx -} - -// Reset sets the initial context and clear the cache context stack. -func (cs *ContextStack) Reset(ctx sdk.Context) { - cs.initialCtx = ctx - if len(cs.cachedContexts) > 0 { - cs.cachedContexts = []cachedContext{} - } -} - -// IsEmpty returns true if the cache context stack is empty. -func (cs *ContextStack) IsEmpty() bool { - return len(cs.cachedContexts) == 0 -} - -// Commit commits all the cached contexts from top to bottom in order and clears the stack by setting an empty slice of cache contexts. -func (cs *ContextStack) Commit() { - // commit in order from top to bottom - for i := len(cs.cachedContexts) - 1; i >= 0; i-- { - // keep all the cosmos events - cs.initialCtx.EventManager().EmitEvents(cs.cachedContexts[i].ctx.EventManager().Events()) - if cs.cachedContexts[i].commit == nil { - panic(fmt.Sprintf("commit function at index %d should not be nil", i)) - } else { - cs.cachedContexts[i].commit() - } - } - cs.cachedContexts = []cachedContext{} -} - -// CommitToRevision commit the cache after the target revision, -// to improve efficiency of db operations. -func (cs *ContextStack) CommitToRevision(target int) error { - if target < 0 || target >= len(cs.cachedContexts) { - return fmt.Errorf("snapshot index %d out of bound [%d..%d)", target, 0, len(cs.cachedContexts)) - } - - targetCtx := cs.cachedContexts[target].ctx - // commit in order from top to bottom - for i := len(cs.cachedContexts) - 1; i > target; i-- { - // keep all the cosmos events - targetCtx.EventManager().EmitEvents(cs.cachedContexts[i].ctx.EventManager().Events()) - if cs.cachedContexts[i].commit == nil { - return fmt.Errorf("commit function at index %d should not be nil", i) - } - cs.cachedContexts[i].commit() - } - cs.cachedContexts = cs.cachedContexts[0 : target+1] - - return nil -} - -// Snapshot pushes a new cached context to the stack, -// and returns the index of it. -func (cs *ContextStack) Snapshot() int { - i := len(cs.cachedContexts) - ctx, commit := cs.CurrentContext().CacheContext() - cs.cachedContexts = append(cs.cachedContexts, cachedContext{ctx: ctx, commit: commit}) - return i -} - -// RevertToSnapshot pops all the cached contexts after the target index (inclusive). -// the target should be snapshot index returned by `Snapshot`. -// This function panics if the index is out of bounds. -func (cs *ContextStack) RevertToSnapshot(target int) { - if target < 0 || target >= len(cs.cachedContexts) { - panic(fmt.Errorf("snapshot index %d out of bound [%d..%d)", target, 0, len(cs.cachedContexts))) - } - cs.cachedContexts = cs.cachedContexts[:target] -} - -// RevertAll discards all the cache contexts. -func (cs *ContextStack) RevertAll() { - if len(cs.cachedContexts) > 0 { - cs.RevertToSnapshot(0) - } -} diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index 0eaebbc1..32a78ea5 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -23,6 +23,7 @@ import ( ethparams "github.com/ethereum/go-ethereum/params" ethermint "github.com/tharsis/ethermint/types" + "github.com/tharsis/ethermint/x/evm/statedb" "github.com/tharsis/ethermint/x/evm/types" ) @@ -47,12 +48,14 @@ func (k Keeper) Account(c context.Context, req *types.QueryAccountRequest) (*typ addr := common.HexToAddress(req.Address) ctx := sdk.UnwrapSDKContext(c) - k.WithContext(ctx) - + acct, err := k.GetAccountOrEmpty(ctx, addr) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } return &types.QueryAccountResponse{ - Balance: k.GetBalance(addr).String(), - CodeHash: k.GetCodeHash(addr).Hex(), - Nonce: k.GetNonce(addr), + Balance: acct.Balance.String(), + CodeHash: common.BytesToHash(acct.CodeHash).Hex(), + Nonce: acct.Nonce, }, nil } @@ -68,7 +71,6 @@ func (k Keeper) CosmosAccount(c context.Context, req *types.QueryCosmosAccountRe } ctx := sdk.UnwrapSDKContext(c) - k.WithContext(ctx) ethAddr := common.HexToAddress(req.Address) cosmosAddr := sdk.AccAddress(ethAddr.Bytes()) @@ -99,7 +101,6 @@ func (k Keeper) ValidatorAccount(c context.Context, req *types.QueryValidatorAcc } ctx := sdk.UnwrapSDKContext(c) - k.WithContext(ctx) validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, consAddr) if !found { @@ -135,9 +136,8 @@ func (k Keeper) Balance(c context.Context, req *types.QueryBalanceRequest) (*typ } ctx := sdk.UnwrapSDKContext(c) - k.WithContext(ctx) - balanceInt := k.GetBalance(common.HexToAddress(req.Address)) + balanceInt := k.GetBalance(ctx, common.HexToAddress(req.Address)) return &types.QueryBalanceResponse{ Balance: balanceInt.String(), @@ -158,12 +158,11 @@ func (k Keeper) Storage(c context.Context, req *types.QueryStorageRequest) (*typ } ctx := sdk.UnwrapSDKContext(c) - k.WithContext(ctx) address := common.HexToAddress(req.Address) key := common.HexToHash(req.Key) - state := k.GetState(address, key) + state := k.GetState(ctx, address, key) stateHex := state.Hex() return &types.QueryStorageResponse{ @@ -185,10 +184,17 @@ func (k Keeper) Code(c context.Context, req *types.QueryCodeRequest) (*types.Que } ctx := sdk.UnwrapSDKContext(c) - k.WithContext(ctx) address := common.HexToAddress(req.Address) - code := k.GetCode(address) + acct, err := k.GetAccountWithoutBalance(ctx, address) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + var code []byte + if acct != nil && acct.IsContract() { + code = k.GetCode(ctx, common.BytesToHash(acct.CodeHash)) + } return &types.QueryCodeResponse{ Code: code, @@ -212,7 +218,6 @@ func (k Keeper) EthCall(c context.Context, req *types.EthCallRequest) (*types.Ms } ctx := sdk.UnwrapSDKContext(c) - k.WithContext(ctx) var args types.TransactionArgs err := json.Unmarshal(req.Args, &args) @@ -226,7 +231,7 @@ func (k Keeper) EthCall(c context.Context, req *types.EthCallRequest) (*types.Ms } // ApplyMessageWithConfig expect correct nonce set in msg - nonce := k.GetNonce(args.GetFrom()) + nonce := k.GetNonce(ctx, args.GetFrom()) args.Nonce = (*hexutil.Uint64)(&nonce) msg, err := args.ToMessage(req.GasCap, cfg.BaseFee) @@ -234,7 +239,10 @@ func (k Keeper) EthCall(c context.Context, req *types.EthCallRequest) (*types.Ms return nil, status.Error(codes.InvalidArgument, err.Error()) } - res, err := k.ApplyMessageWithConfig(msg, nil, false, cfg) + txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash())) + + // pass false to not commit StateDB + res, err := k.ApplyMessageWithConfig(ctx, msg, nil, false, cfg, txConfig) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } @@ -249,7 +257,6 @@ func (k Keeper) EstimateGas(c context.Context, req *types.EthCallRequest) (*type } ctx := sdk.UnwrapSDKContext(c) - k.WithContext(ctx) if req.GasCap < ethparams.TxGas { return nil, status.Error(codes.InvalidArgument, "gas cap cannot be lower than 21,000") @@ -295,23 +302,22 @@ func (k Keeper) EstimateGas(c context.Context, req *types.EthCallRequest) (*type } // ApplyMessageWithConfig expect correct nonce set in msg - nonce := k.GetNonce(args.GetFrom()) + nonce := k.GetNonce(ctx, args.GetFrom()) args.Nonce = (*hexutil.Uint64)(&nonce) + txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash().Bytes())) + // Create a helper to check if a gas allowance results in an executable transaction executable := func(gas uint64) (vmerror bool, rsp *types.MsgEthereumTxResponse, err error) { args.Gas = (*hexutil.Uint64)(&gas) - // Reset to the initial context - k.WithContext(ctx) - msg, err := args.ToMessage(req.GasCap, cfg.BaseFee) if err != nil { return false, nil, err } - rsp, err = k.ApplyMessageWithConfig(msg, nil, false, cfg) - + // pass false to not commit StateDB + rsp, err = k.ApplyMessageWithConfig(ctx, msg, nil, false, cfg, txConfig) if err != nil { if errors.Is(err, core.ErrIntrinsicGas) { return true, nil, nil // Special case, raise gas limit @@ -363,30 +369,33 @@ func (k Keeper) TraceTx(c context.Context, req *types.QueryTraceTxRequest) (*typ ctx = ctx.WithBlockHeight(req.BlockNumber) ctx = ctx.WithBlockTime(req.BlockTime) ctx = ctx.WithHeaderHash(common.Hex2Bytes(req.BlockHash)) - k.WithContext(ctx) cfg, err := k.EVMConfig(ctx) if err != nil { - return nil, status.Error(codes.Internal, "failed to load evm config") + return nil, status.Errorf(codes.Internal, "failed to load evm config: %s", err.Error()) } signer := ethtypes.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) + txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash().Bytes())) for i, tx := range req.Predecessors { ethTx := tx.AsTransaction() msg, err := ethTx.AsMessage(signer, cfg.BaseFee) if err != nil { continue } - k.SetTxHashTransient(ethTx.Hash()) - k.SetTxIndexTransient(uint64(i)) - - if _, err := k.ApplyMessageWithConfig(msg, types.NewNoOpTracer(), true, cfg); err != nil { + txConfig.TxHash = ethTx.Hash() + txConfig.TxIndex = uint(i) + rsp, err := k.ApplyMessageWithConfig(ctx, msg, types.NewNoOpTracer(), true, cfg, txConfig) + if err != nil { continue } + txConfig.LogIndex += uint(len(rsp.Logs)) } tx := req.Msg.AsTransaction() - result, err := k.traceTx(ctx, cfg, signer, req.TxIndex, tx, req.TraceConfig, false) + txConfig.TxHash = tx.Hash() + txConfig.TxIndex++ + result, _, err := k.traceTx(ctx, cfg, txConfig, signer, tx, req.TraceConfig, false) if err != nil { // error will be returned with detail status from traceTx return nil, err @@ -418,7 +427,6 @@ func (k Keeper) TraceBlock(c context.Context, req *types.QueryTraceBlockRequest) ctx = ctx.WithBlockHeight(req.BlockNumber) ctx = ctx.WithBlockTime(req.BlockTime) ctx = ctx.WithHeaderHash(common.Hex2Bytes(req.BlockHash)) - k.WithContext(ctx) cfg, err := k.EVMConfig(ctx) if err != nil { @@ -428,14 +436,18 @@ func (k Keeper) TraceBlock(c context.Context, req *types.QueryTraceBlockRequest) txsLength := len(req.Txs) results := make([]*types.TxTraceResult, 0, txsLength) + txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash().Bytes())) for i, tx := range req.Txs { result := types.TxTraceResult{} ethTx := tx.AsTransaction() - traceResult, err := k.traceTx(ctx, cfg, signer, uint64(i), ethTx, req.TraceConfig, true) + txConfig.TxHash = ethTx.Hash() + txConfig.TxIndex = uint(i) + traceResult, logIndex, err := k.traceTx(ctx, cfg, txConfig, signer, ethTx, req.TraceConfig, true) if err != nil { result.Error = err.Error() continue } + txConfig.LogIndex = logIndex result.Result = traceResult results = append(results, &result) } @@ -450,15 +462,16 @@ func (k Keeper) TraceBlock(c context.Context, req *types.QueryTraceBlockRequest) }, nil } +// traceTx do trace on one transaction, it returns a tuple: (traceResult, nextLogIndex, error). func (k *Keeper) traceTx( ctx sdk.Context, cfg *types.EVMConfig, + txConfig statedb.TxConfig, signer ethtypes.Signer, - txIndex uint64, tx *ethtypes.Transaction, traceConfig *types.TraceConfig, commitMessage bool, -) (*interface{}, error) { +) (*interface{}, uint, error) { // Assemble the structured logger or the JavaScript tracer var ( tracer vm.Tracer @@ -468,11 +481,9 @@ func (k *Keeper) traceTx( msg, err := tx.AsMessage(signer, cfg.BaseFee) if err != nil { - return nil, status.Error(codes.Internal, err.Error()) + return nil, 0, status.Error(codes.Internal, err.Error()) } - txHash := tx.Hash() - if traceConfig != nil && traceConfig.Overrides != nil { overrides = traceConfig.Overrides.EthereumConfig(cfg.ChainConfig.ChainID) } @@ -485,19 +496,19 @@ func (k *Keeper) traceTx( if traceConfig.Timeout != "" { timeout, err = time.ParseDuration(traceConfig.Timeout) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "timeout value: %s", err.Error()) + return nil, 0, status.Errorf(codes.InvalidArgument, "timeout value: %s", err.Error()) } } tCtx := &tracers.Context{ - BlockHash: k.GetHashFn()(uint64(ctx.BlockHeight())), - TxIndex: int(txIndex), - TxHash: txHash, + BlockHash: txConfig.BlockHash, + TxIndex: int(txConfig.TxIndex), + TxHash: txConfig.TxHash, } // Construct the JavaScript tracer to execute with if tracer, err = tracers.New(traceConfig.Tracer, tCtx); err != nil { - return nil, status.Error(codes.Internal, err.Error()) + return nil, 0, status.Error(codes.Internal, err.Error()) } // Handle timeouts and RPC cancellations @@ -526,12 +537,9 @@ func (k *Keeper) traceTx( tracer = types.NewTracer(types.TracerStruct, msg, cfg.ChainConfig, ctx.BlockHeight()) } - k.SetTxHashTransient(txHash) - k.SetTxIndexTransient(txIndex) - - res, err := k.ApplyMessageWithConfig(msg, tracer, commitMessage, cfg) + res, err := k.ApplyMessageWithConfig(ctx, msg, tracer, commitMessage, cfg, txConfig) if err != nil { - return nil, status.Error(codes.Internal, err.Error()) + return nil, 0, status.Error(codes.Internal, err.Error()) } var result interface{} @@ -549,12 +557,12 @@ func (k *Keeper) traceTx( case *tracers.Tracer: result, err = tracer.GetResult() if err != nil { - return nil, status.Error(codes.Internal, err.Error()) + return nil, 0, status.Error(codes.Internal, err.Error()) } default: - return nil, status.Errorf(codes.InvalidArgument, "invalid tracer type %T", tracer) + return nil, 0, status.Errorf(codes.InvalidArgument, "invalid tracer type %T", tracer) } - return &result, nil + return &result, txConfig.LogIndex + uint(len(res.Logs)), nil } diff --git a/x/evm/keeper/grpc_query_test.go b/x/evm/keeper/grpc_query_test.go index 582c8f74..1f495d04 100644 --- a/x/evm/keeper/grpc_query_test.go +++ b/x/evm/keeper/grpc_query_test.go @@ -7,7 +7,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" + "github.com/tharsis/ethermint/x/evm/statedb" sdk "github.com/cosmos/cosmos-sdk/types" @@ -232,12 +234,12 @@ func (suite *KeeperTestSuite) TestQueryStorage() { testCases := []struct { msg string - malleate func() + malleate func(vm.StateDB) expPass bool }{ { "invalid address", - func() { + func(vm.StateDB) { req = &types.QueryStorageRequest{ Address: invalidAddress, } @@ -246,11 +248,11 @@ func (suite *KeeperTestSuite) TestQueryStorage() { }, { "success", - func() { + func(vmdb vm.StateDB) { key := common.BytesToHash([]byte("key")) value := common.BytesToHash([]byte("value")) expValue = value.String() - suite.app.EvmKeeper.SetState(suite.address, key, value) + vmdb.SetState(suite.address, key, value) req = &types.QueryStorageRequest{ Address: suite.address.String(), Key: key.String(), @@ -264,7 +266,10 @@ func (suite *KeeperTestSuite) TestQueryStorage() { suite.Run(fmt.Sprintf("Case %s", tc.msg), func() { suite.SetupTest() // reset - tc.malleate() + vmdb := suite.StateDB() + tc.malleate(vmdb) + suite.Require().NoError(vmdb.Commit()) + ctx := sdk.WrapSDKContext(suite.ctx) res, err := suite.queryClient.Storage(ctx, req) @@ -288,12 +293,12 @@ func (suite *KeeperTestSuite) TestQueryCode() { testCases := []struct { msg string - malleate func() + malleate func(vm.StateDB) expPass bool }{ { "invalid address", - func() { + func(vm.StateDB) { req = &types.QueryCodeRequest{ Address: invalidAddress, } @@ -304,9 +309,9 @@ func (suite *KeeperTestSuite) TestQueryCode() { }, { "success", - func() { + func(vmdb vm.StateDB) { expCode = []byte("code") - suite.app.EvmKeeper.SetCode(suite.address, expCode) + vmdb.SetCode(suite.address, expCode) req = &types.QueryCodeRequest{ Address: suite.address.String(), @@ -320,7 +325,10 @@ func (suite *KeeperTestSuite) TestQueryCode() { suite.Run(fmt.Sprintf("Case %s", tc.msg), func() { suite.SetupTest() // reset - tc.malleate() + vmdb := suite.StateDB() + tc.malleate(vmdb) + suite.Require().NoError(vmdb.Commit()) + ctx := sdk.WrapSDKContext(suite.ctx) res, err := suite.queryClient.Code(ctx, req) @@ -338,26 +346,25 @@ func (suite *KeeperTestSuite) TestQueryCode() { func (suite *KeeperTestSuite) TestQueryTxLogs() { var ( - txHash common.Hash expLogs []*types.Log ) + txHash := common.BytesToHash([]byte("tx_hash")) + txIndex := uint(1) + logIndex := uint(1) testCases := []struct { msg string - malleate func() + malleate func(vm.StateDB) }{ { "empty logs", - func() { - txHash = common.BytesToHash([]byte("hash")) + func(vm.StateDB) { expLogs = nil }, }, { "success", - func() { - txHash = common.BytesToHash([]byte("tx_hash")) - + func(vmdb vm.StateDB) { expLogs = []*types.Log{ { Address: suite.address.String(), @@ -365,17 +372,15 @@ func (suite *KeeperTestSuite) TestQueryTxLogs() { Data: []byte("data"), BlockNumber: 1, TxHash: txHash.String(), - TxIndex: 1, + TxIndex: uint64(txIndex), BlockHash: common.BytesToHash(suite.ctx.HeaderHash()).Hex(), - Index: 0, + Index: uint64(logIndex), Removed: false, }, } - suite.app.EvmKeeper.SetTxHashTransient(txHash) - suite.app.EvmKeeper.IncreaseTxIndexTransient() for _, log := range types.LogsToEthereum(expLogs) { - suite.app.EvmKeeper.AddLog(log) + vmdb.AddLog(log) } }, }, @@ -385,8 +390,11 @@ func (suite *KeeperTestSuite) TestQueryTxLogs() { suite.Run(fmt.Sprintf("Case %s", tc.msg), func() { suite.SetupTest() // reset - tc.malleate() - logs := suite.app.EvmKeeper.GetTxLogsTransient(txHash) + vmdb := statedb.New(suite.ctx, suite.app.EvmKeeper, statedb.NewTxConfig(common.BytesToHash(suite.ctx.HeaderHash().Bytes()), txHash, txIndex, logIndex)) + tc.malleate(vmdb) + suite.Require().NoError(vmdb.Commit()) + + logs := vmdb.Logs() suite.Require().Equal(expLogs, types.NewLogsFromEth(logs)) }) } @@ -680,8 +688,11 @@ func (suite *KeeperTestSuite) TestTraceTx() { malleate: func() { txIndex = 1 traceConfig = nil + // increase nonce to avoid address collision - suite.app.EvmKeeper.SetNonce(suite.address, suite.app.EvmKeeper.GetNonce(suite.address)+1) + vmdb := suite.StateDB() + vmdb.SetNonce(suite.address, vmdb.GetNonce(suite.address)+1) + suite.Require().NoError(vmdb.Commit()) contractAddr := suite.DeployTestContract(suite.T(), suite.address, sdk.NewIntWithDecimal(1000, 18).BigInt()) suite.Commit() @@ -807,8 +818,12 @@ func (suite *KeeperTestSuite) TestTraceBlock() { msg: "tracer with multiple transactions", malleate: func() { traceConfig = nil + // increase nonce to avoid address collision - suite.app.EvmKeeper.SetNonce(suite.address, suite.app.EvmKeeper.GetNonce(suite.address)+1) + vmdb := suite.StateDB() + vmdb.SetNonce(suite.address, vmdb.GetNonce(suite.address)+1) + suite.Require().NoError(vmdb.Commit()) + contractAddr := suite.DeployTestContract(suite.T(), suite.address, sdk.NewIntWithDecimal(1000, 18).BigInt()) suite.Commit() // create multiple transactions in the same block @@ -867,7 +882,7 @@ func (suite *KeeperTestSuite) TestNonceInQuery() { priv, err := ethsecp256k1.GenerateKey() suite.Require().NoError(err) address := common.BytesToAddress(priv.PubKey().Address().Bytes()) - suite.Require().Equal(uint64(0), suite.app.EvmKeeper.GetNonce(address)) + suite.Require().Equal(uint64(0), suite.app.EvmKeeper.GetNonce(suite.ctx, address)) supply := sdk.NewIntWithDecimal(1000, 18).BigInt() // accupy nonce 0 diff --git a/x/evm/keeper/hooks_test.go b/x/evm/keeper/hooks_test.go index 1f512f39..3dc5a83e 100644 --- a/x/evm/keeper/hooks_test.go +++ b/x/evm/keeper/hooks_test.go @@ -9,6 +9,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/tharsis/ethermint/x/evm/keeper" + "github.com/tharsis/ethermint/x/evm/statedb" "github.com/tharsis/ethermint/x/evm/types" ) @@ -62,18 +63,25 @@ func (suite *KeeperTestSuite) TestEvmHooks() { suite.app.EvmKeeper.SetHooks(keeper.NewMultiEvmHooks(hook)) k := suite.app.EvmKeeper + ctx := suite.ctx txHash := common.BigToHash(big.NewInt(1)) - k.SetTxHashTransient(txHash) - k.AddLog(ðtypes.Log{ + vmdb := statedb.New(ctx, k, statedb.NewTxConfig( + common.BytesToHash(ctx.HeaderHash().Bytes()), + txHash, + 0, + 0, + )) + + vmdb.AddLog(ðtypes.Log{ Topics: []common.Hash{}, Address: suite.address, }) - logs := k.GetTxLogsTransient(txHash) + logs := vmdb.Logs() receipt := ðtypes.Receipt{ TxHash: txHash, Logs: logs, } - result := k.PostTxProcessing(common.Address{}, nil, receipt) + result := k.PostTxProcessing(ctx, common.Address{}, nil, receipt) tc.expFunc(hook, result) } diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 077c7689..85fabac8 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -1,21 +1,22 @@ package keeper import ( + "errors" "math/big" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" "github.com/tendermint/tendermint/libs/log" - "github.com/ethereum/go-ethereum/params" ethermint "github.com/tharsis/ethermint/types" + "github.com/tharsis/ethermint/x/evm/statedb" "github.com/tharsis/ethermint/x/evm/types" ) @@ -44,12 +45,6 @@ type Keeper struct { // fetch EIP1559 base fee and parameters feeMarketKeeper types.FeeMarketKeeper - // Manage the initial context and cache context stack for accessing the store, - // emit events and log info. - // It is kept as a field to make is accessible by the StateDb - // functions. Resets on every transaction/block. - ctxStack ContextStack - // chain ID number obtained from the context's chain id eip155ChainID *big.Int @@ -58,9 +53,6 @@ type Keeper struct { // EVM Hooks for tx post-processing hooks types.EvmHooks - - // error from previous state operation - stateErr error } // NewKeeper generates new evm module keeper @@ -92,35 +84,14 @@ func NewKeeper( storeKey: storeKey, transientKey: transientKey, tracer: tracer, - stateErr: nil, } } -// Ctx returns the current context from the context stack. -func (k Keeper) Ctx() sdk.Context { - return k.ctxStack.CurrentContext() -} - -// CommitCachedContexts commit all the cache contexts created by `StateDB.Snapshot`. -func (k *Keeper) CommitCachedContexts() { - k.ctxStack.Commit() -} - -// CachedContextsEmpty returns true if there's no cache contexts. -func (k *Keeper) CachedContextsEmpty() bool { - return k.ctxStack.IsEmpty() -} - // Logger returns a module-specific logger. func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", types.ModuleName) } -// WithContext clears the context stack, and set the initial context. -func (k *Keeper) WithContext(ctx sdk.Context) { - k.ctxStack.Reset(ctx) -} - // WithChainID sets the chain id to the local variable in the keeper func (k *Keeper) WithChainID(ctx sdk.Context) { chainID, err := ethermint.ParseChainID(ctx.ChainID()) @@ -156,9 +127,9 @@ func (k Keeper) EmitBlockBloomEvent(ctx sdk.Context, bloom ethtypes.Bloom) { } // GetBlockBloomTransient returns bloom bytes for the current block height -func (k Keeper) GetBlockBloomTransient() *big.Int { - store := prefix.NewStore(k.Ctx().TransientStore(k.transientKey), types.KeyPrefixTransientBloom) - heightBz := sdk.Uint64ToBigEndian(uint64(k.Ctx().BlockHeight())) +func (k Keeper) GetBlockBloomTransient(ctx sdk.Context) *big.Int { + store := prefix.NewStore(ctx.TransientStore(k.transientKey), types.KeyPrefixTransientBloom) + heightBz := sdk.Uint64ToBigEndian(uint64(ctx.BlockHeight())) bz := store.Get(heightBz) if len(bz) == 0 { return big.NewInt(0) @@ -169,9 +140,9 @@ func (k Keeper) GetBlockBloomTransient() *big.Int { // SetBlockBloomTransient sets the given bloom bytes to the transient store. This value is reset on // every block. -func (k Keeper) SetBlockBloomTransient(bloom *big.Int) { - store := prefix.NewStore(k.Ctx().TransientStore(k.transientKey), types.KeyPrefixTransientBloom) - heightBz := sdk.Uint64ToBigEndian(uint64(k.Ctx().BlockHeight())) +func (k Keeper) SetBlockBloomTransient(ctx sdk.Context, bloom *big.Int) { + store := prefix.NewStore(ctx.TransientStore(k.transientKey), types.KeyPrefixTransientBloom) + heightBz := sdk.Uint64ToBigEndian(uint64(ctx.BlockHeight())) store.Set(heightBz, bloom.Bytes()) } @@ -179,32 +150,15 @@ func (k Keeper) SetBlockBloomTransient(bloom *big.Int) { // Tx // ---------------------------------------------------------------------------- -// GetTxHashTransient returns the hash of current processing transaction -func (k Keeper) GetTxHashTransient() common.Hash { - store := k.Ctx().TransientStore(k.transientKey) - bz := store.Get(types.KeyPrefixTransientTxHash) - if len(bz) == 0 { - return common.Hash{} - } - - return common.BytesToHash(bz) -} - -// SetTxHashTransient set the hash of processing transaction -func (k Keeper) SetTxHashTransient(hash common.Hash) { - store := k.Ctx().TransientStore(k.transientKey) - store.Set(types.KeyPrefixTransientTxHash, hash.Bytes()) -} - // SetTxIndexTransient set the index of processing transaction -func (k Keeper) SetTxIndexTransient(index uint64) { - store := k.Ctx().TransientStore(k.transientKey) +func (k Keeper) SetTxIndexTransient(ctx sdk.Context, index uint64) { + store := ctx.TransientStore(k.transientKey) store.Set(types.KeyPrefixTransientTxIndex, sdk.Uint64ToBigEndian(index)) } // GetTxIndexTransient returns EVM transaction index on the current block. -func (k Keeper) GetTxIndexTransient() uint64 { - store := k.Ctx().TransientStore(k.transientKey) +func (k Keeper) GetTxIndexTransient(ctx sdk.Context) uint64 { + store := ctx.TransientStore(k.transientKey) bz := store.Get(types.KeyPrefixTransientTxIndex) if len(bz) == 0 { return 0 @@ -213,61 +167,13 @@ func (k Keeper) GetTxIndexTransient() uint64 { return sdk.BigEndianToUint64(bz) } -// IncreaseTxIndexTransient fetches the current EVM tx index from the transient store, increases its -// value by one and then sets the new index back to the transient store. -func (k Keeper) IncreaseTxIndexTransient() { - txIndex := k.GetTxIndexTransient() - k.SetTxIndexTransient(txIndex + 1) -} - -// ResetRefundTransient resets the available refund amount to 0 -func (k Keeper) ResetRefundTransient(ctx sdk.Context) { - store := ctx.TransientStore(k.transientKey) - store.Set(types.KeyPrefixTransientRefund, sdk.Uint64ToBigEndian(0)) -} - // ---------------------------------------------------------------------------- // Log // ---------------------------------------------------------------------------- -// GetTxLogsTransient returns the current logs for a given transaction hash from the KVStore. -// This function returns an empty, non-nil slice if no logs are found. -func (k Keeper) GetTxLogsTransient(txHash common.Hash) []*ethtypes.Log { - store := prefix.NewStore(k.Ctx().TransientStore(k.transientKey), types.KeyPrefixTransientTxLogs) - - // We store the logs with key equal to txHash.Bytes() | sdk.Uint64ToBigEndian(uint64(log.Index)), - // therefore, we set the end boundary(excluded) to txHash.Bytes() | uint64.Max -> []byte - end := txHash.Bytes() - end = append(end, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}...) - - iter := store.Iterator(txHash.Bytes(), end) - defer iter.Close() - - logs := []*ethtypes.Log{} - for ; iter.Valid(); iter.Next() { - var log types.Log - k.cdc.MustUnmarshal(iter.Value(), &log) - logs = append(logs, log.ToEthereum()) - } - - return logs -} - -// SetLog sets the log for a transaction in the KVStore. -func (k Keeper) AddLogTransient(log *ethtypes.Log) { - store := prefix.NewStore(k.Ctx().TransientStore(k.transientKey), types.KeyPrefixTransientTxLogs) - - key := log.TxHash.Bytes() - key = append(key, sdk.Uint64ToBigEndian(uint64(log.Index))...) - - txIndexLog := types.NewLogFromEth(log) - bz := k.cdc.MustMarshal(txIndexLog) - store.Set(key, bz) -} - // GetLogSizeTransient returns EVM log index on the current block. -func (k Keeper) GetLogSizeTransient() uint64 { - store := k.Ctx().TransientStore(k.transientKey) +func (k Keeper) GetLogSizeTransient(ctx sdk.Context) uint64 { + store := ctx.TransientStore(k.transientKey) bz := store.Get(types.KeyPrefixTransientLogSize) if len(bz) == 0 { return 0 @@ -276,12 +182,11 @@ func (k Keeper) GetLogSizeTransient() uint64 { return sdk.BigEndianToUint64(bz) } -// IncreaseLogSizeTransient fetches the current EVM log index from the transient store, increases its +// SetLogSizeTransient fetches the current EVM log index from the transient store, increases its // value by one and then sets the new index back to the transient store. -func (k Keeper) IncreaseLogSizeTransient() { - logSize := k.GetLogSizeTransient() - store := k.Ctx().TransientStore(k.transientKey) - store.Set(types.KeyPrefixTransientLogSize, sdk.Uint64ToBigEndian(logSize+1)) +func (k Keeper) SetLogSizeTransient(ctx sdk.Context, logSize uint64) { + store := ctx.TransientStore(k.transientKey) + store.Set(types.KeyPrefixTransientLogSize, sdk.Uint64ToBigEndian(logSize)) } // ---------------------------------------------------------------------------- @@ -289,70 +194,23 @@ func (k Keeper) IncreaseLogSizeTransient() { // ---------------------------------------------------------------------------- // GetAccountStorage return state storage associated with an account -func (k Keeper) GetAccountStorage(ctx sdk.Context, address common.Address) (types.Storage, error) { +func (k Keeper) GetAccountStorage(ctx sdk.Context, address common.Address) types.Storage { storage := types.Storage{} - err := k.ForEachStorage(address, func(key, value common.Hash) bool { + k.ForEachStorage(ctx, address, func(key, value common.Hash) bool { storage = append(storage, types.NewState(key, value)) return true }) - if err != nil { - return types.Storage{}, err - } - return storage, nil + return storage } // ---------------------------------------------------------------------------- // Account // ---------------------------------------------------------------------------- -func (k Keeper) DeleteState(addr common.Address, key common.Hash) { - store := prefix.NewStore(k.Ctx().KVStore(k.storeKey), types.AddressStoragePrefix(addr)) - store.Delete(key.Bytes()) -} - -// DeleteAccountStorage clears all the storage state associated with the given address. -func (k Keeper) DeleteAccountStorage(addr common.Address) { - _ = k.ForEachStorage(addr, func(key, _ common.Hash) bool { - k.DeleteState(addr, key) - return true - }) -} - -// DeleteCode removes the contract code byte array from the store associated with -// the given address and empties CodeHash on account. -func (k Keeper) DeleteCode(addr common.Address) { - k.SetCode(addr, nil) -} - -// ClearBalance subtracts the EVM all the balance denomination from the address -// balance while also updating the total supply. -func (k Keeper) ClearBalance(addr sdk.AccAddress) (prevBalance sdk.Coin, err error) { - params := k.GetParams(k.Ctx()) - - prevBalance = k.bankKeeper.GetBalance(k.Ctx(), addr, params.EvmDenom) - if prevBalance.IsPositive() { - if err := k.bankKeeper.SendCoinsFromAccountToModule(k.Ctx(), addr, types.ModuleName, sdk.Coins{prevBalance}); err != nil { - return sdk.Coin{}, sdkerrors.Wrap(err, "failed to transfer to module account") - } - - if err := k.bankKeeper.BurnCoins(k.Ctx(), types.ModuleName, sdk.Coins{prevBalance}); err != nil { - return sdk.Coin{}, sdkerrors.Wrap(err, "failed to burn coins from evm module account") - } - } - - return prevBalance, nil -} - -// ResetAccount removes the code, storage state, but keep all the native tokens stored -// with the given address. -func (k Keeper) ResetAccount(addr common.Address) { - k.DeleteCode(addr) - k.DeleteAccountStorage(addr) -} - // SetHooks sets the hooks for the EVM module +// It should be called only once during initialization, it panic if called more than once. func (k *Keeper) SetHooks(eh types.EvmHooks) *Keeper { if k.hooks != nil { panic("cannot set evm hooks twice") @@ -363,16 +221,76 @@ func (k *Keeper) SetHooks(eh types.EvmHooks) *Keeper { } // PostTxProcessing delegate the call to the hooks. If no hook has been registered, this function returns with a `nil` error -func (k *Keeper) PostTxProcessing(from common.Address, to *common.Address, receipt *ethtypes.Receipt) error { +func (k *Keeper) PostTxProcessing(ctx sdk.Context, from common.Address, to *common.Address, receipt *ethtypes.Receipt) error { if k.hooks == nil { return nil } - return k.hooks.PostTxProcessing(k.Ctx(), from, to, receipt) + return k.hooks.PostTxProcessing(ctx, from, to, receipt) } // Tracer return a default vm.Tracer based on current keeper state -func (k Keeper) Tracer(msg core.Message, ethCfg *params.ChainConfig) vm.Tracer { - return types.NewTracer(k.tracer, msg, ethCfg, k.Ctx().BlockHeight()) +func (k Keeper) Tracer(ctx sdk.Context, msg core.Message, ethCfg *params.ChainConfig) vm.Tracer { + return types.NewTracer(k.tracer, msg, ethCfg, ctx.BlockHeight()) +} + +// GetAccountWithoutBalance load nonce and codehash without balance, +// more efficient in cases where balance is not needed. +func (k *Keeper) GetAccountWithoutBalance(ctx sdk.Context, addr common.Address) (*statedb.Account, error) { + cosmosAddr := sdk.AccAddress(addr.Bytes()) + acct := k.accountKeeper.GetAccount(ctx, cosmosAddr) + if acct == nil { + return nil, nil + } + + ethAcct, ok := acct.(*ethermint.EthAccount) + if !ok { + return nil, errors.New("not EthAccount") + } + + return &statedb.Account{ + Nonce: ethAcct.Sequence, + CodeHash: common.FromHex(ethAcct.CodeHash), + }, nil +} + +// GetAccountOrEmpty returns empty account if not exist, returns error if it's not `EthAccount` +func (k *Keeper) GetAccountOrEmpty(ctx sdk.Context, addr common.Address) (statedb.Account, error) { + acct, err := k.GetAccount(ctx, addr) + if err != nil { + return statedb.Account{}, err + } + if acct == nil { + // empty account + return statedb.Account{ + Balance: new(big.Int), + CodeHash: types.EmptyCodeHash, + }, nil + } + return *acct, nil +} + +// GetNonce returns the sequence number of an account, returns 0 if not exists. +func (k *Keeper) GetNonce(ctx sdk.Context, addr common.Address) uint64 { + cosmosAddr := sdk.AccAddress(addr.Bytes()) + acct := k.accountKeeper.GetAccount(ctx, cosmosAddr) + if acct == nil { + return 0 + } + + ethAcct, ok := acct.(*ethermint.EthAccount) + if !ok { + return 0 + } + + return ethAcct.Sequence +} + +// GetBalance load account's balance of gas token +func (k *Keeper) GetBalance(ctx sdk.Context, addr common.Address) *big.Int { + cosmosAddr := sdk.AccAddress(addr.Bytes()) + params := k.GetParams(ctx) + coin := k.bankKeeper.GetBalance(ctx, cosmosAddr, params.EvmDenom) + return coin.Amount.BigInt() } // BaseFee returns current base fee, return values: diff --git a/x/evm/keeper/keeper_test.go b/x/evm/keeper/keeper_test.go index 10501632..ed208e49 100644 --- a/x/evm/keeper/keeper_test.go +++ b/x/evm/keeper/keeper_test.go @@ -29,6 +29,7 @@ import ( "github.com/tharsis/ethermint/server/config" "github.com/tharsis/ethermint/tests" ethermint "github.com/tharsis/ethermint/types" + "github.com/tharsis/ethermint/x/evm/statedb" "github.com/tharsis/ethermint/x/evm/types" "github.com/ethereum/go-ethereum/common" @@ -153,7 +154,6 @@ func (suite *KeeperTestSuite) DoSetupTest(t require.TestingT) { ConsensusHash: tmhash.Sum([]byte("consensus")), LastResultsHash: tmhash.Sum([]byte("last_result")), }) - suite.app.EvmKeeper.WithContext(suite.ctx) queryHelper := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry()) types.RegisterQueryServer(queryHelper, suite.app.EvmKeeper) @@ -201,13 +201,16 @@ func (suite *KeeperTestSuite) Commit() { // update ctx suite.ctx = suite.app.BaseApp.NewContext(false, header) - suite.app.EvmKeeper.WithContext(suite.ctx) queryHelper := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry()) types.RegisterQueryServer(queryHelper, suite.app.EvmKeeper) suite.queryClient = types.NewQueryClient(queryHelper) } +func (suite *KeeperTestSuite) StateDB() *statedb.StateDB { + return statedb.New(suite.ctx, suite.app.EvmKeeper, statedb.NewEmptyTxConfig(common.BytesToHash(suite.ctx.HeaderHash().Bytes()))) +} + // DeployTestContract deploy a test erc20 contract and returns the contract address func (suite *KeeperTestSuite) DeployTestContract(t require.TestingT, owner common.Address, supply *big.Int) common.Address { ctx := sdk.WrapSDKContext(suite.ctx) @@ -216,7 +219,7 @@ func (suite *KeeperTestSuite) DeployTestContract(t require.TestingT, owner commo ctorArgs, err := types.ERC20Contract.ABI.Pack("", owner, supply) require.NoError(t, err) - nonce := suite.app.EvmKeeper.GetNonce(suite.address) + nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) data := append(types.ERC20Contract.Bin, ctorArgs...) args, err := json.Marshal(&types.TransactionArgs{ @@ -280,7 +283,7 @@ func (suite *KeeperTestSuite) TransferERC20Token(t require.TestingT, contractAdd }) require.NoError(t, err) - nonce := suite.app.EvmKeeper.GetNonce(suite.address) + nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) var ercTransferTx *types.MsgEthereumTx if suite.enableFeemarket { @@ -337,7 +340,7 @@ func (suite *KeeperTestSuite) DeployTestMessageCall(t require.TestingT) common.A }) require.NoError(t, err) - nonce := suite.app.EvmKeeper.GetNonce(suite.address) + nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) var erc20DeployTx *types.MsgEthereumTx if suite.enableFeemarket { diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index bc052cd7..f3cfe68e 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -23,13 +23,12 @@ var _ types.MsgServer = &Keeper{} // parameter. func (k *Keeper) EthereumTx(goCtx context.Context, msg *types.MsgEthereumTx) (*types.MsgEthereumTxResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - k.WithContext(ctx) sender := msg.From tx := msg.AsTransaction() - txIndex := k.GetTxIndexTransient() + txIndex := k.GetTxIndexTransient(ctx) - response, err := k.ApplyTransaction(tx) + response, err := k.ApplyTransaction(ctx, tx) if err != nil { return nil, sdkerrors.Wrap(err, "failed to apply transaction") } diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go index be86fcfa..1560b616 100644 --- a/x/evm/keeper/state_transition.go +++ b/x/evm/keeper/state_transition.go @@ -12,6 +12,7 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ethermint "github.com/tharsis/ethermint/types" + "github.com/tharsis/ethermint/x/evm/statedb" "github.com/tharsis/ethermint/x/evm/types" "github.com/ethereum/go-ethereum/common" @@ -22,6 +23,18 @@ import ( "github.com/ethereum/go-ethereum/params" ) +// GasToRefund calculates the amount of gas the state machine should refund to the sender. It is +// capped by the refund quotient value. +// Note: do not pass 0 to refundQuotient +func GasToRefund(availableRefund, gasConsumed, refundQuotient uint64) uint64 { + // Apply refund counter + refund := gasConsumed / refundQuotient + if refund > availableRefund { + return availableRefund + } + return refund +} + // EVMConfig creates the EVMConfig based on current state func (k *Keeper) EVMConfig(ctx sdk.Context) (*types.EVMConfig, error) { params := k.GetParams(ctx) @@ -42,39 +55,51 @@ func (k *Keeper) EVMConfig(ctx sdk.Context) (*types.EVMConfig, error) { }, nil } +// TxConfig load `TxConfig` from current transient storage +func (k *Keeper) TxConfig(ctx sdk.Context, txHash common.Hash) statedb.TxConfig { + return statedb.NewTxConfig( + common.BytesToHash(ctx.HeaderHash()), // BlockHash + txHash, // TxHash + uint(k.GetTxIndexTransient(ctx)), // TxIndex + uint(k.GetLogSizeTransient(ctx)), // LogIndex + ) +} + // NewEVM generates a go-ethereum VM from the provided Message fields and the chain parameters // (ChainConfig and module Params). It additionally sets the validator operator address as the // coinbase address to make it available for the COINBASE opcode, even though there is no // beneficiary of the coinbase transaction (since we're not mining). func (k *Keeper) NewEVM( + ctx sdk.Context, msg core.Message, cfg *types.EVMConfig, tracer vm.Tracer, + stateDB vm.StateDB, ) *vm.EVM { blockCtx := vm.BlockContext{ CanTransfer: core.CanTransfer, Transfer: core.Transfer, - GetHash: k.GetHashFn(), + GetHash: k.GetHashFn(ctx), Coinbase: cfg.CoinBase, - GasLimit: ethermint.BlockGasLimit(k.Ctx()), - BlockNumber: big.NewInt(k.Ctx().BlockHeight()), - Time: big.NewInt(k.Ctx().BlockHeader().Time.Unix()), + GasLimit: ethermint.BlockGasLimit(ctx), + BlockNumber: big.NewInt(ctx.BlockHeight()), + Time: big.NewInt(ctx.BlockHeader().Time.Unix()), Difficulty: big.NewInt(0), // unused. Only required in PoW context BaseFee: cfg.BaseFee, } txCtx := core.NewEVMTxContext(msg) if tracer == nil { - tracer = k.Tracer(msg, cfg.ChainConfig) + tracer = k.Tracer(ctx, msg, cfg.ChainConfig) } - vmConfig := k.VMConfig(cfg.Params, tracer) - return vm.NewEVM(blockCtx, txCtx, k, cfg.ChainConfig, vmConfig) + vmConfig := k.VMConfig(ctx, msg, cfg.Params, tracer) + return vm.NewEVM(blockCtx, txCtx, stateDB, cfg.ChainConfig, vmConfig) } // VMConfig creates an EVM configuration from the debug setting and the extra EIPs enabled on the // module parameters. The config generated uses the default JumpTable from the EVM. -func (k Keeper) VMConfig(params types.Params, tracer vm.Tracer) vm.Config { - fmParams := k.feeMarketKeeper.GetParams(k.Ctx()) +func (k Keeper) VMConfig(ctx sdk.Context, msg core.Message, params types.Params, tracer vm.Tracer) vm.Config { + fmParams := k.feeMarketKeeper.GetParams(ctx) var debug bool if _, ok := tracer.(types.NoOpTracer); !ok { @@ -94,10 +119,8 @@ func (k Keeper) VMConfig(params types.Params, tracer vm.Tracer) vm.Config { // 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 { +func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { return func(height uint64) common.Hash { - ctx := k.Ctx() - h, err := ethermint.SafeInt64(height) if err != nil { k.Logger(ctx).Error("failed to cast height to int64", "error", err) @@ -165,21 +188,17 @@ func (k Keeper) GetHashFn() vm.GetHashFunc { // returning. // // For relevant discussion see: https://github.com/cosmos/cosmos-sdk/discussions/9072 -func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumTxResponse, error) { +func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*types.MsgEthereumTxResponse, error) { var ( bloom *big.Int bloomReceipt ethtypes.Bloom ) - ctx := k.Ctx() - - // ensure keeper state error is cleared - defer k.ClearStateError() - cfg, err := k.EVMConfig(ctx) if err != nil { return nil, sdkerrors.Wrap(err, "failed to load evm config") } + txConfig := k.TxConfig(ctx, tx.Hash()) // get the signer according to the chain rules from the config and block height signer := ethtypes.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) @@ -188,38 +207,28 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT return nil, sdkerrors.Wrap(err, "failed to return ethereum transaction as core message") } - txHash := tx.Hash() - - // set the transaction hash and index to the impermanent (transient) block state so that it's also - // available on the StateDB functions (eg: AddLog) - k.SetTxHashTransient(txHash) - // snapshot to contain the tx processing and post processing in same scope var commit func() + tmpCtx := ctx if k.hooks != nil { // Create a cache context to revert state when tx hooks fails, // the cache context is only committed when both tx and hooks executed successfully. // Didn't use `Snapshot` because the context stack has exponential complexity on certain operations, // thus restricted to be used only inside `ApplyMessage`. - var cacheCtx sdk.Context - cacheCtx, commit = ctx.CacheContext() - k.WithContext(cacheCtx) - defer (func() { - k.WithContext(ctx) - })() + tmpCtx, commit = ctx.CacheContext() } - res, err := k.ApplyMessageWithConfig(msg, nil, true, cfg) + // pass true to commit the StateDB + res, err := k.ApplyMessageWithConfig(tmpCtx, msg, nil, true, cfg, txConfig) if err != nil { return nil, sdkerrors.Wrap(err, "failed to apply ethereum core message") } - res.Hash = txHash.Hex() - logs := k.GetTxLogsTransient(txHash) + logs := types.LogsToEthereum(res.Logs) // Compute block bloom filter if len(logs) > 0 { - bloom = k.GetBlockBloomTransient() + bloom = k.GetBlockBloomTransient(ctx) bloom.Or(bloom, big.NewInt(0).SetBytes(ethtypes.LogsBloom(logs))) bloomReceipt = ethtypes.BytesToBloom(bloom.Bytes()) } @@ -244,45 +253,42 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT CumulativeGasUsed: cumulativeGasUsed, Bloom: bloomReceipt, Logs: logs, - TxHash: txHash, + TxHash: txConfig.TxHash, ContractAddress: contractAddr, GasUsed: res.GasUsed, - BlockHash: common.BytesToHash(ctx.HeaderHash()), + BlockHash: txConfig.BlockHash, BlockNumber: big.NewInt(ctx.BlockHeight()), - TransactionIndex: uint(k.GetTxIndexTransient()), + TransactionIndex: txConfig.TxIndex, } // Only call hooks if tx executed successfully. - if err = k.PostTxProcessing(msg.From(), tx.To(), receipt); err != nil { + if err = k.PostTxProcessing(tmpCtx, msg.From(), tx.To(), receipt); err != nil { // If hooks return error, revert the whole tx. res.VmError = types.ErrPostTxProcessing.Error() k.Logger(ctx).Error("tx post processing failed", "error", err) } else if commit != nil { - // PostTxProcessing is successful, commit the cache context + // PostTxProcessing is successful, commit the tmpCtx commit() - ctx.EventManager().EmitEvents(k.Ctx().EventManager().Events()) + ctx.EventManager().EmitEvents(tmpCtx.EventManager().Events()) } } - // change to original context - k.WithContext(ctx) - - // refund gas according to Ethereum gas accounting rules. - if err := k.RefundGas(msg, msg.Gas()-res.GasUsed, cfg.Params.EvmDenom); err != nil { + // refund gas in order to match the Ethereum gas consumption instead of the default SDK one. + if err = k.RefundGas(ctx, msg, msg.Gas()-res.GasUsed, cfg.Params.EvmDenom); err != nil { return nil, sdkerrors.Wrapf(err, "failed to refund gas leftover gas to sender %s", msg.From()) } if len(logs) > 0 { - res.Logs = types.NewLogsFromEth(logs) - // Update transient block bloom filter - k.SetBlockBloomTransient(bloom) + k.SetBlockBloomTransient(ctx, bloom) + + k.SetLogSizeTransient(ctx, uint64(txConfig.LogIndex)+uint64(len(logs))) } - k.IncreaseTxIndexTransient() + k.SetTxIndexTransient(ctx, uint64(txConfig.TxIndex)+1) // update the gas used after refund - k.ResetGasMeterAndConsumeGas(res.GasUsed) + k.ResetGasMeterAndConsumeGas(ctx, res.GasUsed) return res, nil } @@ -292,8 +298,7 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT // // Reverted state // -// The snapshot and rollback are supported by the `ContextStack`, which should be only used inside `ApplyMessage`, -// because some operations has exponential computational complexity with deep stack. +// The snapshot and rollback are supported by the `statedb.StateDB`. // // Different Callers // @@ -324,22 +329,13 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT // // Commit parameter // -// If commit is true, the cache context stack will be committed, otherwise discarded. -func (k *Keeper) ApplyMessageWithConfig(msg core.Message, tracer vm.Tracer, commit bool, cfg *types.EVMConfig) (*types.MsgEthereumTxResponse, error) { +// If commit is true, the `StateDB` will be committed, otherwise discarded. +func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, msg core.Message, tracer vm.Tracer, commit bool, cfg *types.EVMConfig, txConfig statedb.TxConfig) (*types.MsgEthereumTxResponse, error) { var ( ret []byte // return bytes from evm execution vmErr error // vm errors do not effect consensus and are therefore not assigned to err ) - if !k.ctxStack.IsEmpty() { - panic("context stack shouldn't be dirty before apply message") - } - - evm := k.NewEVM(msg, cfg, tracer) - - // ensure keeper state error is cleared - defer k.ClearStateError() - // return error if contract creation or call are disabled through governance if !cfg.Params.EnableCreate && msg.To() == nil { return nil, sdkerrors.Wrap(types.ErrCreateDisabled, "failed to create new contract") @@ -347,11 +343,14 @@ func (k *Keeper) ApplyMessageWithConfig(msg core.Message, tracer vm.Tracer, comm return nil, sdkerrors.Wrap(types.ErrCallDisabled, "failed to call contract") } + stateDB := statedb.New(ctx, k, txConfig) + evm := k.NewEVM(ctx, msg, cfg, tracer, stateDB) + sender := vm.AccountRef(msg.From()) contractCreation := msg.To() == nil isLondon := cfg.ChainConfig.IsLondon(evm.Context.BlockNumber) - intrinsicGas, err := k.GetEthIntrinsicGas(msg, cfg.ChainConfig, contractCreation) + intrinsicGas, err := k.GetEthIntrinsicGas(ctx, msg, cfg.ChainConfig, contractCreation) if err != nil { // should have already been checked on Ante Handler return nil, sdkerrors.Wrap(err, "intrinsic gas failed") @@ -363,21 +362,19 @@ func (k *Keeper) ApplyMessageWithConfig(msg core.Message, tracer vm.Tracer, comm } leftoverGas := msg.Gas() - intrinsicGas - // Clear access list before executing the contract - k.ClearAccessList() // access list preparaion is moved from ante handler to here, because it's needed when `ApplyMessage` is called // under contexts where ante handlers are not run, for example `eth_call` and `eth_estimateGas`. - if rules := cfg.ChainConfig.Rules(big.NewInt(k.Ctx().BlockHeight())); rules.IsBerlin { - k.PrepareAccessList(msg.From(), msg.To(), vm.ActivePrecompiles(rules), msg.AccessList()) + if rules := cfg.ChainConfig.Rules(big.NewInt(ctx.BlockHeight())); rules.IsBerlin { + stateDB.PrepareAccessList(msg.From(), msg.To(), vm.ActivePrecompiles(rules), msg.AccessList()) } if contractCreation { // take over the nonce management from evm: // - reset sender's nonce to msg.Nonce() before calling evm. // - increase sender's nonce by one no matter the result. - k.SetNonce(sender.Address(), msg.Nonce()) + stateDB.SetNonce(sender.Address(), msg.Nonce()) ret, _, leftoverGas, vmErr = evm.Create(sender, msg.Data(), leftoverGas, msg.Value()) - k.SetNonce(sender.Address(), msg.Nonce()+1) + stateDB.SetNonce(sender.Address(), msg.Nonce()+1) } else { ret, leftoverGas, vmErr = evm.Call(sender, *msg.To(), msg.Data(), leftoverGas, msg.Value()) } @@ -394,7 +391,7 @@ func (k *Keeper) ApplyMessageWithConfig(msg core.Message, tracer vm.Tracer, comm return nil, sdkerrors.Wrap(types.ErrGasOverflow, "apply message") } gasUsed := msg.Gas() - leftoverGas - refund := k.GasToRefund(gasUsed, refundQuotient) + refund := GasToRefund(stateDB.GetRefund(), gasUsed, refundQuotient) if refund > gasUsed { return nil, sdkerrors.Wrap(types.ErrGasOverflow, "apply message") } @@ -406,57 +403,46 @@ func (k *Keeper) ApplyMessageWithConfig(msg core.Message, tracer vm.Tracer, comm vmError = vmErr.Error() } - // The context stack is designed specifically for `StateDB` interface, it should only be used in `ApplyMessage`, - // after return, the stack should be clean, the cached states are either committed or discarded. + // The dirty states in `StateDB` is either committed or discarded after return if commit { - k.CommitCachedContexts() - } else { - k.ctxStack.RevertAll() + if err := stateDB.Commit(); err != nil { + return nil, sdkerrors.Wrap(err, "failed to commit stateDB") + } } return &types.MsgEthereumTxResponse{ GasUsed: gasUsed, VmError: vmError, Ret: ret, + Logs: types.NewLogsFromEth(stateDB.Logs()), + Hash: txConfig.TxHash.Hex(), }, nil } // ApplyMessage calls ApplyMessageWithConfig with default EVMConfig -func (k *Keeper) ApplyMessage(msg core.Message, tracer vm.Tracer, commit bool) (*types.MsgEthereumTxResponse, error) { - cfg, err := k.EVMConfig(k.Ctx()) +func (k *Keeper) ApplyMessage(ctx sdk.Context, msg core.Message, tracer vm.Tracer, commit bool) (*types.MsgEthereumTxResponse, error) { + cfg, err := k.EVMConfig(ctx) if err != nil { return nil, sdkerrors.Wrap(err, "failed to load evm config") } - return k.ApplyMessageWithConfig(msg, tracer, commit, cfg) + txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash())) + return k.ApplyMessageWithConfig(ctx, msg, tracer, commit, cfg, txConfig) } // GetEthIntrinsicGas returns the intrinsic gas cost for the transaction -func (k *Keeper) GetEthIntrinsicGas(msg core.Message, cfg *params.ChainConfig, isContractCreation bool) (uint64, error) { - height := big.NewInt(k.Ctx().BlockHeight()) +func (k *Keeper) GetEthIntrinsicGas(ctx sdk.Context, msg core.Message, cfg *params.ChainConfig, isContractCreation bool) (uint64, error) { + height := big.NewInt(ctx.BlockHeight()) homestead := cfg.IsHomestead(height) istanbul := cfg.IsIstanbul(height) return core.IntrinsicGas(msg.Data(), msg.AccessList(), isContractCreation, homestead, istanbul) } -// GasToRefund calculates the amount of gas the state machine should refund to the sender. It is -// capped by the refund quotient value. -// Note: do not pass 0 to refundQuotient -func (k *Keeper) GasToRefund(gasConsumed, refundQuotient uint64) uint64 { - // Apply refund counter - refund := gasConsumed / refundQuotient - availableRefund := k.GetRefund() - if refund > availableRefund { - return availableRefund - } - return refund -} - // 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 consumed during in the // AnteHandler. -func (k *Keeper) RefundGas(msg core.Message, leftoverGas uint64, denom string) error { +func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64, denom string) error { // Return EVM tokens for remaining gas, exchanged at the original rate. remaining := new(big.Int).Mul(new(big.Int).SetUint64(leftoverGas), msg.GasPrice()) @@ -470,7 +456,7 @@ func (k *Keeper) RefundGas(msg core.Message, leftoverGas uint64, denom string) e // refund to sender from the fee collector module account, which is the escrow account in charge of collecting tx fees - err := k.bankKeeper.SendCoinsFromModuleToAccount(k.Ctx(), authtypes.FeeCollectorName, msg.From().Bytes(), refundedCoins) + err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, authtypes.FeeCollectorName, msg.From().Bytes(), refundedCoins) if err != nil { err = sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "fee collector account failed to refund fees: %s", err.Error()) return sdkerrors.Wrapf(err, "failed to refund %d leftover gas (%s)", leftoverGas, refundedCoins.String()) @@ -484,9 +470,8 @@ func (k *Keeper) RefundGas(msg core.Message, leftoverGas uint64, denom string) e // ResetGasMeterAndConsumeGas reset first the gas meter consumed value to zero and set it back to the new value // 'gasUsed' -func (k *Keeper) ResetGasMeterAndConsumeGas(gasUsed uint64) { +func (k *Keeper) ResetGasMeterAndConsumeGas(ctx sdk.Context, gasUsed uint64) { // reset the gas count - ctx := k.Ctx() ctx.GasMeter().RefundGas(ctx.GasMeter().GasConsumed(), "reset the gas count") ctx.GasMeter().ConsumeGas(gasUsed, "apply evm transaction") } diff --git a/x/evm/keeper/state_transition_benchmark_test.go b/x/evm/keeper/state_transition_benchmark_test.go index a013cc9f..9b4f912d 100644 --- a/x/evm/keeper/state_transition_benchmark_test.go +++ b/x/evm/keeper/state_transition_benchmark_test.go @@ -152,7 +152,7 @@ func BenchmarkApplyTransaction(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() tx, err := newSignedEthTx(templateAccessListTx, - suite.app.EvmKeeper.GetNonce(suite.address), + suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address), sdk.AccAddress(suite.address.Bytes()), suite.signer, ethSigner, @@ -160,7 +160,7 @@ func BenchmarkApplyTransaction(b *testing.B) { require.NoError(b, err) b.StartTimer() - resp, err := suite.app.EvmKeeper.ApplyTransaction(tx) + resp, err := suite.app.EvmKeeper.ApplyTransaction(suite.ctx, tx) b.StopTimer() require.NoError(b, err) @@ -179,7 +179,7 @@ func BenchmarkApplyTransactionWithLegacyTx(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() tx, err := newSignedEthTx(templateLegacyTx, - suite.app.EvmKeeper.GetNonce(suite.address), + suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address), sdk.AccAddress(suite.address.Bytes()), suite.signer, ethSigner, @@ -187,7 +187,7 @@ func BenchmarkApplyTransactionWithLegacyTx(b *testing.B) { require.NoError(b, err) b.StartTimer() - resp, err := suite.app.EvmKeeper.ApplyTransaction(tx) + resp, err := suite.app.EvmKeeper.ApplyTransaction(suite.ctx, tx) b.StopTimer() require.NoError(b, err) @@ -206,7 +206,7 @@ func BenchmarkApplyTransactionWithDynamicFeeTx(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() tx, err := newSignedEthTx(templateDynamicFeeTx, - suite.app.EvmKeeper.GetNonce(suite.address), + suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address), sdk.AccAddress(suite.address.Bytes()), suite.signer, ethSigner, @@ -214,7 +214,7 @@ func BenchmarkApplyTransactionWithDynamicFeeTx(b *testing.B) { require.NoError(b, err) b.StartTimer() - resp, err := suite.app.EvmKeeper.ApplyTransaction(tx) + resp, err := suite.app.EvmKeeper.ApplyTransaction(suite.ctx, tx) b.StopTimer() require.NoError(b, err) @@ -236,7 +236,7 @@ func BenchmarkApplyMessage(b *testing.B) { b.StopTimer() m, err := newNativeMessage( - suite.app.EvmKeeper.GetNonce(suite.address), + suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address), suite.ctx.BlockHeight(), suite.address, ethCfg, @@ -249,7 +249,7 @@ func BenchmarkApplyMessage(b *testing.B) { require.NoError(b, err) b.StartTimer() - resp, err := suite.app.EvmKeeper.ApplyMessage(m, nil, true) + resp, err := suite.app.EvmKeeper.ApplyMessage(suite.ctx, m, nil, true) b.StopTimer() require.NoError(b, err) @@ -271,7 +271,7 @@ func BenchmarkApplyMessageWithLegacyTx(b *testing.B) { b.StopTimer() m, err := newNativeMessage( - suite.app.EvmKeeper.GetNonce(suite.address), + suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address), suite.ctx.BlockHeight(), suite.address, ethCfg, @@ -284,7 +284,7 @@ func BenchmarkApplyMessageWithLegacyTx(b *testing.B) { require.NoError(b, err) b.StartTimer() - resp, err := suite.app.EvmKeeper.ApplyMessage(m, nil, true) + resp, err := suite.app.EvmKeeper.ApplyMessage(suite.ctx, m, nil, true) b.StopTimer() require.NoError(b, err) @@ -306,7 +306,7 @@ func BenchmarkApplyMessageWithDynamicFeeTx(b *testing.B) { b.StopTimer() m, err := newNativeMessage( - suite.app.EvmKeeper.GetNonce(suite.address), + suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address), suite.ctx.BlockHeight(), suite.address, ethCfg, @@ -319,7 +319,7 @@ func BenchmarkApplyMessageWithDynamicFeeTx(b *testing.B) { require.NoError(b, err) b.StartTimer() - resp, err := suite.app.EvmKeeper.ApplyMessage(m, nil, true) + resp, err := suite.app.EvmKeeper.ApplyMessage(suite.ctx, m, nil, true) b.StopTimer() require.NoError(b, err) diff --git a/x/evm/keeper/state_transition_test.go b/x/evm/keeper/state_transition_test.go index cbaa5101..c2586ba4 100644 --- a/x/evm/keeper/state_transition_test.go +++ b/x/evm/keeper/state_transition_test.go @@ -15,6 +15,7 @@ import ( tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmtypes "github.com/tendermint/tendermint/types" "github.com/tharsis/ethermint/tests" + "github.com/tharsis/ethermint/x/evm/keeper" "github.com/tharsis/ethermint/x/evm/types" ) @@ -34,7 +35,6 @@ func (suite *KeeperTestSuite) TestGetHashFn() { uint64(suite.ctx.BlockHeight()), func() { suite.ctx = suite.ctx.WithHeaderHash(tmhash.Sum([]byte("header"))) - suite.app.EvmKeeper.WithContext(suite.ctx) }, common.BytesToHash(tmhash.Sum([]byte("header"))), }, @@ -45,7 +45,6 @@ func (suite *KeeperTestSuite) TestGetHashFn() { header := tmproto.Header{} header.Height = suite.ctx.BlockHeight() suite.ctx = suite.ctx.WithBlockHeader(header) - suite.app.EvmKeeper.WithContext(suite.ctx) }, common.Hash{}, }, @@ -54,7 +53,6 @@ func (suite *KeeperTestSuite) TestGetHashFn() { uint64(suite.ctx.BlockHeight()), func() { suite.ctx = suite.ctx.WithBlockHeader(header) - suite.app.EvmKeeper.WithContext(suite.ctx) }, common.BytesToHash(hash), }, @@ -63,7 +61,6 @@ func (suite *KeeperTestSuite) TestGetHashFn() { 1, func() { suite.ctx = suite.ctx.WithBlockHeight(10) - suite.app.EvmKeeper.WithContext(suite.ctx) }, common.Hash{}, }, @@ -73,7 +70,6 @@ func (suite *KeeperTestSuite) TestGetHashFn() { func() { suite.app.StakingKeeper.SetHistoricalInfo(suite.ctx, 1, &stakingtypes.HistoricalInfo{}) suite.ctx = suite.ctx.WithBlockHeight(10) - suite.app.EvmKeeper.WithContext(suite.ctx) }, common.Hash{}, }, @@ -86,7 +82,6 @@ func (suite *KeeperTestSuite) TestGetHashFn() { } suite.app.StakingKeeper.SetHistoricalInfo(suite.ctx, 1, histInfo) suite.ctx = suite.ctx.WithBlockHeight(10) - suite.app.EvmKeeper.WithContext(suite.ctx) }, common.BytesToHash(hash), }, @@ -104,7 +99,7 @@ func (suite *KeeperTestSuite) TestGetHashFn() { tc.malleate() - hash := suite.app.EvmKeeper.GetHashFn()(tc.height) + hash := suite.app.EvmKeeper.GetHashFn(suite.ctx)(tc.height) suite.Require().Equal(tc.expHash, hash) }) } @@ -124,7 +119,6 @@ func (suite *KeeperTestSuite) TestGetCoinbaseAddress() { header := suite.ctx.BlockHeader() header.ProposerAddress = []byte{} suite.ctx = suite.ctx.WithBlockHeader(header) - suite.app.EvmKeeper.WithContext(suite.ctx) }, false, }, @@ -152,7 +146,6 @@ func (suite *KeeperTestSuite) TestGetCoinbaseAddress() { _, found := suite.app.StakingKeeper.GetValidatorByConsAddr(suite.ctx, valConsAddr.Bytes()) suite.Require().True(found) - suite.app.EvmKeeper.WithContext(suite.ctx) suite.Require().NotEmpty(suite.ctx.BlockHeader().ProposerAddress) }, true, @@ -276,10 +269,10 @@ func (suite *KeeperTestSuite) TestGetEthIntrinsicGas() { signer := ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID()) suite.ctx = suite.ctx.WithBlockHeight(tc.height) - suite.app.EvmKeeper.WithContext(suite.ctx) + nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) m, err := newNativeMessage( - suite.app.EvmKeeper.GetNonce(suite.address), + nonce, suite.ctx.BlockHeight(), suite.address, ethCfg, @@ -291,7 +284,7 @@ func (suite *KeeperTestSuite) TestGetEthIntrinsicGas() { ) suite.Require().NoError(err) - gas, err := suite.app.EvmKeeper.GetEthIntrinsicGas(m, ethCfg, tc.isContractCreation) + gas, err := suite.app.EvmKeeper.GetEthIntrinsicGas(suite.ctx, m, ethCfg, tc.isContractCreation) if tc.noError { suite.Require().NoError(err) } else { @@ -345,15 +338,16 @@ func (suite *KeeperTestSuite) TestGasToRefund() { suite.Run(fmt.Sprintf("Case %s", tc.name), func() { suite.mintFeeCollector = true suite.SetupTest() // reset - suite.app.EvmKeeper.AddRefund(10) + vmdb := suite.StateDB() + vmdb.AddRefund(10) if tc.expPanic { panicF := func() { - suite.app.EvmKeeper.GasToRefund(tc.gasconsumed, tc.refundQuotient) + keeper.GasToRefund(vmdb.GetRefund(), tc.gasconsumed, tc.refundQuotient) } suite.Require().Panics(panicF) } else { - gr := suite.app.EvmKeeper.GasToRefund(tc.gasconsumed, tc.refundQuotient) + gr := keeper.GasToRefund(vmdb.GetRefund(), tc.gasconsumed, tc.refundQuotient) suite.Require().Equal(tc.expGasRefund, gr) } }) @@ -407,9 +401,10 @@ func (suite *KeeperTestSuite) TestRefundGas() { keeperParams := suite.app.EvmKeeper.GetParams(suite.ctx) ethCfg := keeperParams.ChainConfig.EthereumConfig(suite.app.EvmKeeper.ChainID()) signer := ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID()) + vmdb := suite.StateDB() m, err := newNativeMessage( - suite.app.EvmKeeper.GetNonce(suite.address), + vmdb.GetNonce(suite.address), suite.ctx.BlockHeight(), suite.address, ethCfg, @@ -421,16 +416,16 @@ func (suite *KeeperTestSuite) TestRefundGas() { ) suite.Require().NoError(err) - suite.app.EvmKeeper.AddRefund(params.TxGas) + vmdb.AddRefund(params.TxGas) if tc.leftoverGas > m.Gas() { return } gasUsed := m.Gas() - tc.leftoverGas - refund := suite.app.EvmKeeper.GasToRefund(gasUsed, tc.refundQuotient) + refund := keeper.GasToRefund(vmdb.GetRefund(), gasUsed, tc.refundQuotient) suite.Require().Equal(tc.expGasRefund, refund) - err = suite.app.EvmKeeper.RefundGas(m, refund, "aphoton") + err = suite.app.EvmKeeper.RefundGas(suite.ctx, m, refund, "aphoton") if tc.noError { suite.Require().NoError(err) } else { @@ -487,10 +482,8 @@ func (suite *KeeperTestSuite) TestResetGasMeterAndConsumeGas() { panicF := func() { gm := sdk.NewGasMeter(10) gm.ConsumeGas(tc.gasConsumed, "") - suite.ctx = suite.ctx.WithGasMeter(gm) - suite.app.EvmKeeper.WithContext(suite.ctx) - - suite.app.EvmKeeper.ResetGasMeterAndConsumeGas(tc.gasUsed) + ctx := suite.ctx.WithGasMeter(gm) + suite.app.EvmKeeper.ResetGasMeterAndConsumeGas(ctx, tc.gasUsed) } if tc.expPanic { @@ -516,5 +509,6 @@ func (suite *KeeperTestSuite) TestEVMConfig() { func (suite *KeeperTestSuite) TestContractDeployment() { suite.SetupTest() contractAddress := suite.DeployTestContract(suite.T(), suite.address, big.NewInt(10000000000000)) - suite.Require().Greater(suite.app.EvmKeeper.GetCodeSize(contractAddress), 0) + db := suite.StateDB() + suite.Require().Greater(db.GetCodeSize(contractAddress), 0) } diff --git a/x/evm/keeper/statedb.go b/x/evm/keeper/statedb.go index 4adb50ec..ae908057 100644 --- a/x/evm/keeper/statedb.go +++ b/x/evm/keeper/statedb.go @@ -2,460 +2,38 @@ package keeper import ( "bytes" + "errors" "fmt" "math/big" - "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/crypto" - "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - + "github.com/ethereum/go-ethereum/common" ethermint "github.com/tharsis/ethermint/types" + "github.com/tharsis/ethermint/x/evm/statedb" "github.com/tharsis/ethermint/x/evm/types" ) -var _ vm.StateDB = &Keeper{} +var _ statedb.Keeper = &Keeper{} // ---------------------------------------------------------------------------- -// Account +// statedb.Keeper implementation // ---------------------------------------------------------------------------- -// CreateAccount creates a new EthAccount instance from the provided address and -// sets the value to store. If an account with the given address already exists, -// this function also resets any preexisting code and storage associated with that -// address. -func (k *Keeper) CreateAccount(addr common.Address) { - if k.HasStateError() { - return +// GetAccount returns nil if account is not exist, returns error if it's not `EthAccount` +func (k *Keeper) GetAccount(ctx sdk.Context, addr common.Address) (*statedb.Account, error) { + acct, err := k.GetAccountWithoutBalance(ctx, addr) + if acct == nil || err != nil { + return acct, err } - cosmosAddr := sdk.AccAddress(addr.Bytes()) - ctx := k.Ctx() - account := k.accountKeeper.GetAccount(ctx, cosmosAddr) - log := "" - if account == nil { - log = "account created" - } else { - log = "account overwritten" - k.ResetAccount(addr) - } - - account = k.accountKeeper.NewAccountWithAddress(ctx, cosmosAddr) - k.accountKeeper.SetAccount(ctx, account) - - k.Logger(ctx).Debug( - log, - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - ) + acct.Balance = k.GetBalance(ctx, addr) + return acct, nil } -// ---------------------------------------------------------------------------- -// Balance -// ---------------------------------------------------------------------------- - -// AddBalance adds the given amount to the address balance coin by minting new -// coins and transferring them to the address. The coin denomination is obtained -// from the module parameters. -func (k *Keeper) AddBalance(addr common.Address, amount *big.Int) { - if k.HasStateError() { - return - } - - ctx := k.Ctx() - - if amount.Sign() != 1 { - k.Logger(ctx).Debug( - "ignored non-positive amount addition", - "ethereum-address", addr.Hex(), - "amount", amount.Int64(), - ) - return - } - - cosmosAddr := sdk.AccAddress(addr.Bytes()) - params := k.GetParams(ctx) - - // Coin denom and amount already validated - coins := sdk.Coins{ - { - Denom: params.EvmDenom, - Amount: sdk.NewIntFromBigInt(amount), - }, - } - - if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, coins); err != nil { - k.Logger(ctx).Error( - "failed to mint coins when adding balance", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - "error", err, - ) - k.stateErr = err - return - } - - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, cosmosAddr, coins); err != nil { - k.Logger(ctx).Error( - "failed to send from module to account when adding balance", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - "error", err, - ) - k.stateErr = err - return - } - - k.Logger(ctx).Debug( - "balance addition", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - ) -} - -// SubBalance subtracts the given amount from the address balance by transferring the -// coins to an escrow account and then burning them. The coin denomination is obtained -// from the module parameters. This function performs a no-op if the amount is negative -// or the user doesn't have enough funds for the transfer. -func (k *Keeper) SubBalance(addr common.Address, amount *big.Int) { - if k.HasStateError() { - return - } - - ctx := k.Ctx() - - if amount.Sign() != 1 { - k.Logger(ctx).Debug( - "ignored non-positive amount addition", - "ethereum-address", addr.Hex(), - "amount", amount.Int64(), - ) - return - } - - cosmosAddr := sdk.AccAddress(addr.Bytes()) - - params := k.GetParams(ctx) - - // Coin denom and amount already validated - coins := sdk.Coins{ - { - Denom: params.EvmDenom, - Amount: sdk.NewIntFromBigInt(amount), - }, - } - - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, cosmosAddr, types.ModuleName, coins); err != nil { - k.Logger(ctx).Debug( - "failed to send from account to module when subtracting balance", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - "error", err, - ) - - return - } - - if err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, coins); err != nil { - k.Logger(ctx).Error( - "failed to burn coins when subtracting balance", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - "error", err, - ) - k.stateErr = err - return - } - - k.Logger(ctx).Debug( - "balance subtraction", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - ) -} - -// GetBalance returns the EVM denomination balance of the provided address. The -// denomination is obtained from the module parameters. -func (k *Keeper) GetBalance(addr common.Address) *big.Int { - if k.HasStateError() { - return big.NewInt(0) - } - - ctx := k.Ctx() - - cosmosAddr := sdk.AccAddress(addr.Bytes()) - params := k.GetParams(ctx) - balance := k.bankKeeper.GetBalance(ctx, cosmosAddr, params.EvmDenom) - - return balance.Amount.BigInt() -} - -// ---------------------------------------------------------------------------- -// Nonce -// ---------------------------------------------------------------------------- - -// GetNonce retrieves the account with the given address and returns the tx -// sequence (i.e nonce). The function performs a no-op if the account is not found. -func (k *Keeper) GetNonce(addr common.Address) uint64 { - if k.HasStateError() { - return 0 - } - - ctx := k.Ctx() - - cosmosAddr := sdk.AccAddress(addr.Bytes()) - nonce, err := k.accountKeeper.GetSequence(ctx, cosmosAddr) - if err != nil { - k.Logger(ctx).Error( - "account not found", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - "error", err, - ) - // since adding state error here will break some logic in the go-ethereum (unwanted panic), no - // state error will be store here - // Refer panic: https://github.com/ethereum/go-ethereum/blob/991384a7f6719e1125ca0be7fb27d0c4d1c5d2d3/core/vm/operations_acl.go#L66 - } - - return nonce -} - -// SetNonce sets the given nonce as the sequence of the address' account. If the -// account doesn't exist, a new one will be created from the address. -func (k *Keeper) SetNonce(addr common.Address, nonce uint64) { - if k.HasStateError() { - return - } - - ctx := k.Ctx() - - cosmosAddr := sdk.AccAddress(addr.Bytes()) - account := k.accountKeeper.GetAccount(ctx, cosmosAddr) - if account == nil { - k.Logger(ctx).Debug( - "account not found", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - ) - - // create address if it doesn't exist - account = k.accountKeeper.NewAccountWithAddress(ctx, cosmosAddr) - } - - if err := account.SetSequence(nonce); err != nil { - k.Logger(ctx).Error( - "failed to set nonce", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - "nonce", nonce, - "error", err, - ) - k.stateErr = err - return - } - - k.accountKeeper.SetAccount(ctx, account) - - k.Logger(ctx).Debug( - "nonce set", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - "nonce", nonce, - ) -} - -// ---------------------------------------------------------------------------- -// Code -// ---------------------------------------------------------------------------- - -// GetCodeHash fetches the account from the store and returns its code hash. If the account doesn't -// exist or is not an EthAccount type, GetCodeHash returns the empty code hash value. -func (k *Keeper) GetCodeHash(addr common.Address) common.Hash { - if k.HasStateError() { - return common.Hash{} - } - - ctx := k.Ctx() - cosmosAddr := sdk.AccAddress(addr.Bytes()) - - account := k.accountKeeper.GetAccount(ctx, cosmosAddr) - if account == nil { - return common.BytesToHash(types.EmptyCodeHash) - } - - ethAccount, isEthAccount := account.(*ethermint.EthAccount) - if !isEthAccount { - return common.BytesToHash(types.EmptyCodeHash) - } - - return common.HexToHash(ethAccount.CodeHash) -} - -// GetCode returns the code byte array associated with the given address. -// If the code hash from the account is empty, this function returns nil. -func (k *Keeper) GetCode(addr common.Address) []byte { - if k.HasStateError() { - return nil - } - - ctx := k.Ctx() - hash := k.GetCodeHash(addr) - - if bytes.Equal(hash.Bytes(), common.BytesToHash(types.EmptyCodeHash).Bytes()) { - return nil - } - - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixCode) - code := store.Get(hash.Bytes()) - - if len(code) == 0 { - k.Logger(ctx).Debug( - "code not found", - "ethereum-address", addr.Hex(), - "code-hash", hash.Hex(), - ) - } - - return code -} - -// SetCode stores the code byte array to the application KVStore and sets the -// code hash to the given account. The code is deleted from the store if it is empty. -func (k *Keeper) SetCode(addr common.Address, code []byte) { - if k.HasStateError() { - return - } - - ctx := k.Ctx() - - if bytes.Equal(code, types.EmptyCodeHash) { - k.Logger(ctx).Debug("passed in EmptyCodeHash, but expected empty code") - } - hash := crypto.Keccak256Hash(code) - - // update account code hash - account := k.accountKeeper.GetAccount(ctx, addr.Bytes()) - if account == nil { - account = k.accountKeeper.NewAccountWithAddress(ctx, addr.Bytes()) - k.accountKeeper.SetAccount(ctx, account) - } - - ethAccount, isEthAccount := account.(*ethermint.EthAccount) - if !isEthAccount { - k.Logger(ctx).Error( - "invalid account type", - "ethereum-address", addr.Hex(), - "code-hash", hash.Hex(), - ) - k.stateErr = fmt.Errorf("invalid account type, ethereum-address %v, code-hash %v", addr.Hex(), hash.Hex()) - return - } - - ethAccount.CodeHash = hash.Hex() - k.accountKeeper.SetAccount(ctx, ethAccount) - - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixCode) - - action := "updated" - - // store or delete code - if len(code) == 0 { - store.Delete(hash.Bytes()) - action = "deleted" - } else { - store.Set(hash.Bytes(), code) - } - - k.Logger(ctx).Debug( - fmt.Sprintf("code %s", action), - "ethereum-address", addr.Hex(), - "code-hash", hash.Hex(), - ) -} - -// GetCodeSize returns the size of the contract code associated with this object, -// or zero if none. -func (k *Keeper) GetCodeSize(addr common.Address) int { - if k.HasStateError() { - return 0 - } - - code := k.GetCode(addr) - return len(code) -} - -// ---------------------------------------------------------------------------- -// Refund -// ---------------------------------------------------------------------------- - -// NOTE: gas refunded needs to be tracked and stored in a separate variable in -// order to add it subtract/add it from/to the gas used value after the EVM -// execution has finalized. The refund value is cleared on every transaction and -// at the end of every block. - -// AddRefund adds the given amount of gas to the refund transient value. -func (k *Keeper) AddRefund(gas uint64) { - if k.HasStateError() { - return - } - - ctx := k.Ctx() - refund := k.GetRefund() - - refund += gas - - store := ctx.TransientStore(k.transientKey) - store.Set(types.KeyPrefixTransientRefund, sdk.Uint64ToBigEndian(refund)) -} - -// SubRefund subtracts the given amount of gas from the transient refund value. This function -// will panic if gas amount is greater than the stored refund. -func (k *Keeper) SubRefund(gas uint64) { - if k.HasStateError() { - return - } - - ctx := k.Ctx() - refund := k.GetRefund() - - if gas > refund { - // TODO: (@fedekunze) set to 0?? Geth panics here - panic("refund counter below zero") - } - - refund -= gas - - store := ctx.TransientStore(k.transientKey) - store.Set(types.KeyPrefixTransientRefund, sdk.Uint64ToBigEndian(refund)) -} - -// GetRefund returns the amount of gas available for return after the tx execution -// finalizes. This value is reset to 0 on every transaction. -func (k *Keeper) GetRefund() uint64 { - if k.HasStateError() { - return 0 - } - - ctx := k.Ctx() - store := ctx.TransientStore(k.transientKey) - - bz := store.Get(types.KeyPrefixTransientRefund) - if len(bz) == 0 { - return 0 - } - - return sdk.BigEndianToUint64(bz) -} - -// ---------------------------------------------------------------------------- -// State -// ---------------------------------------------------------------------------- - -func doGetState(ctx sdk.Context, storeKey sdk.StoreKey, addr common.Address, key common.Hash) common.Hash { - store := prefix.NewStore(ctx.KVStore(storeKey), types.AddressStoragePrefix(addr)) +// GetState loads contract state from database, implements `statedb.Keeper` interface. +func (k *Keeper) GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AddressStoragePrefix(addr)) value := store.Get(key.Bytes()) if len(value) == 0 { @@ -465,355 +43,14 @@ func doGetState(ctx sdk.Context, storeKey sdk.StoreKey, addr common.Address, key return common.BytesToHash(value) } -// GetCommittedState returns the value set in store for the given key hash. If the key is not registered -// this function returns the empty hash. -func (k *Keeper) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { - if k.HasStateError() { - return common.Hash{} - } - - return doGetState(k.ctxStack.initialCtx, k.storeKey, addr, hash) +// GetCode loads contract code from database, implements `statedb.Keeper` interface. +func (k *Keeper) GetCode(ctx sdk.Context, codeHash common.Hash) []byte { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixCode) + return store.Get(codeHash.Bytes()) } -// GetState returns the committed state for the given key hash, as all changes are committed directly -// to the KVStore. -func (k *Keeper) GetState(addr common.Address, hash common.Hash) common.Hash { - if k.HasStateError() { - return common.Hash{} - } - - ctx := k.Ctx() - return doGetState(ctx, k.storeKey, addr, hash) -} - -// SetState sets the given hashes (key, value) to the KVStore. If the value hash is empty, this -// function deletes the key from the store. -func (k *Keeper) SetState(addr common.Address, key, value common.Hash) { - if k.HasStateError() { - return - } - - ctx := k.Ctx() - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AddressStoragePrefix(addr)) - - action := "updated" - if ethermint.IsEmptyHash(value.Hex()) { - store.Delete(key.Bytes()) - action = "deleted" - } else { - store.Set(key.Bytes(), value.Bytes()) - } - - k.Logger(ctx).Debug( - fmt.Sprintf("state %s", action), - "ethereum-address", addr.Hex(), - "key", key.Hex(), - ) -} - -// ---------------------------------------------------------------------------- -// Suicide -// ---------------------------------------------------------------------------- - -// Suicide marks the given account as suicided and clears the account balance of -// the EVM tokens. -func (k *Keeper) Suicide(addr common.Address) bool { - if k.HasStateError() { - return false - } - - ctx := k.Ctx() - - prev := k.HasSuicided(addr) - if prev { - return true - } - - cosmosAddr := sdk.AccAddress(addr.Bytes()) - - _, err := k.ClearBalance(cosmosAddr) - if err != nil { - k.Logger(ctx).Error( - "failed to subtract balance on suicide", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - "error", err, - ) - k.stateErr = err - return false - } - - k.setSuicided(ctx, addr) - - // delete account code and state - k.ResetAccount(addr) - - k.Logger(ctx).Debug( - "account suicided", - "ethereum-address", addr.Hex(), - "cosmos-address", cosmosAddr.String(), - ) - - return true -} - -// setSuicided sets a single byte to the transient store and marks the address as suicided -func (k Keeper) setSuicided(ctx sdk.Context, addr common.Address) { - store := prefix.NewStore(ctx.TransientStore(k.transientKey), types.KeyPrefixTransientSuicided) - store.Set(addr.Bytes(), []byte{1}) -} - -// HasSuicided queries the transient store to check if the account has been marked as suicided in the -// current block. Accounts that are suicided will be returned as non-nil during queries and "cleared" -// after the block has been committed. -func (k *Keeper) HasSuicided(addr common.Address) bool { - if k.HasStateError() { - return false - } - - ctx := k.Ctx() - store := prefix.NewStore(ctx.TransientStore(k.transientKey), types.KeyPrefixTransientSuicided) - return store.Has(addr.Bytes()) -} - -// ---------------------------------------------------------------------------- -// Account Exist / Empty -// ---------------------------------------------------------------------------- - -// Exist returns true if the given account exists in store or if it has been -// marked as suicided in the transient store. -func (k *Keeper) Exist(addr common.Address) bool { - if k.HasStateError() { - return false - } - - ctx := k.Ctx() - // return true if the account has suicided - if k.HasSuicided(addr) { - return true - } - - cosmosAddr := sdk.AccAddress(addr.Bytes()) - account := k.accountKeeper.GetAccount(ctx, cosmosAddr) - return account != nil -} - -// Empty returns true if the address meets the following conditions: -// - nonce is 0 -// - balance amount for evm denom is 0 -// - account code hash is empty -// -// Non-ethereum accounts are considered not empty -func (k *Keeper) Empty(addr common.Address) bool { - if k.HasStateError() { - return false - } - - ctx := k.Ctx() - nonce := uint64(0) - codeHash := types.EmptyCodeHash - - cosmosAddr := sdk.AccAddress(addr.Bytes()) - account := k.accountKeeper.GetAccount(ctx, cosmosAddr) - - if account != nil { - nonce = account.GetSequence() - ethAccount, isEthAccount := account.(*ethermint.EthAccount) - if !isEthAccount { - return false - } - - codeHash = common.HexToHash(ethAccount.CodeHash).Bytes() - } - - balance := k.GetBalance(addr) - hasZeroBalance := balance.Sign() == 0 - hasEmptyCodeHash := bytes.Equal(codeHash, types.EmptyCodeHash) - - return hasZeroBalance && nonce == 0 && hasEmptyCodeHash -} - -// ---------------------------------------------------------------------------- -// Access List -// ---------------------------------------------------------------------------- - -// PrepareAccessList handles the preparatory steps for executing a state transition with -// regards to both EIP-2929 and EIP-2930: -// -// - 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) -// -// This method should only be called if Yolov3/Berlin/2929+2930 is applicable at the current number. -func (k *Keeper) PrepareAccessList(sender common.Address, dest *common.Address, precompiles []common.Address, txAccesses ethtypes.AccessList) { - if k.HasStateError() { - return - } - - k.AddAddressToAccessList(sender) - if dest != nil { - k.AddAddressToAccessList(*dest) - // If it's a create-tx, the destination will be added inside evm.create - } - for _, addr := range precompiles { - k.AddAddressToAccessList(addr) - } - for _, tuple := range txAccesses { - k.AddAddressToAccessList(tuple.Address) - for _, key := range tuple.StorageKeys { - k.AddSlotToAccessList(tuple.Address, key) - } - } -} - -// AddressInAccessList returns true if the address is registered on the transient store. -func (k *Keeper) AddressInAccessList(addr common.Address) bool { - if k.HasStateError() { - return false - } - - ctx := k.Ctx() - ts := prefix.NewStore(ctx.TransientStore(k.transientKey), types.KeyPrefixTransientAccessListAddress) - return ts.Has(addr.Bytes()) -} - -// SlotInAccessList checks if the address and the slots are registered in the transient store -func (k *Keeper) SlotInAccessList(addr common.Address, slot common.Hash) (addressOk, slotOk bool) { - if k.HasStateError() { - return false, false - } - - addressOk = k.AddressInAccessList(addr) - slotOk = k.addressSlotInAccessList(addr, slot) - return addressOk, slotOk -} - -// addressSlotInAccessList returns true if the address's slot is registered on the transient store. -func (k *Keeper) addressSlotInAccessList(addr common.Address, slot common.Hash) bool { - ctx := k.Ctx() - ts := prefix.NewStore(ctx.TransientStore(k.transientKey), types.KeyPrefixTransientAccessListSlot) - key := append(addr.Bytes(), slot.Bytes()...) - return ts.Has(key) -} - -// AddAddressToAccessList adds the given address to the access list. If the address is already -// in the access list, this function performs a no-op. -func (k *Keeper) AddAddressToAccessList(addr common.Address) { - if k.HasStateError() { - return - } - - if k.AddressInAccessList(addr) { - return - } - - ctx := k.Ctx() - ts := prefix.NewStore(ctx.TransientStore(k.transientKey), types.KeyPrefixTransientAccessListAddress) - ts.Set(addr.Bytes(), []byte{0x1}) -} - -// AddSlotToAccessList adds the given (address, slot) to the access list. If the address and slot are -// already in the access list, this function performs a no-op. -func (k *Keeper) AddSlotToAccessList(addr common.Address, slot common.Hash) { - if k.HasStateError() { - return - } - - k.AddAddressToAccessList(addr) - if k.addressSlotInAccessList(addr, slot) { - return - } - - ctx := k.Ctx() - ts := prefix.NewStore(ctx.TransientStore(k.transientKey), types.KeyPrefixTransientAccessListSlot) - key := append(addr.Bytes(), slot.Bytes()...) - ts.Set(key, []byte{0x1}) -} - -// ClearAccessList clear current access list -func (k *Keeper) ClearAccessList() { - ctx := k.Ctx() - ts := prefix.NewStore(ctx.TransientStore(k.transientKey), types.KeyPrefixTransientAccessListSlot) - itr := ts.Iterator(nil, nil) - for ; itr.Valid(); itr.Next() { - ts.Delete(itr.Key()) - } -} - -// ---------------------------------------------------------------------------- -// Snapshotting -// ---------------------------------------------------------------------------- - -// Snapshot return the index in the cached context stack -func (k *Keeper) Snapshot() int { - if k.HasStateError() { - return 0 - } - - return k.ctxStack.Snapshot() -} - -// RevertToSnapshot pop all the cached contexts after(including) the snapshot -func (k *Keeper) RevertToSnapshot(target int) { - if k.HasStateError() { - return - } - - k.ctxStack.RevertToSnapshot(target) -} - -// ---------------------------------------------------------------------------- -// Log -// ---------------------------------------------------------------------------- - -// AddLog appends the given ethereum Log to the list of Logs associated with the transaction hash kept in the current -// context. This function also fills in the tx hash, block hash, tx index and log index fields before setting the log -// to store. -func (k *Keeper) AddLog(log *ethtypes.Log) { - if k.HasStateError() { - return - } - - ctx := k.Ctx() - - log.BlockHash = common.BytesToHash(ctx.HeaderHash()) - log.TxIndex = uint(k.GetTxIndexTransient()) - log.TxHash = k.GetTxHashTransient() - - log.Index = uint(k.GetLogSizeTransient()) - k.IncreaseLogSizeTransient() - k.AddLogTransient(log) - - k.Logger(ctx).Debug( - "log added", - "tx-hash-ethereum", log.TxHash.Hex(), - "log-index", int(log.Index), - ) -} - -// ---------------------------------------------------------------------------- -// Trie -// ---------------------------------------------------------------------------- - -// AddPreimage performs a no-op since the EnablePreimageRecording flag is disabled -// on the vm.Config during state transitions. No store trie preimages are written -// to the database. -func (k *Keeper) AddPreimage(_ common.Hash, _ []byte) {} - -// ---------------------------------------------------------------------------- -// Iterator -// ---------------------------------------------------------------------------- - -// ForEachStorage uses the store iterator to iterate over all the state keys and perform a callback -// function on each of them. -// The callback should return `true` to continue, return `false` to break early. -func (k *Keeper) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error { - if k.HasStateError() { - return k.stateErr - } - - ctx := k.Ctx() +// ForEachStorage iterate contract storage, callback return false to break early +func (k *Keeper) ForEachStorage(ctx sdk.Context, addr common.Address, cb func(key, value common.Hash) bool) { store := ctx.KVStore(k.storeKey) prefix := types.AddressStoragePrefix(addr) @@ -821,26 +58,155 @@ func (k *Keeper) ForEachStorage(addr common.Address, cb func(key, value common.H defer iterator.Close() for ; iterator.Valid(); iterator.Next() { - - // TODO: check if the key prefix needs to be trimmed key := common.BytesToHash(iterator.Key()) value := common.BytesToHash(iterator.Value()) // check if iteration stops if !cb(key, value) { - return nil + return } } +} +// SetBalance update account's balance, compare with current balance first, then decide to mint or burn. +func (k *Keeper) SetBalance(ctx sdk.Context, addr common.Address, amount *big.Int) error { + cosmosAddr := sdk.AccAddress(addr.Bytes()) + + params := k.GetParams(ctx) + coin := k.bankKeeper.GetBalance(ctx, cosmosAddr, params.EvmDenom) + balance := coin.Amount.BigInt() + delta := new(big.Int).Sub(amount, balance) + switch delta.Sign() { + case 1: + // mint + coins := sdk.NewCoins(sdk.NewCoin(params.EvmDenom, sdk.NewIntFromBigInt(delta))) + if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, coins); err != nil { + return err + } + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, cosmosAddr, coins); err != nil { + return err + } + case -1: + // burn + coins := sdk.NewCoins(sdk.NewCoin(params.EvmDenom, sdk.NewIntFromBigInt(new(big.Int).Neg(delta)))) + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, cosmosAddr, types.ModuleName, coins); err != nil { + return err + } + if err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, coins); err != nil { + return err + } + default: + // not changed + } return nil } -// HasStateError return the previous error for any state operations -func (k *Keeper) HasStateError() bool { - return k.stateErr != nil +// SetAccount updates nonce/balance/codeHash together. +func (k *Keeper) SetAccount(ctx sdk.Context, addr common.Address, account statedb.Account) error { + // update account + cosmosAddr := sdk.AccAddress(addr.Bytes()) + acct := k.accountKeeper.GetAccount(ctx, cosmosAddr) + if acct == nil { + acct = k.accountKeeper.NewAccountWithAddress(ctx, cosmosAddr) + } + ethAcct, ok := acct.(*ethermint.EthAccount) + if !ok { + return errors.New("not EthAccount") + } + if err := ethAcct.SetSequence(account.Nonce); err != nil { + return err + } + ethAcct.CodeHash = common.BytesToHash(account.CodeHash).Hex() + k.accountKeeper.SetAccount(ctx, ethAcct) + + err := k.SetBalance(ctx, addr, account.Balance) + k.Logger(ctx).Debug( + "account updated", + "ethereum-address", addr.Hex(), + "nonce", account.Nonce, + "codeHash", common.BytesToHash(account.CodeHash).Hex(), + "balance", account.Balance, + ) + return err } -// ClearStateError reset the previous state operation error to nil -func (k *Keeper) ClearStateError() { - k.stateErr = nil +// SetState update contract storage, delete if value is empty. +func (k *Keeper) SetState(ctx sdk.Context, addr common.Address, key common.Hash, value []byte) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AddressStoragePrefix(addr)) + action := "updated" + if len(value) == 0 { + store.Delete(key.Bytes()) + action = "deleted" + } else { + store.Set(key.Bytes(), value) + } + k.Logger(ctx).Debug( + fmt.Sprintf("state %s", action), + "ethereum-address", addr.Hex(), + "key", key.Hex(), + ) +} + +// SetCode set contract code, delete if code is empty. +func (k *Keeper) SetCode(ctx sdk.Context, codeHash, code []byte) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixCode) + + // store or delete code + action := "updated" + if len(code) == 0 { + store.Delete(codeHash) + action = "deleted" + } else { + store.Set(codeHash, code) + } + k.Logger(ctx).Debug( + fmt.Sprintf("code %s", action), + "code-hash", common.BytesToHash(codeHash).Hex(), + ) +} + +// DeleteAccount handles contract's suicide call: +// - clear balance +// - remove code +// - remove states +// - remove auth account +func (k *Keeper) DeleteAccount(ctx sdk.Context, addr common.Address) error { + cosmosAddr := sdk.AccAddress(addr.Bytes()) + acct := k.accountKeeper.GetAccount(ctx, cosmosAddr) + if acct == nil { + return nil + } + + ethAcct, ok := acct.(*ethermint.EthAccount) + if !ok { + return errors.New("not EthAccount") + } + + // clear balance + if err := k.SetBalance(ctx, addr, new(big.Int)); err != nil { + return err + } + + // remove code + codeHash := common.HexToHash(ethAcct.CodeHash).Bytes() + if !bytes.Equal(codeHash, types.EmptyCodeHash) { + k.SetCode(ctx, codeHash, nil) + } + + // clear storage + k.ForEachStorage(ctx, addr, func(key, value common.Hash) bool { + k.SetState(ctx, addr, key, nil) + return true + }) + + // remove auth account + k.accountKeeper.RemoveAccount(ctx, acct) + + k.Logger(ctx).Debug( + "account suicided", + "ethereum-address", addr.Hex(), + "cosmos-address", cosmosAddr.String(), + ) + + return nil } diff --git a/x/evm/keeper/statedb_benchmark_test.go b/x/evm/keeper/statedb_benchmark_test.go index baaa27a4..834a8161 100644 --- a/x/evm/keeper/statedb_benchmark_test.go +++ b/x/evm/keeper/statedb_benchmark_test.go @@ -16,6 +16,7 @@ import ( func BenchmarkCreateAccountNew(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() b.ResetTimer() b.ReportAllocs() @@ -24,25 +25,27 @@ func BenchmarkCreateAccountNew(b *testing.B) { b.StopTimer() addr := tests.GenerateAddress() b.StartTimer() - suite.app.EvmKeeper.CreateAccount(addr) + vmdb.CreateAccount(addr) } } func BenchmarkCreateAccountExisting(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - suite.app.EvmKeeper.CreateAccount(suite.address) + vmdb.CreateAccount(suite.address) } } func BenchmarkAddBalance(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() amt := big.NewInt(10) @@ -50,13 +53,14 @@ func BenchmarkAddBalance(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - suite.app.EvmKeeper.AddBalance(suite.address, amt) + vmdb.AddBalance(suite.address, amt) } } func BenchmarkSetCode(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() hash := crypto.Keccak256Hash([]byte("code")).Bytes() @@ -64,13 +68,14 @@ func BenchmarkSetCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - suite.app.EvmKeeper.SetCode(suite.address, hash) + vmdb.SetCode(suite.address, hash) } } func BenchmarkSetState(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() hash := crypto.Keccak256Hash([]byte("topic")).Bytes() @@ -78,13 +83,14 @@ func BenchmarkSetState(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - suite.app.EvmKeeper.SetCode(suite.address, hash) + vmdb.SetCode(suite.address, hash) } } func BenchmarkAddLog(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() topic := crypto.Keccak256Hash([]byte("topic")) txHash := crypto.Keccak256Hash([]byte("tx_hash")) @@ -94,7 +100,7 @@ func BenchmarkAddLog(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - suite.app.EvmKeeper.AddLog(ðtypes.Log{ + vmdb.AddLog(ðtypes.Log{ Address: suite.address, Topics: []common.Hash{topic}, Data: []byte("data"), @@ -111,18 +117,19 @@ func BenchmarkAddLog(b *testing.B) { func BenchmarkSnapshot(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - target := suite.app.EvmKeeper.Snapshot() + target := vmdb.Snapshot() require.Equal(b, i, target) } for i := b.N - 1; i >= 0; i-- { require.NotPanics(b, func() { - suite.app.EvmKeeper.RevertToSnapshot(i) + vmdb.RevertToSnapshot(i) }) } } @@ -130,6 +137,7 @@ func BenchmarkSnapshot(b *testing.B) { func BenchmarkSubBalance(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() amt := big.NewInt(10) @@ -137,46 +145,49 @@ func BenchmarkSubBalance(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - suite.app.EvmKeeper.SubBalance(suite.address, amt) + vmdb.SubBalance(suite.address, amt) } } func BenchmarkSetNonce(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - suite.app.EvmKeeper.SetNonce(suite.address, 1) + vmdb.SetNonce(suite.address, 1) } } func BenchmarkAddRefund(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - suite.app.EvmKeeper.AddRefund(1) + vmdb.AddRefund(1) } } func BenchmarkSuicide(b *testing.B) { suite := KeeperTestSuite{} suite.DoSetupTest(b) + vmdb := suite.StateDB() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { b.StopTimer() addr := tests.GenerateAddress() - suite.app.EvmKeeper.CreateAccount(addr) + vmdb.CreateAccount(addr) b.StartTimer() - suite.app.EvmKeeper.Suicide(addr) + vmdb.Suicide(addr) } } diff --git a/x/evm/keeper/statedb_test.go b/x/evm/keeper/statedb_test.go index 837234e8..b7550099 100644 --- a/x/evm/keeper/statedb_test.go +++ b/x/evm/keeper/statedb_test.go @@ -13,8 +13,10 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/tharsis/ethermint/tests" + "github.com/tharsis/ethermint/x/evm/statedb" "github.com/tharsis/ethermint/x/evm/types" ) @@ -22,39 +24,38 @@ func (suite *KeeperTestSuite) TestCreateAccount() { testCases := []struct { name string addr common.Address - malleate func(common.Address) - callback func(common.Address) + malleate func(vm.StateDB, common.Address) + callback func(vm.StateDB, common.Address) }{ { "reset account (keep balance)", suite.address, - func(addr common.Address) { - suite.app.EvmKeeper.AddBalance(addr, big.NewInt(100)) - suite.Require().NotZero(suite.app.EvmKeeper.GetBalance(addr).Int64()) + func(vmdb vm.StateDB, addr common.Address) { + vmdb.AddBalance(addr, big.NewInt(100)) + suite.Require().NotZero(vmdb.GetBalance(addr).Int64()) }, - func(addr common.Address) { - suite.Require().Equal(suite.app.EvmKeeper.GetBalance(addr).Int64(), int64(100)) + func(vmdb vm.StateDB, addr common.Address) { + suite.Require().Equal(vmdb.GetBalance(addr).Int64(), int64(100)) }, }, { "create account", tests.GenerateAddress(), - func(addr common.Address) { - acc := suite.app.AccountKeeper.GetAccount(suite.ctx, addr.Bytes()) - suite.Require().Nil(acc) + func(vmdb vm.StateDB, addr common.Address) { + suite.Require().False(vmdb.Exist(addr)) }, - func(addr common.Address) { - acc := suite.app.AccountKeeper.GetAccount(suite.ctx, addr.Bytes()) - suite.Require().NotNil(acc) + func(vmdb vm.StateDB, addr common.Address) { + suite.Require().True(vmdb.Exist(addr)) }, }, } for _, tc := range testCases { suite.Run(tc.name, func() { - tc.malleate(tc.addr) - suite.app.EvmKeeper.CreateAccount(tc.addr) - tc.callback(tc.addr) + vmdb := suite.StateDB() + tc.malleate(vmdb, tc.addr) + vmdb.CreateAccount(tc.addr) + tc.callback(vmdb, tc.addr) }) } } @@ -78,15 +79,16 @@ func (suite *KeeperTestSuite) TestAddBalance() { { "negative amount", big.NewInt(-1), - true, + false, // seems to be consistent with go-ethereum's implementation }, } for _, tc := range testCases { suite.Run(tc.name, func() { - prev := suite.app.EvmKeeper.GetBalance(suite.address) - suite.app.EvmKeeper.AddBalance(suite.address, tc.amount) - post := suite.app.EvmKeeper.GetBalance(suite.address) + vmdb := suite.StateDB() + prev := vmdb.GetBalance(suite.address) + vmdb.AddBalance(suite.address, tc.amount) + post := vmdb.GetBalance(suite.address) if tc.isNoOp { suite.Require().Equal(prev.Int64(), post.Int64()) @@ -101,44 +103,45 @@ func (suite *KeeperTestSuite) TestSubBalance() { testCases := []struct { name string amount *big.Int - malleate func() + malleate func(vm.StateDB) isNoOp bool }{ { "positive amount, below zero", big.NewInt(100), - func() {}, - true, + func(vm.StateDB) {}, + false, }, { - "positive amount, below zero", + "positive amount, above zero", big.NewInt(50), - func() { - suite.app.EvmKeeper.AddBalance(suite.address, big.NewInt(100)) + func(vmdb vm.StateDB) { + vmdb.AddBalance(suite.address, big.NewInt(100)) }, false, }, { "zero amount", big.NewInt(0), - func() {}, + func(vm.StateDB) {}, true, }, { "negative amount", big.NewInt(-1), - func() {}, - true, + func(vm.StateDB) {}, + false, }, } for _, tc := range testCases { suite.Run(tc.name, func() { - tc.malleate() + vmdb := suite.StateDB() + tc.malleate(vmdb) - prev := suite.app.EvmKeeper.GetBalance(suite.address) - suite.app.EvmKeeper.SubBalance(suite.address, tc.amount) - post := suite.app.EvmKeeper.GetBalance(suite.address) + prev := vmdb.GetBalance(suite.address) + vmdb.SubBalance(suite.address, tc.amount) + post := vmdb.GetBalance(suite.address) if tc.isNoOp { suite.Require().Equal(prev.Int64(), post.Int64()) @@ -154,29 +157,30 @@ func (suite *KeeperTestSuite) TestGetNonce() { name string address common.Address expectedNonce uint64 - malleate func() + malleate func(vm.StateDB) }{ { "account not found", tests.GenerateAddress(), 0, - func() {}, + func(vm.StateDB) {}, }, { "existing account", suite.address, 1, - func() { - suite.app.EvmKeeper.SetNonce(suite.address, 1) + func(vmdb vm.StateDB) { + vmdb.SetNonce(suite.address, 1) }, }, } for _, tc := range testCases { suite.Run(tc.name, func() { - tc.malleate() + vmdb := suite.StateDB() + tc.malleate(vmdb) - nonce := suite.app.EvmKeeper.GetNonce(tc.address) + nonce := vmdb.GetNonce(tc.address) suite.Require().Equal(tc.expectedNonce, nonce) }) } @@ -205,8 +209,9 @@ func (suite *KeeperTestSuite) TestSetNonce() { for _, tc := range testCases { suite.Run(tc.name, func() { - suite.app.EvmKeeper.SetNonce(tc.address, tc.nonce) - nonce := suite.app.EvmKeeper.GetNonce(tc.address) + vmdb := suite.StateDB() + vmdb.SetNonce(tc.address, tc.nonce) + nonce := vmdb.GetNonce(tc.address) suite.Require().Equal(tc.nonce, nonce) }) } @@ -221,35 +226,36 @@ func (suite *KeeperTestSuite) TestGetCodeHash() { name string address common.Address expHash common.Hash - malleate func() + malleate func(vm.StateDB) }{ { "account not found", tests.GenerateAddress(), - common.BytesToHash(types.EmptyCodeHash), - func() {}, + common.Hash{}, + func(vm.StateDB) {}, }, { - "account not EthAccount type", + "account not EthAccount type, error", addr, - common.BytesToHash(types.EmptyCodeHash), - func() {}, + common.Hash{}, + func(vm.StateDB) {}, }, { "existing account", suite.address, crypto.Keccak256Hash([]byte("codeHash")), - func() { - suite.app.EvmKeeper.SetCode(suite.address, []byte("codeHash")) + func(vmdb vm.StateDB) { + vmdb.SetCode(suite.address, []byte("codeHash")) }, }, } for _, tc := range testCases { suite.Run(tc.name, func() { - tc.malleate() + vmdb := suite.StateDB() + tc.malleate(vmdb) - hash := suite.app.EvmKeeper.GetCodeHash(tc.address) + hash := vmdb.GetCodeHash(tc.address) suite.Require().Equal(tc.expHash, hash) }) } @@ -294,9 +300,10 @@ func (suite *KeeperTestSuite) TestSetCode() { for _, tc := range testCases { suite.Run(tc.name, func() { - prev := suite.app.EvmKeeper.GetCode(tc.address) - suite.app.EvmKeeper.SetCode(tc.address, tc.code) - post := suite.app.EvmKeeper.GetCode(tc.address) + vmdb := suite.StateDB() + prev := vmdb.GetCode(tc.address) + vmdb.SetCode(tc.address, tc.code) + post := vmdb.GetCode(tc.address) if tc.isNoOp { suite.Require().Equal(prev, post) @@ -304,9 +311,7 @@ func (suite *KeeperTestSuite) TestSetCode() { suite.Require().Equal(tc.code, post) } - suite.Require().Equal(len(post), suite.app.EvmKeeper.GetCodeSize(tc.address)) - - suite.app.EvmKeeper.ClearStateError() + suite.Require().Equal(len(post), vmdb.GetCodeSize(tc.address)) }) } } @@ -314,21 +319,21 @@ func (suite *KeeperTestSuite) TestSetCode() { func (suite *KeeperTestSuite) TestRefund() { testCases := []struct { name string - malleate func() + malleate func(vm.StateDB) expRefund uint64 expPanic bool }{ { "success - add and subtract refund", - func() { - suite.app.EvmKeeper.AddRefund(11) + func(vmdb vm.StateDB) { + vmdb.AddRefund(11) }, 1, false, }, { "fail - subtract amount > current refund", - func() { + func(vm.StateDB) { }, 0, true, @@ -337,18 +342,15 @@ func (suite *KeeperTestSuite) TestRefund() { for _, tc := range testCases { suite.Run(tc.name, func() { - tc.malleate() + vmdb := suite.StateDB() + tc.malleate(vmdb) if tc.expPanic { - suite.Require().Panics(func() { suite.app.EvmKeeper.SubRefund(10) }) + suite.Require().Panics(func() { vmdb.SubRefund(10) }) } else { - suite.app.EvmKeeper.SubRefund(10) - suite.Require().Equal(tc.expRefund, suite.app.EvmKeeper.GetRefund()) + vmdb.SubRefund(10) + suite.Require().Equal(tc.expRefund, vmdb.GetRefund()) } - - // clear and reset refund from store - suite.app.EvmKeeper.ResetRefundTransient(suite.ctx) - suite.Require().Zero(suite.app.EvmKeeper.GetRefund()) }) } } @@ -372,8 +374,9 @@ func (suite *KeeperTestSuite) TestState() { for _, tc := range testCases { suite.Run(tc.name, func() { - suite.app.EvmKeeper.SetState(suite.address, tc.key, tc.value) - value := suite.app.EvmKeeper.GetState(suite.address, tc.key) + vmdb := suite.StateDB() + vmdb.SetState(suite.address, tc.key, tc.value) + value := vmdb.GetState(suite.address, tc.key) suite.Require().Equal(tc.value, value) }) } @@ -386,99 +389,120 @@ func (suite *KeeperTestSuite) TestCommittedState() { value1 := common.BytesToHash([]byte("value1")) value2 := common.BytesToHash([]byte("value2")) - suite.app.EvmKeeper.SetState(suite.address, key, value1) + vmdb := suite.StateDB() + vmdb.SetState(suite.address, key, value1) + vmdb.Commit() - suite.app.EvmKeeper.Snapshot() - - suite.app.EvmKeeper.SetState(suite.address, key, value2) - tmp := suite.app.EvmKeeper.GetState(suite.address, key) + vmdb = suite.StateDB() + vmdb.SetState(suite.address, key, value2) + tmp := vmdb.GetState(suite.address, key) suite.Require().Equal(value2, tmp) - tmp = suite.app.EvmKeeper.GetCommittedState(suite.address, key) + tmp = vmdb.GetCommittedState(suite.address, key) suite.Require().Equal(value1, tmp) + vmdb.Commit() - suite.app.EvmKeeper.CommitCachedContexts() - - tmp = suite.app.EvmKeeper.GetCommittedState(suite.address, key) + vmdb = suite.StateDB() + tmp = vmdb.GetCommittedState(suite.address, key) suite.Require().Equal(value2, tmp) } func (suite *KeeperTestSuite) TestSuicide() { code := []byte("code") + db := suite.StateDB() // Add code to account - suite.app.EvmKeeper.SetCode(suite.address, code) - suite.Require().Equal(code, suite.app.EvmKeeper.GetCode(suite.address)) + db.SetCode(suite.address, code) + suite.Require().Equal(code, db.GetCode(suite.address)) // Add state to account for i := 0; i < 5; i++ { - suite.app.EvmKeeper.SetState(suite.address, common.BytesToHash([]byte(fmt.Sprintf("key%d", i))), common.BytesToHash([]byte(fmt.Sprintf("value%d", i)))) + db.SetState(suite.address, common.BytesToHash([]byte(fmt.Sprintf("key%d", i))), common.BytesToHash([]byte(fmt.Sprintf("value%d", i)))) } + suite.Require().NoError(db.Commit()) + db = suite.StateDB() + // Call Suicide - suite.Require().Equal(true, suite.app.EvmKeeper.Suicide(suite.address)) + suite.Require().Equal(true, db.Suicide(suite.address)) // Check suicided is marked - suite.Require().Equal(true, suite.app.EvmKeeper.HasSuicided(suite.address)) + suite.Require().Equal(true, db.HasSuicided(suite.address)) + + suite.Require().NoError(db.Commit()) + db = suite.StateDB() + // Check code is deleted - suite.Require().Nil(suite.app.EvmKeeper.GetCode(suite.address)) + suite.Require().Nil(db.GetCode(suite.address)) // Check state is deleted var storage types.Storage - err := suite.app.EvmKeeper.ForEachStorage(suite.address, func(key, value common.Hash) bool { + suite.app.EvmKeeper.ForEachStorage(suite.ctx, suite.address, func(key, value common.Hash) bool { storage = append(storage, types.NewState(key, value)) return true }) - suite.Require().NoError(err) suite.Require().Equal(0, len(storage)) - // Check CodeHash is emptied - suite.Require().Equal(common.BytesToHash(types.EmptyCodeHash).Bytes(), suite.app.EvmKeeper.GetCodeHash(suite.address).Bytes()) + // Check account is deleted + suite.Require().Equal(common.Hash{}, db.GetCodeHash(suite.address)) } func (suite *KeeperTestSuite) TestExist() { testCases := []struct { name string address common.Address - malleate func() + malleate func(vm.StateDB) exists bool }{ - {"success, account exists", suite.address, func() {}, true}, - {"success, has suicided", suite.address, func() { - suite.app.EvmKeeper.Suicide(suite.address) + {"success, account exists", suite.address, func(vm.StateDB) {}, true}, + {"success, has suicided", suite.address, func(vmdb vm.StateDB) { + vmdb.Suicide(suite.address) }, true}, - {"success, account doesn't exist", tests.GenerateAddress(), func() {}, false}, + {"success, account doesn't exist", tests.GenerateAddress(), func(vm.StateDB) {}, false}, } for _, tc := range testCases { suite.Run(tc.name, func() { - tc.malleate() + vmdb := suite.StateDB() + tc.malleate(vmdb) - suite.Require().Equal(tc.exists, suite.app.EvmKeeper.Exist(tc.address)) + suite.Require().Equal(tc.exists, vmdb.Exist(tc.address)) }) } } func (suite *KeeperTestSuite) TestEmpty() { + suite.SetupTest() addr := tests.GenerateAddress() baseAcc := &authtypes.BaseAccount{Address: sdk.AccAddress(addr.Bytes()).String()} suite.app.AccountKeeper.SetAccount(suite.ctx, baseAcc) + acct, err := suite.app.EvmKeeper.GetAccount(suite.ctx, suite.address) + suite.Require().NoError(err) + fmt.Println("default address", acct) + testCases := []struct { name string address common.Address - malleate func() + malleate func(vm.StateDB) empty bool + expErr bool }{ - {"empty, account exists", suite.address, func() {}, true}, - {"not empty, non ethereum account", addr, func() {}, false}, - {"not empty, positive balance", suite.address, func() { - suite.app.EvmKeeper.AddBalance(suite.address, big.NewInt(100)) - }, false}, - {"empty, account doesn't exist", tests.GenerateAddress(), func() {}, true}, + {"empty, account exists", suite.address, func(vm.StateDB) {}, true, false}, + {"error, non ethereum account", addr, func(vm.StateDB) {}, true, true}, + {"not empty, positive balance", suite.address, func(vmdb vm.StateDB) { + vmdb.AddBalance(suite.address, big.NewInt(100)) + }, false, false}, + {"empty, account doesn't exist", tests.GenerateAddress(), func(vm.StateDB) {}, true, false}, } for _, tc := range testCases { suite.Run(tc.name, func() { - tc.malleate() + vmdb := suite.StateDB() + tc.malleate(vmdb) - suite.Require().Equal(tc.empty, suite.app.EvmKeeper.Empty(tc.address)) + suite.Require().Equal(tc.empty, vmdb.Empty(tc.address)) + if tc.expErr { + suite.Require().Error(vmdb.Error()) + } else { + suite.Require().NoError(vmdb.Error()) + } }) } } @@ -490,53 +514,52 @@ func (suite *KeeperTestSuite) TestSnapshot() { testCases := []struct { name string - malleate func() + malleate func(vm.StateDB) }{ - {"simple revert", func() { - revision := suite.app.EvmKeeper.Snapshot() + {"simple revert", func(vmdb vm.StateDB) { + revision := vmdb.Snapshot() suite.Require().Zero(revision) - suite.app.EvmKeeper.SetState(suite.address, key, value1) - suite.Require().Equal(value1, suite.app.EvmKeeper.GetState(suite.address, key)) + vmdb.SetState(suite.address, key, value1) + suite.Require().Equal(value1, vmdb.GetState(suite.address, key)) - suite.app.EvmKeeper.RevertToSnapshot(revision) + vmdb.RevertToSnapshot(revision) // reverted - suite.Require().Equal(common.Hash{}, suite.app.EvmKeeper.GetState(suite.address, key)) + suite.Require().Equal(common.Hash{}, vmdb.GetState(suite.address, key)) }}, - {"nested snapshot/revert", func() { - revision1 := suite.app.EvmKeeper.Snapshot() + {"nested snapshot/revert", func(vmdb vm.StateDB) { + revision1 := vmdb.Snapshot() suite.Require().Zero(revision1) - suite.app.EvmKeeper.SetState(suite.address, key, value1) + vmdb.SetState(suite.address, key, value1) - revision2 := suite.app.EvmKeeper.Snapshot() + revision2 := vmdb.Snapshot() - suite.app.EvmKeeper.SetState(suite.address, key, value2) - suite.Require().Equal(value2, suite.app.EvmKeeper.GetState(suite.address, key)) + vmdb.SetState(suite.address, key, value2) + suite.Require().Equal(value2, vmdb.GetState(suite.address, key)) - suite.app.EvmKeeper.RevertToSnapshot(revision2) - suite.Require().Equal(value1, suite.app.EvmKeeper.GetState(suite.address, key)) + vmdb.RevertToSnapshot(revision2) + suite.Require().Equal(value1, vmdb.GetState(suite.address, key)) - suite.app.EvmKeeper.RevertToSnapshot(revision1) - suite.Require().Equal(common.Hash{}, suite.app.EvmKeeper.GetState(suite.address, key)) + vmdb.RevertToSnapshot(revision1) + suite.Require().Equal(common.Hash{}, vmdb.GetState(suite.address, key)) }}, - {"jump revert", func() { - revision1 := suite.app.EvmKeeper.Snapshot() - suite.app.EvmKeeper.SetState(suite.address, key, value1) - suite.app.EvmKeeper.Snapshot() - suite.app.EvmKeeper.SetState(suite.address, key, value2) - suite.app.EvmKeeper.RevertToSnapshot(revision1) - suite.Require().Equal(common.Hash{}, suite.app.EvmKeeper.GetState(suite.address, key)) + {"jump revert", func(vmdb vm.StateDB) { + revision1 := vmdb.Snapshot() + vmdb.SetState(suite.address, key, value1) + vmdb.Snapshot() + vmdb.SetState(suite.address, key, value2) + vmdb.RevertToSnapshot(revision1) + suite.Require().Equal(common.Hash{}, vmdb.GetState(suite.address, key)) }}, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() - tc.malleate() - // the test case should finish in clean state - suite.Require().True(suite.app.EvmKeeper.CachedContextsEmpty()) + vmdb := suite.StateDB() + tc.malleate(vmdb) }) } } @@ -574,7 +597,6 @@ func (suite *KeeperTestSuite) TestAddLog() { tx2 := suite.CreateTestTx(msg2, privKey) msg2, _ = tx2.GetMsgs()[0].(*types.MsgEthereumTx) - txHash2 := msg2.AsTransaction().Hash() msg3 := types.NewTx(big.NewInt(1), 0, &suite.address, big.NewInt(1), 100000, nil, big.NewInt(1), big.NewInt(1), []byte("test"), nil) msg3.From = addr.Hex() @@ -588,92 +610,55 @@ func (suite *KeeperTestSuite) TestAddLog() { tx4 := suite.CreateTestTx(msg4, privKey) msg4, _ = tx4.GetMsgs()[0].(*types.MsgEthereumTx) - txHash4 := msg4.AsTransaction().Hash() testCases := []struct { name string hash common.Hash log, expLog *ethtypes.Log // pre and post populating log fields - malleate func() + malleate func(vm.StateDB) }{ { "tx hash from message", txHash, ðtypes.Log{ Address: addr, + Topics: make([]common.Hash, 0), }, ðtypes.Log{ Address: addr, TxHash: txHash, Topics: make([]common.Hash, 0), }, - func() {}, - }, - { - "log index keep increasing in new tx", - txHash2, - ðtypes.Log{ - Address: addr, - }, - ðtypes.Log{ - Address: addr, - TxHash: txHash2, - TxIndex: 1, - Index: 1, - Topics: make([]common.Hash, 0), - }, - func() { - suite.app.EvmKeeper.SetTxHashTransient(txHash) - suite.app.EvmKeeper.AddLog(ðtypes.Log{ - Address: addr, - }) - suite.app.EvmKeeper.IncreaseTxIndexTransient() - }, + func(vm.StateDB) {}, }, { "dynamicfee tx hash from message", txHash3, ðtypes.Log{ Address: addr, + Topics: make([]common.Hash, 0), }, ðtypes.Log{ Address: addr, TxHash: txHash3, Topics: make([]common.Hash, 0), }, - func() {}, - }, - { - "log index keep increasing in new dynamicfee tx", - txHash4, - ðtypes.Log{ - Address: addr, - }, - ðtypes.Log{ - Address: addr, - TxHash: txHash4, - TxIndex: 1, - Index: 1, - Topics: make([]common.Hash, 0), - }, - func() { - suite.app.EvmKeeper.SetTxHashTransient(txHash) - suite.app.EvmKeeper.AddLog(ðtypes.Log{ - Address: addr, - }) - suite.app.EvmKeeper.IncreaseTxIndexTransient() - }, + func(vm.StateDB) {}, }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() - tc.malleate() + vmdb := statedb.New(suite.ctx, suite.app.EvmKeeper, statedb.NewTxConfig( + common.BytesToHash(suite.ctx.HeaderHash().Bytes()), + tc.hash, + 0, 0, + )) + tc.malleate(vmdb) - suite.app.EvmKeeper.SetTxHashTransient(tc.hash) - suite.app.EvmKeeper.AddLog(tc.log) - logs := suite.app.EvmKeeper.GetTxLogsTransient(tc.hash) + vmdb.AddLog(tc.log) + logs := vmdb.Logs() suite.Require().Equal(1, len(logs)) suite.Require().Equal(tc.expLog, logs[0]) }) @@ -688,18 +673,19 @@ func (suite *KeeperTestSuite) TestPrepareAccessList() { {Address: tests.GenerateAddress(), StorageKeys: []common.Hash{common.BytesToHash([]byte("key1"))}}, } - suite.app.EvmKeeper.PrepareAccessList(suite.address, &dest, precompiles, accesses) + vmdb := suite.StateDB() + vmdb.PrepareAccessList(suite.address, &dest, precompiles, accesses) - suite.Require().True(suite.app.EvmKeeper.AddressInAccessList(suite.address)) - suite.Require().True(suite.app.EvmKeeper.AddressInAccessList(dest)) + suite.Require().True(vmdb.AddressInAccessList(suite.address)) + suite.Require().True(vmdb.AddressInAccessList(dest)) for _, precompile := range precompiles { - suite.Require().True(suite.app.EvmKeeper.AddressInAccessList(precompile)) + suite.Require().True(vmdb.AddressInAccessList(precompile)) } for _, access := range accesses { for _, key := range access.StorageKeys { - addrOK, slotOK := suite.app.EvmKeeper.SlotInAccessList(access.Address, key) + addrOK, slotOK := vmdb.SlotInAccessList(access.Address, key) suite.Require().True(addrOK, access.Address.Hex()) suite.Require().True(slotOK, key.Hex()) } @@ -717,8 +703,9 @@ func (suite *KeeperTestSuite) TestAddAddressToAccessList() { for _, tc := range testCases { suite.Run(tc.name, func() { - suite.app.EvmKeeper.AddAddressToAccessList(tc.addr) - addrOk := suite.app.EvmKeeper.AddressInAccessList(tc.addr) + vmdb := suite.StateDB() + vmdb.AddAddressToAccessList(tc.addr) + addrOk := vmdb.AddressInAccessList(tc.addr) suite.Require().True(addrOk, tc.addr.Hex()) }) } @@ -738,28 +725,30 @@ func (suite *KeeperTestSuite) AddSlotToAccessList() { for _, tc := range testCases { suite.Run(tc.name, func() { - suite.app.EvmKeeper.AddSlotToAccessList(tc.addr, tc.slot) - addrOk, slotOk := suite.app.EvmKeeper.SlotInAccessList(tc.addr, tc.slot) + vmdb := suite.StateDB() + vmdb.AddSlotToAccessList(tc.addr, tc.slot) + addrOk, slotOk := vmdb.SlotInAccessList(tc.addr, tc.slot) suite.Require().True(addrOk, tc.addr.Hex()) suite.Require().True(slotOk, tc.slot.Hex()) }) } } -func (suite *KeeperTestSuite) TestForEachStorage() { +// FIXME skip for now +func (suite *KeeperTestSuite) _TestForEachStorage() { var storage types.Storage testCase := []struct { name string - malleate func() + malleate func(vm.StateDB) callback func(key, value common.Hash) (stop bool) expValues []common.Hash }{ { "aggregate state", - func() { + func(vmdb vm.StateDB) { for i := 0; i < 5; i++ { - suite.app.EvmKeeper.SetState(suite.address, common.BytesToHash([]byte(fmt.Sprintf("key%d", i))), common.BytesToHash([]byte(fmt.Sprintf("value%d", i)))) + vmdb.SetState(suite.address, common.BytesToHash([]byte(fmt.Sprintf("key%d", i))), common.BytesToHash([]byte(fmt.Sprintf("value%d", i)))) } }, func(key, value common.Hash) bool { @@ -776,9 +765,9 @@ func (suite *KeeperTestSuite) TestForEachStorage() { }, { "filter state", - func() { - suite.app.EvmKeeper.SetState(suite.address, common.BytesToHash([]byte("key")), common.BytesToHash([]byte("value"))) - suite.app.EvmKeeper.SetState(suite.address, common.BytesToHash([]byte("filterkey")), common.BytesToHash([]byte("filtervalue"))) + func(vmdb vm.StateDB) { + vmdb.SetState(suite.address, common.BytesToHash([]byte("key")), common.BytesToHash([]byte("value"))) + vmdb.SetState(suite.address, common.BytesToHash([]byte("filterkey")), common.BytesToHash([]byte("filtervalue"))) }, func(key, value common.Hash) bool { if value == common.BytesToHash([]byte("filtervalue")) { @@ -796,9 +785,10 @@ func (suite *KeeperTestSuite) TestForEachStorage() { for _, tc := range testCase { suite.Run(tc.name, func() { suite.SetupTest() // reset - tc.malleate() + vmdb := suite.StateDB() + tc.malleate(vmdb) - err := suite.app.EvmKeeper.ForEachStorage(suite.address, tc.callback) + err := vmdb.ForEachStorage(suite.address, tc.callback) suite.Require().NoError(err) suite.Require().Equal(len(tc.expValues), len(storage), fmt.Sprintf("Expected values:\n%v\nStorage Values\n%v", tc.expValues, storage)) diff --git a/x/evm/keeper/utils.go b/x/evm/keeper/utils.go index 75303632..3846e03a 100644 --- a/x/evm/keeper/utils.go +++ b/x/evm/keeper/utils.go @@ -82,26 +82,22 @@ func (k Keeper) DeductTxCostsFromUserBalance( // CheckSenderBalance validates that the tx cost value is positive and that the // sender has enough funds to pay for the fees and value of the transaction. func CheckSenderBalance( - ctx sdk.Context, - bankKeeper evmtypes.BankKeeper, - sender sdk.AccAddress, + balance sdk.Int, txData evmtypes.TxData, - denom string, ) error { - balance := bankKeeper.GetBalance(ctx, sender, denom) cost := txData.Cost() if cost.Sign() < 0 { return sdkerrors.Wrapf( sdkerrors.ErrInvalidCoins, - "tx cost (%s%s) is negative and invalid", cost, denom, + "tx cost (%s) is negative and invalid", cost, ) } - if balance.IsNegative() || balance.Amount.BigInt().Cmp(cost) < 0 { + if balance.IsNegative() || balance.BigInt().Cmp(cost) < 0 { return sdkerrors.Wrapf( sdkerrors.ErrInsufficientFunds, - "sender balance < tx cost (%s < %s%s)", balance, txData.Cost(), denom, + "sender balance < tx cost (%s < %s)", balance, txData.Cost(), ) } return nil diff --git a/x/evm/keeper/utils_test.go b/x/evm/keeper/utils_test.go index 0f7a2e35..e2c4f5b3 100644 --- a/x/evm/keeper/utils_test.go +++ b/x/evm/keeper/utils_test.go @@ -202,9 +202,11 @@ func (suite *KeeperTestSuite) TestCheckSenderBalance() { }, } - suite.app.EvmKeeper.AddBalance(suite.address, hundredInt.BigInt()) - balance := suite.app.EvmKeeper.GetBalance(suite.address) + vmdb := suite.StateDB() + vmdb.AddBalance(suite.address, hundredInt.BigInt()) + balance := vmdb.GetBalance(suite.address) suite.Require().Equal(balance, hundredInt.BigInt()) + vmdb.Commit() for i, tc := range testCases { suite.Run(tc.name, func() { @@ -233,12 +235,11 @@ func (suite *KeeperTestSuite) TestCheckSenderBalance() { txData, _ := evmtypes.UnpackTxData(tx.Data) - err := evmkeeper.CheckSenderBalance( - suite.app.EvmKeeper.Ctx(), - suite.app.BankKeeper, - suite.address[:], + acct, err := suite.app.EvmKeeper.GetAccountOrEmpty(suite.ctx, suite.address) + suite.Require().NoError(err) + err = evmkeeper.CheckSenderBalance( + sdk.NewIntFromBigInt(acct.Balance), txData, - evmtypes.DefaultEVMDenom, ) if tc.expectPass { @@ -367,6 +368,7 @@ func (suite *KeeperTestSuite) TestDeductTxCostsFromUserBalance() { suite.Run(tc.name, func() { suite.enableFeemarket = tc.enableFeemarket suite.SetupTest() + vmdb := suite.StateDB() var amount, gasPrice, gasFeeCap, gasTipCap *big.Int if tc.cost != nil { @@ -382,18 +384,19 @@ func (suite *KeeperTestSuite) TestDeductTxCostsFromUserBalance() { } else { gasTipCap = tc.gasTipCap } - suite.app.EvmKeeper.AddBalance(suite.address, initBalance.BigInt()) - balance := suite.app.EvmKeeper.GetBalance(suite.address) + vmdb.AddBalance(suite.address, initBalance.BigInt()) + balance := vmdb.GetBalance(suite.address) suite.Require().Equal(balance, initBalance.BigInt()) } else { if tc.gasPrice != nil { gasPrice = tc.gasPrice.BigInt() } - suite.app.EvmKeeper.AddBalance(suite.address, hundredInt.BigInt()) - balance := suite.app.EvmKeeper.GetBalance(suite.address) + vmdb.AddBalance(suite.address, hundredInt.BigInt()) + balance := vmdb.GetBalance(suite.address) suite.Require().Equal(balance, hundredInt.BigInt()) } + vmdb.Commit() tx := evmtypes.NewTx(zeroInt.BigInt(), 1, &suite.address, amount, tc.gasLimit, gasPrice, gasFeeCap, gasTipCap, nil, tc.accessList) tx.From = suite.address.String() @@ -401,7 +404,7 @@ func (suite *KeeperTestSuite) TestDeductTxCostsFromUserBalance() { txData, _ := evmtypes.UnpackTxData(tx.Data) fees, err := suite.app.EvmKeeper.DeductTxCostsFromUserBalance( - suite.app.EvmKeeper.Ctx(), + suite.ctx, *tx, txData, evmtypes.DefaultEVMDenom, diff --git a/x/evm/statedb/access_list.go b/x/evm/statedb/access_list.go new file mode 100644 index 00000000..99342819 --- /dev/null +++ b/x/evm/statedb/access_list.go @@ -0,0 +1,135 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package statedb + +import ( + "github.com/ethereum/go-ethereum/common" +) + +type accessList struct { + addresses map[common.Address]int + slots []map[common.Hash]struct{} +} + +// ContainsAddress returns true if the address is in the access list. +func (al *accessList) ContainsAddress(address common.Address) bool { + _, ok := al.addresses[address] + return ok +} + +// Contains checks if a slot within an account is present in the access list, returning +// separate flags for the presence of the account and the slot respectively. +func (al *accessList) Contains(address common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { + idx, ok := al.addresses[address] + if !ok { + // no such address (and hence zero slots) + return false, false + } + if idx == -1 { + // address yes, but no slots + return true, false + } + _, slotPresent = al.slots[idx][slot] + return true, slotPresent +} + +// newAccessList creates a new accessList. +func newAccessList() *accessList { + return &accessList{ + addresses: make(map[common.Address]int), + } +} + +// Copy creates an independent copy of an accessList. +func (al *accessList) Copy() *accessList { + cp := newAccessList() + for k, v := range al.addresses { + cp.addresses[k] = v + } + cp.slots = make([]map[common.Hash]struct{}, len(al.slots)) + for i, slotMap := range al.slots { + newSlotmap := make(map[common.Hash]struct{}, len(slotMap)) + for k := range slotMap { + newSlotmap[k] = struct{}{} + } + cp.slots[i] = newSlotmap + } + return cp +} + +// AddAddress adds an address to the access list, and returns 'true' if the operation +// caused a change (addr was not previously in the list). +func (al *accessList) AddAddress(address common.Address) bool { + if _, present := al.addresses[address]; present { + return false + } + al.addresses[address] = -1 + return true +} + +// AddSlot adds the specified (addr, slot) combo to the access list. +// Return values are: +// - address added +// - slot added +// For any 'true' value returned, a corresponding journal entry must be made. +func (al *accessList) AddSlot(address common.Address, slot common.Hash) (addrChange bool, slotChange bool) { + idx, addrPresent := al.addresses[address] + if !addrPresent || idx == -1 { + // Address not present, or addr present but no slots there + al.addresses[address] = len(al.slots) + slotmap := map[common.Hash]struct{}{slot: {}} + al.slots = append(al.slots, slotmap) + return !addrPresent, true + } + // There is already an (address,slot) mapping + slotmap := al.slots[idx] + if _, ok := slotmap[slot]; !ok { + slotmap[slot] = struct{}{} + // Journal add slot change + return false, true + } + // No changes required + return false, false +} + +// DeleteSlot removes an (address, slot)-tuple from the access list. +// This operation needs to be performed in the same order as the addition happened. +// This method is meant to be used by the journal, which maintains ordering of +// operations. +func (al *accessList) DeleteSlot(address common.Address, slot common.Hash) { + idx, addrOk := al.addresses[address] + if !addrOk { + panic("reverting slot change, address not present in list") + } + slotmap := al.slots[idx] + delete(slotmap, slot) + // If that was the last (first) slot, remove it + // Since additions and rollbacks are always performed in order, + // we can delete the item without worrying about screwing up later indices + if len(slotmap) == 0 { + al.slots = al.slots[:idx] + al.addresses[address] = -1 + } +} + +// DeleteAddress removes an address from the access list. This operation +// needs to be performed in the same order as the addition happened. +// This method is meant to be used by the journal, which maintains ordering of +// operations. +func (al *accessList) DeleteAddress(address common.Address) { + delete(al.addresses, address) +} diff --git a/x/evm/statedb/config.go b/x/evm/statedb/config.go new file mode 100644 index 00000000..9932b7b5 --- /dev/null +++ b/x/evm/statedb/config.go @@ -0,0 +1,32 @@ +package statedb + +import "github.com/ethereum/go-ethereum/common" + +// TxConfig encapulates the readonly information of current tx for `StateDB`. +type TxConfig struct { + BlockHash common.Hash // hash of current block + TxHash common.Hash // hash of current tx + TxIndex uint // the index of current transaction + LogIndex uint // the index of next log within current block +} + +// NewTxConfig returns a TxConfig +func NewTxConfig(bhash, thash common.Hash, txIndex, logIndex uint) TxConfig { + return TxConfig{ + BlockHash: bhash, + TxHash: thash, + TxIndex: txIndex, + LogIndex: logIndex, + } +} + +// NewEmptyTxConfig construct an empty TxConfig, +// used in context where there's no transaction, e.g. `eth_call`/`eth_estimateGas`. +func NewEmptyTxConfig(bhash common.Hash) TxConfig { + return TxConfig{ + BlockHash: bhash, + TxHash: common.Hash{}, + TxIndex: 0, + LogIndex: 0, + } +} diff --git a/x/evm/statedb/interfaces.go b/x/evm/statedb/interfaces.go new file mode 100644 index 00000000..5775ecdb --- /dev/null +++ b/x/evm/statedb/interfaces.go @@ -0,0 +1,22 @@ +package statedb + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" +) + +// Keeper provide underlying storage of StateDB +type Keeper interface { + // Read methods + GetAccount(ctx sdk.Context, addr common.Address) (*Account, error) + GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash + GetCode(ctx sdk.Context, codeHash common.Hash) []byte + // the callback returns false to break early + ForEachStorage(ctx sdk.Context, addr common.Address, cb func(key, value common.Hash) bool) + + // Write methods, only called by `StateDB.Commit()` + SetAccount(ctx sdk.Context, addr common.Address, account Account) error + SetState(ctx sdk.Context, addr common.Address, key common.Hash, value []byte) + SetCode(ctx sdk.Context, codeHash []byte, code []byte) + DeleteAccount(ctx sdk.Context, addr common.Address) error +} diff --git a/x/evm/statedb/journal.go b/x/evm/statedb/journal.go new file mode 100644 index 00000000..c3f692f9 --- /dev/null +++ b/x/evm/statedb/journal.go @@ -0,0 +1,243 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package statedb + +import ( + "bytes" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" +) + +// journalEntry is a modification entry in the state change journal that can be +// reverted on demand. +type journalEntry interface { + // revert undoes the changes introduced by this journal entry. + revert(*StateDB) + + // dirtied returns the Ethereum address modified by this journal entry. + dirtied() *common.Address +} + +// journal contains the list of state modifications applied since the last state +// commit. These are tracked to be able to be reverted in the case of an execution +// exception or request for reversal. +type journal struct { + entries []journalEntry // Current changes tracked by the journal + dirties map[common.Address]int // Dirty accounts and the number of changes +} + +// newJournal creates a new initialized journal. +func newJournal() *journal { + return &journal{ + dirties: make(map[common.Address]int), + } +} + +// sortedDirties sort the dirty addresses for deterministic iteration +func (j *journal) sortedDirties() []common.Address { + keys := make([]common.Address, len(j.dirties)) + i := 0 + for k := range j.dirties { + keys[i] = k + i++ + } + sort.Slice(keys, func(i, j int) bool { + return bytes.Compare(keys[i].Bytes(), keys[j].Bytes()) < 0 + }) + return keys +} + +// append inserts a new modification entry to the end of the change journal. +func (j *journal) append(entry journalEntry) { + j.entries = append(j.entries, entry) + if addr := entry.dirtied(); addr != nil { + j.dirties[*addr]++ + } +} + +// revert undoes a batch of journalled modifications along with any reverted +// dirty handling too. +func (j *journal) revert(statedb *StateDB, snapshot int) { + for i := len(j.entries) - 1; i >= snapshot; i-- { + // Undo the changes made by the operation + j.entries[i].revert(statedb) + + // Drop any dirty tracking induced by the change + if addr := j.entries[i].dirtied(); addr != nil { + if j.dirties[*addr]--; j.dirties[*addr] == 0 { + delete(j.dirties, *addr) + } + } + } + j.entries = j.entries[:snapshot] +} + +// length returns the current number of entries in the journal. +func (j *journal) length() int { + return len(j.entries) +} + +type ( + // Changes to the account trie. + createObjectChange struct { + account *common.Address + } + resetObjectChange struct { + prev *stateObject + } + suicideChange struct { + account *common.Address + prev bool // whether account had already suicided + prevbalance *big.Int + } + + // Changes to individual accounts. + balanceChange struct { + account *common.Address + prev *big.Int + } + nonceChange struct { + account *common.Address + prev uint64 + } + storageChange struct { + account *common.Address + key, prevalue common.Hash + } + codeChange struct { + account *common.Address + prevcode, prevhash []byte + } + + // Changes to other state values. + refundChange struct { + prev uint64 + } + addLogChange struct{} + + // Changes to the access list + accessListAddAccountChange struct { + address *common.Address + } + accessListAddSlotChange struct { + address *common.Address + slot *common.Hash + } +) + +func (ch createObjectChange) revert(s *StateDB) { + delete(s.stateObjects, *ch.account) +} + +func (ch createObjectChange) dirtied() *common.Address { + return ch.account +} + +func (ch resetObjectChange) revert(s *StateDB) { + s.setStateObject(ch.prev) +} + +func (ch resetObjectChange) dirtied() *common.Address { + return nil +} + +func (ch suicideChange) revert(s *StateDB) { + obj := s.getStateObject(*ch.account) + if obj != nil { + obj.suicided = ch.prev + obj.setBalance(ch.prevbalance) + } +} + +func (ch suicideChange) dirtied() *common.Address { + return ch.account +} + +func (ch balanceChange) revert(s *StateDB) { + s.getStateObject(*ch.account).setBalance(ch.prev) +} + +func (ch balanceChange) dirtied() *common.Address { + return ch.account +} + +func (ch nonceChange) revert(s *StateDB) { + s.getStateObject(*ch.account).setNonce(ch.prev) +} + +func (ch nonceChange) dirtied() *common.Address { + return ch.account +} + +func (ch codeChange) revert(s *StateDB) { + s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) +} + +func (ch codeChange) dirtied() *common.Address { + return ch.account +} + +func (ch storageChange) revert(s *StateDB) { + s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) +} + +func (ch storageChange) dirtied() *common.Address { + return ch.account +} + +func (ch refundChange) revert(s *StateDB) { + s.refund = ch.prev +} + +func (ch refundChange) dirtied() *common.Address { + return nil +} + +func (ch addLogChange) revert(s *StateDB) { + s.logs = s.logs[:len(s.logs)-1] +} + +func (ch addLogChange) dirtied() *common.Address { + return nil +} + +func (ch accessListAddAccountChange) revert(s *StateDB) { + /* + One important invariant here, is that whenever a (addr, slot) is added, if the + addr is not already present, the add causes two journal entries: + - one for the address, + - one for the (address,slot) + Therefore, when unrolling the change, we can always blindly delete the + (addr) at this point, since no storage adds can remain when come upon + a single (addr) change. + */ + s.accessList.DeleteAddress(*ch.address) +} + +func (ch accessListAddAccountChange) dirtied() *common.Address { + return nil +} + +func (ch accessListAddSlotChange) revert(s *StateDB) { + s.accessList.DeleteSlot(*ch.address, *ch.slot) +} + +func (ch accessListAddSlotChange) dirtied() *common.Address { + return nil +} diff --git a/x/evm/statedb/mock_test.go b/x/evm/statedb/mock_test.go new file mode 100644 index 00000000..263653a4 --- /dev/null +++ b/x/evm/statedb/mock_test.go @@ -0,0 +1,84 @@ +package statedb_test + +import ( + "errors" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/tharsis/ethermint/x/evm/statedb" +) + +var _ statedb.Keeper = &MockKeeper{} + +type MockKeeper struct { + errAddress common.Address + + accounts map[common.Address]statedb.Account + states map[common.Address]statedb.Storage + codes map[common.Hash][]byte +} + +func NewMockKeeper() *MockKeeper { + return &MockKeeper{ + errAddress: common.BigToAddress(big.NewInt(1)), + + accounts: make(map[common.Address]statedb.Account), + states: make(map[common.Address]statedb.Storage), + codes: make(map[common.Hash][]byte), + } +} + +func (k MockKeeper) GetAccount(ctx sdk.Context, addr common.Address) (*statedb.Account, error) { + if addr == k.errAddress { + return nil, errors.New("mock db error") + } + acct, ok := k.accounts[addr] + if !ok { + return nil, nil + } + return &acct, nil +} + +func (k MockKeeper) GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash { + return k.states[addr][key] +} + +func (k MockKeeper) GetCode(ctx sdk.Context, codeHash common.Hash) []byte { + return k.codes[codeHash] +} + +func (k MockKeeper) ForEachStorage(ctx sdk.Context, addr common.Address, cb func(key, value common.Hash) bool) { + for k, v := range k.states[addr] { + if !cb(k, v) { + return + } + } +} + +func (k MockKeeper) SetAccount(ctx sdk.Context, addr common.Address, account statedb.Account) error { + k.accounts[addr] = account + return nil +} + +func (k MockKeeper) SetState(ctx sdk.Context, addr common.Address, key common.Hash, value []byte) { + if len(value) == 0 { + delete(k.states[addr], key) + } else { + k.states[addr][key] = common.BytesToHash(value) + } +} + +func (k MockKeeper) SetCode(ctx sdk.Context, codeHash []byte, code []byte) { + k.codes[common.BytesToHash(codeHash)] = code +} + +func (k MockKeeper) DeleteAccount(ctx sdk.Context, addr common.Address) error { + old := k.accounts[addr] + delete(k.accounts, addr) + delete(k.states, addr) + if len(old.CodeHash) > 0 { + delete(k.codes, common.BytesToHash(old.CodeHash)) + } + return nil +} diff --git a/x/evm/statedb/state_object.go b/x/evm/statedb/state_object.go new file mode 100644 index 00000000..f107e5a3 --- /dev/null +++ b/x/evm/statedb/state_object.go @@ -0,0 +1,240 @@ +package statedb + +import ( + "bytes" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +var emptyCodeHash = crypto.Keccak256(nil) + +// Account is the Ethereum consensus representation of accounts. +// These objects are stored in the storage of auth module. +type Account struct { + Nonce uint64 + Balance *big.Int + CodeHash []byte +} + +// NewEmptyAccount returns an empty account. +func NewEmptyAccount() *Account { + return &Account{ + Balance: new(big.Int), + CodeHash: emptyCodeHash, + } +} + +// IsContract returns if the account contains contract code. +func (acct Account) IsContract() bool { + return !bytes.Equal(acct.CodeHash, emptyCodeHash) +} + +// Storage represents in-memory cache/buffer of contract storage. +type Storage map[common.Hash]common.Hash + +// SortedKeys sort the keys for deterministic iteration +func (s Storage) SortedKeys() []common.Hash { + keys := make([]common.Hash, len(s)) + i := 0 + for k := range s { + keys[i] = k + i++ + } + sort.Slice(keys, func(i, j int) bool { + return bytes.Compare(keys[i].Bytes(), keys[j].Bytes()) < 0 + }) + return keys +} + +// stateObject is the state of an acount +type stateObject struct { + db *StateDB + + account Account + code []byte + + // state storage + originStorage Storage + dirtyStorage Storage + + address common.Address + + // flags + dirtyCode bool + suicided bool +} + +// newObject creates a state object. +func newObject(db *StateDB, address common.Address, account Account) *stateObject { + if account.Balance == nil { + account.Balance = new(big.Int) + } + if account.CodeHash == nil { + account.CodeHash = emptyCodeHash + } + return &stateObject{ + db: db, + address: address, + account: account, + originStorage: make(Storage), + dirtyStorage: make(Storage), + } +} + +// empty returns whether the account is considered empty. +func (s *stateObject) empty() bool { + return s.account.Nonce == 0 && s.account.Balance.Sign() == 0 && bytes.Equal(s.account.CodeHash, emptyCodeHash) +} + +func (s *stateObject) markSuicided() { + s.suicided = true +} + +// AddBalance adds amount to s's balance. +// It is used to add funds to the destination account of a transfer. +func (s *stateObject) AddBalance(amount *big.Int) { + if amount.Sign() == 0 { + return + } + s.SetBalance(new(big.Int).Add(s.Balance(), amount)) +} + +// SubBalance removes amount from s's balance. +// It is used to remove funds from the origin account of a transfer. +func (s *stateObject) SubBalance(amount *big.Int) { + if amount.Sign() == 0 { + return + } + s.SetBalance(new(big.Int).Sub(s.Balance(), amount)) +} + +// SetBalance update account balance. +func (s *stateObject) SetBalance(amount *big.Int) { + s.db.journal.append(balanceChange{ + account: &s.address, + prev: new(big.Int).Set(s.account.Balance), + }) + s.setBalance(amount) +} + +func (s *stateObject) setBalance(amount *big.Int) { + s.account.Balance = amount +} + +// Return the gas back to the origin. Used by the Virtual machine or Closures +func (s *stateObject) ReturnGas(gas *big.Int) {} + +// +// Attribute accessors +// + +// Returns the address of the contract/account +func (s *stateObject) Address() common.Address { + return s.address +} + +// Code returns the contract code associated with this object, if any. +func (s *stateObject) Code() []byte { + if s.code != nil { + return s.code + } + if bytes.Equal(s.CodeHash(), emptyCodeHash) { + return nil + } + code := s.db.keeper.GetCode(s.db.ctx, common.BytesToHash(s.CodeHash())) + s.code = code + return code +} + +// CodeSize returns the size of the contract code associated with this object, +// or zero if none. +func (s *stateObject) CodeSize() int { + return len(s.Code()) +} + +// SetCode set contract code to account +func (s *stateObject) SetCode(codeHash common.Hash, code []byte) { + prevcode := s.Code() + s.db.journal.append(codeChange{ + account: &s.address, + prevhash: s.CodeHash(), + prevcode: prevcode, + }) + s.setCode(codeHash, code) +} + +func (s *stateObject) setCode(codeHash common.Hash, code []byte) { + s.code = code + s.account.CodeHash = codeHash[:] + s.dirtyCode = true +} + +// SetCode set nonce to account +func (s *stateObject) SetNonce(nonce uint64) { + s.db.journal.append(nonceChange{ + account: &s.address, + prev: s.account.Nonce, + }) + s.setNonce(nonce) +} + +func (s *stateObject) setNonce(nonce uint64) { + s.account.Nonce = nonce +} + +// CodeHash returns the code hash of account +func (s *stateObject) CodeHash() []byte { + return s.account.CodeHash +} + +// Balance returns the balance of account +func (s *stateObject) Balance() *big.Int { + return s.account.Balance +} + +// Nonce returns the nonce of account +func (s *stateObject) Nonce() uint64 { + return s.account.Nonce +} + +// GetCommittedState query the committed state +func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { + if value, cached := s.originStorage[key]; cached { + return value + } + // If no live objects are available, load it from keeper + value := s.db.keeper.GetState(s.db.ctx, s.Address(), key) + s.originStorage[key] = value + return value +} + +// GetState query the current state (including dirty state) +func (s *stateObject) GetState(key common.Hash) common.Hash { + if value, dirty := s.dirtyStorage[key]; dirty { + return value + } + return s.GetCommittedState(key) +} + +// SetState sets the contract state +func (s *stateObject) SetState(key common.Hash, value common.Hash) { + // If the new value is the same as old, don't set + prev := s.GetState(key) + if prev == value { + return + } + // New value is different, update and journal the change + s.db.journal.append(storageChange{ + account: &s.address, + key: key, + prevalue: prev, + }) + s.setState(key, value) +} + +func (s *stateObject) setState(key, value common.Hash) { + s.dirtyStorage[key] = value +} diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go new file mode 100644 index 00000000..1ec7b0a4 --- /dev/null +++ b/x/evm/statedb/statedb.go @@ -0,0 +1,498 @@ +package statedb + +import ( + "fmt" + "math/big" + "sort" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" +) + +// revision is the identifier of a version of state. +// it consists of an auto-increment id and a journal index. +// it's safer to use than using journal index alone. +type revision struct { + id int + journalIndex int +} + +var _ vm.StateDB = &StateDB{} + +// StateDB structs within the ethereum protocol are used to store anything +// within the merkle trie. StateDBs take care of caching and storing +// nested states. It's the general query interface to retrieve: +// * Contracts +// * Accounts +type StateDB struct { + keeper Keeper + ctx sdk.Context + dbErr error + + // Journal of state modifications. This is the backbone of + // Snapshot and RevertToSnapshot. + journal *journal + validRevisions []revision + nextRevisionID int + + stateObjects map[common.Address]*stateObject + + txConfig TxConfig + + // The refund counter, also used by state transitioning. + refund uint64 + + // Per-transaction logs + logs []*ethtypes.Log + + // Per-transaction access list + accessList *accessList +} + +// New creates a new state from a given trie. +func New(ctx sdk.Context, keeper Keeper, txConfig TxConfig) *StateDB { + return &StateDB{ + keeper: keeper, + ctx: ctx, + stateObjects: make(map[common.Address]*stateObject), + journal: newJournal(), + accessList: newAccessList(), + + txConfig: txConfig, + } +} + +// Keeper returns the underlying `Keeper` +func (s *StateDB) Keeper() Keeper { + return s.keeper +} + +// Context returns the embedded `sdk.Context` +func (s *StateDB) Context() sdk.Context { + return s.ctx +} + +// setError remembers the first non-nil error it is called with. +func (s *StateDB) setError(err error) { + if s.dbErr == nil { + s.dbErr = err + } +} + +// Error returns the database error recorded. +func (s *StateDB) Error() error { + return s.dbErr +} + +// AddLog adds a log, called by evm. +func (s *StateDB) AddLog(log *ethtypes.Log) { + s.journal.append(addLogChange{}) + + log.TxHash = s.txConfig.TxHash + log.BlockHash = s.txConfig.BlockHash + log.TxIndex = s.txConfig.TxIndex + log.Index = s.txConfig.LogIndex + uint(len(s.logs)) + s.logs = append(s.logs, log) +} + +// Logs returns the logs of current transaction. +func (s *StateDB) Logs() []*ethtypes.Log { + return s.logs +} + +// AddRefund adds gas to the refund counter +func (s *StateDB) AddRefund(gas uint64) { + s.journal.append(refundChange{prev: s.refund}) + s.refund += gas +} + +// SubRefund removes gas from the refund counter. +// This method will panic if the refund counter goes below zero +func (s *StateDB) SubRefund(gas uint64) { + s.journal.append(refundChange{prev: s.refund}) + if gas > s.refund { + panic(fmt.Sprintf("Refund counter below zero (gas: %d > refund: %d)", gas, s.refund)) + } + s.refund -= gas +} + +// Exist reports whether the given account address exists in the state. +// Notably this also returns true for suicided accounts. +func (s *StateDB) Exist(addr common.Address) bool { + return s.getStateObject(addr) != nil +} + +// Empty returns whether the state object is either non-existent +// or empty according to the EIP161 specification (balance = nonce = code = 0) +func (s *StateDB) Empty(addr common.Address) bool { + so := s.getStateObject(addr) + return so == nil || so.empty() +} + +// GetBalance retrieves the balance from the given address or 0 if object not found +func (s *StateDB) GetBalance(addr common.Address) *big.Int { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Balance() + } + return common.Big0 +} + +// GetNonce returns the nonce of account, 0 if not exists. +func (s *StateDB) GetNonce(addr common.Address) uint64 { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Nonce() + } + + return 0 +} + +// TxIndex returns the current transaction index. +func (s *StateDB) TxIndex() uint { + return s.txConfig.TxIndex +} + +// BlockHash returns the current block hash. +func (s *StateDB) BlockHash() common.Hash { + return s.txConfig.BlockHash +} + +// GetCode returns the code of account, nil if not exists. +func (s *StateDB) GetCode(addr common.Address) []byte { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Code() + } + return nil +} + +// GetCodeSize returns the code size of account. +func (s *StateDB) GetCodeSize(addr common.Address) int { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.CodeSize() + } + return 0 +} + +// GetCodeHash returns the code hash of account. +func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject == nil { + return common.Hash{} + } + return common.BytesToHash(stateObject.CodeHash()) +} + +// GetState retrieves a value from the given account's storage trie. +func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.GetState(hash) + } + return common.Hash{} +} + +// GetCommittedState retrieves a value from the given account's committed storage trie. +func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.GetCommittedState(hash) + } + return common.Hash{} +} + +// GetRefund returns the current value of the refund counter. +func (s *StateDB) GetRefund() uint64 { + return s.refund +} + +// HasSuicided returns if the contract is suicided in current transaction. +func (s *StateDB) HasSuicided(addr common.Address) bool { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.suicided + } + return false +} + +// AddPreimage records a SHA3 preimage seen by the VM. +// AddPreimage performs a no-op since the EnablePreimageRecording flag is disabled +// on the vm.Config during state transitions. No store trie preimages are written +// to the database. +func (s *StateDB) AddPreimage(hash common.Hash, preimage []byte) {} + +// getStateObject retrieves a state object given by the address, returning nil if +// the object is not found. +func (s *StateDB) getStateObject(addr common.Address) *stateObject { + // Prefer live objects if any is available + if obj := s.stateObjects[addr]; obj != nil { + return obj + } + // If no live objects are available, load it from keeper + account, err := s.keeper.GetAccount(s.ctx, addr) + if err != nil { + s.setError(err) + return nil + } + if account == nil { + return nil + } + // Insert into the live set + obj := newObject(s, addr, *account) + s.setStateObject(obj) + return obj +} + +// getOrNewStateObject retrieves a state object or create a new state object if nil. +func (s *StateDB) getOrNewStateObject(addr common.Address) *stateObject { + stateObject := s.getStateObject(addr) + if stateObject == nil { + stateObject, _ = s.createObject(addr) + } + return stateObject +} + +// createObject creates a new state object. If there is an existing account with +// the given address, it is overwritten and returned as the second return value. +func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) { + prev = s.getStateObject(addr) + + newobj = newObject(s, addr, Account{}) + if prev == nil { + s.journal.append(createObjectChange{account: &addr}) + } else { + s.journal.append(resetObjectChange{prev: prev}) + } + s.setStateObject(newobj) + if prev != nil { + return newobj, prev + } + return newobj, nil +} + +// CreateAccount explicitly creates a state object. If a state object with the address +// already exists the balance is carried over to the new account. +// +// CreateAccount is called during the EVM CREATE operation. The situation might arise that +// a contract does the following: +// +// 1. sends funds to sha(account ++ (nonce + 1)) +// 2. tx_create(sha(account ++ nonce)) (note that this gets the address of 1) +// +// Carrying over the balance ensures that Ether doesn't disappear. +func (s *StateDB) CreateAccount(addr common.Address) { + newObj, prev := s.createObject(addr) + if prev != nil { + newObj.setBalance(prev.account.Balance) + } +} + +// ForEachStorage iterate the contract storage, the iteration order is not defined. +func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error { + so := s.getStateObject(addr) + if so == nil { + return nil + } + s.keeper.ForEachStorage(s.ctx, addr, func(key, value common.Hash) bool { + if value, dirty := so.dirtyStorage[key]; dirty { + return cb(key, value) + } + if len(value) > 0 { + return cb(key, value) + } + return true + }) + return nil +} + +func (s *StateDB) setStateObject(object *stateObject) { + s.stateObjects[object.Address()] = object +} + +/* + * SETTERS + */ + +// AddBalance adds amount to the account associated with addr. +func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { + stateObject := s.getOrNewStateObject(addr) + if stateObject != nil { + stateObject.AddBalance(amount) + } +} + +// SubBalance subtracts amount from the account associated with addr. +func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { + stateObject := s.getOrNewStateObject(addr) + if stateObject != nil { + stateObject.SubBalance(amount) + } +} + +// SetNonce sets the nonce of account. +func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { + stateObject := s.getOrNewStateObject(addr) + if stateObject != nil { + stateObject.SetNonce(nonce) + } +} + +// SetCode sets the code of account. +func (s *StateDB) SetCode(addr common.Address, code []byte) { + stateObject := s.getOrNewStateObject(addr) + if stateObject != nil { + stateObject.SetCode(crypto.Keccak256Hash(code), code) + } +} + +// SetState sets the contract state. +func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { + stateObject := s.getOrNewStateObject(addr) + if stateObject != nil { + stateObject.SetState(key, value) + } +} + +// Suicide marks the given account as suicided. +// This clears the account balance. +// +// The account's state object is still available until the state is committed, +// getStateObject will return a non-nil account after Suicide. +func (s *StateDB) Suicide(addr common.Address) bool { + stateObject := s.getStateObject(addr) + if stateObject == nil { + return false + } + s.journal.append(suicideChange{ + account: &addr, + prev: stateObject.suicided, + prevbalance: new(big.Int).Set(stateObject.Balance()), + }) + stateObject.markSuicided() + stateObject.account.Balance = new(big.Int) + + return true +} + +// PrepareAccessList handles the preparatory steps for executing a state transition with +// regards to both EIP-2929 and EIP-2930: +// +// - 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) +// +// This method should only be called if Yolov3/Berlin/2929+2930 is applicable at the current number. +func (s *StateDB) PrepareAccessList(sender common.Address, dst *common.Address, precompiles []common.Address, list ethtypes.AccessList) { + s.AddAddressToAccessList(sender) + if dst != nil { + s.AddAddressToAccessList(*dst) + // If it's a create-tx, the destination will be added inside evm.create + } + for _, addr := range precompiles { + s.AddAddressToAccessList(addr) + } + for _, el := range list { + s.AddAddressToAccessList(el.Address) + for _, key := range el.StorageKeys { + s.AddSlotToAccessList(el.Address, key) + } + } +} + +// AddAddressToAccessList adds the given address to the access list +func (s *StateDB) AddAddressToAccessList(addr common.Address) { + if s.accessList.AddAddress(addr) { + s.journal.append(accessListAddAccountChange{&addr}) + } +} + +// AddSlotToAccessList adds the given (address, slot)-tuple to the access list +func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { + addrMod, slotMod := s.accessList.AddSlot(addr, slot) + if addrMod { + // In practice, this should not happen, since there is no way to enter the + // scope of 'address' without having the 'address' become already added + // to the access list (via call-variant, create, etc). + // Better safe than sorry, though + s.journal.append(accessListAddAccountChange{&addr}) + } + if slotMod { + s.journal.append(accessListAddSlotChange{ + address: &addr, + slot: &slot, + }) + } +} + +// AddressInAccessList returns true if the given address is in the access list. +func (s *StateDB) AddressInAccessList(addr common.Address) bool { + return s.accessList.ContainsAddress(addr) +} + +// SlotInAccessList returns true if the given (address, slot)-tuple is in the access list. +func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { + return s.accessList.Contains(addr, slot) +} + +// Snapshot returns an identifier for the current revision of the state. +func (s *StateDB) Snapshot() int { + id := s.nextRevisionID + s.nextRevisionID++ + s.validRevisions = append(s.validRevisions, revision{id, s.journal.length()}) + return id +} + +// RevertToSnapshot reverts all state changes made since the given revision. +func (s *StateDB) RevertToSnapshot(revid int) { + // Find the snapshot in the stack of valid snapshots. + idx := sort.Search(len(s.validRevisions), func(i int) bool { + return s.validRevisions[i].id >= revid + }) + if idx == len(s.validRevisions) || s.validRevisions[idx].id != revid { + panic(fmt.Errorf("revision id %v cannot be reverted", revid)) + } + snapshot := s.validRevisions[idx].journalIndex + + // Replay the journal to undo changes and remove invalidated snapshots + s.journal.revert(s, snapshot) + s.validRevisions = s.validRevisions[:idx] +} + +// Commit writes the dirty states to keeper +// the StateDB object should be discarded after committed. +func (s *StateDB) Commit() error { + if s.dbErr != nil { + return fmt.Errorf("commit aborted due to earlier error: %v", s.dbErr) + } + for _, addr := range s.journal.sortedDirties() { + obj := s.stateObjects[addr] + if obj.suicided { + if err := s.keeper.DeleteAccount(s.ctx, obj.Address()); err != nil { + return sdkerrors.Wrap(err, "failed to delete account") + } + } else { + if obj.code != nil && obj.dirtyCode { + s.keeper.SetCode(s.ctx, obj.CodeHash(), obj.code) + } + if err := s.keeper.SetAccount(s.ctx, obj.Address(), obj.account); err != nil { + return sdkerrors.Wrap(err, "failed to set account") + } + for _, key := range obj.dirtyStorage.SortedKeys() { + value := obj.dirtyStorage[key] + // Skip noop changes, persist actual changes + if value == obj.originStorage[key] { + continue + } + s.keeper.SetState(s.ctx, obj.Address(), key, value.Bytes()) + } + } + } + return nil +} diff --git a/x/evm/statedb/statedb_test.go b/x/evm/statedb/statedb_test.go new file mode 100644 index 00000000..0cc0e85f --- /dev/null +++ b/x/evm/statedb/statedb_test.go @@ -0,0 +1,291 @@ +package statedb_test + +import ( + "math/big" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/suite" + "github.com/tharsis/ethermint/x/evm/statedb" +) + +type StateDBTestSuite struct { + suite.Suite +} + +func (suite *StateDBTestSuite) TestAccounts() { + addrErr := common.BigToAddress(big.NewInt(1)) + addr2 := common.BigToAddress(big.NewInt(2)) + testTxConfig := statedb.NewTxConfig( + common.BigToHash(big.NewInt(10)), // tx hash + common.BigToHash(big.NewInt(11)), // block hash + 1, // txIndex + 1, // logSize + ) + + testCases := []struct { + msg string + test func(*statedb.StateDB) + }{ + { + "success,empty account", + func(db *statedb.StateDB) { + suite.Require().Equal(true, db.Empty(addr2)) + suite.Require().Equal(big.NewInt(0), db.GetBalance(addr2)) + suite.Require().Equal([]byte(nil), db.GetCode(addr2)) + suite.Require().Equal(uint64(0), db.GetNonce(addr2)) + }, + }, + { + "success,GetBalance", + func(db *statedb.StateDB) { + db.AddBalance(addr2, big.NewInt(1)) + suite.Require().Equal(big.NewInt(1), db.GetBalance(addr2)) + }, + }, + { + "fail,GetBalance dbErr", + func(db *statedb.StateDB) { + suite.Require().Equal(big.NewInt(0), db.GetBalance(addrErr)) + suite.Require().Error(db.Commit()) + }, + }, + { + "success,change balance", + func(db *statedb.StateDB) { + db.AddBalance(addr2, big.NewInt(2)) + suite.Require().Equal(big.NewInt(2), db.GetBalance(addr2)) + db.SubBalance(addr2, big.NewInt(1)) + suite.Require().Equal(big.NewInt(1), db.GetBalance(addr2)) + + suite.Require().NoError(db.Commit()) + + // create a clean StateDB, check the balance is committed + db = statedb.New(db.Context(), db.Keeper(), testTxConfig) + suite.Require().Equal(big.NewInt(1), db.GetBalance(addr2)) + }, + }, + { + "success,SetState", + func(db *statedb.StateDB) { + key := common.BigToHash(big.NewInt(1)) + value := common.BigToHash(big.NewInt(1)) + + suite.Require().Equal(common.Hash{}, db.GetState(addr2, key)) + db.SetState(addr2, key, value) + suite.Require().Equal(value, db.GetState(addr2, key)) + suite.Require().Equal(common.Hash{}, db.GetCommittedState(addr2, key)) + }, + }, + { + "success,SetCode", + func(db *statedb.StateDB) { + code := []byte("hello world") + codeHash := crypto.Keccak256Hash(code) + db.SetCode(addr2, code) + suite.Require().Equal(code, db.GetCode(addr2)) + suite.Require().Equal(codeHash, db.GetCodeHash(addr2)) + + suite.Require().NoError(db.Commit()) + + // create a clean StateDB, check the code is committed + db = statedb.New(db.Context(), db.Keeper(), testTxConfig) + suite.Require().Equal(code, db.GetCode(addr2)) + suite.Require().Equal(codeHash, db.GetCodeHash(addr2)) + }, + }, + { + "success,CreateAccount", + func(db *statedb.StateDB) { + // test balance carry over when overwritten + amount := big.NewInt(1) + code := []byte("hello world") + key := common.BigToHash(big.NewInt(1)) + value := common.BigToHash(big.NewInt(1)) + + db.AddBalance(addr2, amount) + db.SetCode(addr2, code) + db.SetState(addr2, key, value) + + rev := db.Snapshot() + + db.CreateAccount(addr2) + suite.Require().Equal(amount, db.GetBalance(addr2)) + suite.Require().Equal([]byte(nil), db.GetCode(addr2)) + suite.Require().Equal(common.Hash{}, db.GetState(addr2, key)) + + db.RevertToSnapshot(rev) + suite.Require().Equal(amount, db.GetBalance(addr2)) + suite.Require().Equal(code, db.GetCode(addr2)) + suite.Require().Equal(value, db.GetState(addr2, key)) + + db.CreateAccount(addr2) + suite.Require().NoError(db.Commit()) + db = statedb.New(db.Context(), db.Keeper(), testTxConfig) + suite.Require().Equal(amount, db.GetBalance(addr2)) + suite.Require().Equal([]byte(nil), db.GetCode(addr2)) + suite.Require().Equal(common.Hash{}, db.GetState(addr2, key)) + }, + }, + { + "success,nested snapshot revert", + func(db *statedb.StateDB) { + key := common.BigToHash(big.NewInt(1)) + value1 := common.BigToHash(big.NewInt(1)) + value2 := common.BigToHash(big.NewInt(2)) + + rev1 := db.Snapshot() + db.SetState(addr2, key, value1) + + rev2 := db.Snapshot() + db.SetState(addr2, key, value2) + suite.Require().Equal(value2, db.GetState(addr2, key)) + + db.RevertToSnapshot(rev2) + suite.Require().Equal(value1, db.GetState(addr2, key)) + + db.RevertToSnapshot(rev1) + suite.Require().Equal(common.Hash{}, db.GetState(addr2, key)) + }, + }, + { + "success,nonce", + func(db *statedb.StateDB) { + suite.Require().Equal(uint64(0), db.GetNonce(addr2)) + db.SetNonce(addr2, 1) + suite.Require().Equal(uint64(1), db.GetNonce(addr2)) + + suite.Require().NoError(db.Commit()) + + db = statedb.New(db.Context(), db.Keeper(), testTxConfig) + suite.Require().Equal(uint64(1), db.GetNonce(addr2)) + }, + }, + { + "success,logs", + func(db *statedb.StateDB) { + data := []byte("hello world") + db.AddLog(ðtypes.Log{ + Address: addr2, + Topics: []common.Hash{}, + Data: data, + BlockNumber: 1, + }) + suite.Require().Equal(1, len(db.Logs())) + expecedLog := ðtypes.Log{ + Address: addr2, + Topics: []common.Hash{}, + Data: data, + BlockNumber: 1, + BlockHash: common.BigToHash(big.NewInt(10)), + TxHash: common.BigToHash(big.NewInt(11)), + TxIndex: 1, + Index: 1, + } + suite.Require().Equal(expecedLog, db.Logs()[0]) + + rev := db.Snapshot() + + db.AddLog(ðtypes.Log{ + Address: addr2, + Topics: []common.Hash{}, + Data: data, + BlockNumber: 1, + }) + suite.Require().Equal(2, len(db.Logs())) + suite.Require().Equal(uint(2), db.Logs()[1].Index) + + db.RevertToSnapshot(rev) + suite.Require().Equal(1, len(db.Logs())) + }, + }, + { + "success,refund", + func(db *statedb.StateDB) { + db.AddRefund(uint64(10)) + suite.Require().Equal(uint64(10), db.GetRefund()) + + rev := db.Snapshot() + + db.SubRefund(uint64(5)) + suite.Require().Equal(uint64(5), db.GetRefund()) + + db.RevertToSnapshot(rev) + suite.Require().Equal(uint64(10), db.GetRefund()) + }, + }, + { + "success,empty", + func(db *statedb.StateDB) { + suite.Require().False(db.Exist(addr2)) + suite.Require().True(db.Empty(addr2)) + + db.AddBalance(addr2, big.NewInt(1)) + suite.Require().True(db.Exist(addr2)) + suite.Require().False(db.Empty(addr2)) + + db.SubBalance(addr2, big.NewInt(1)) + suite.Require().True(db.Exist(addr2)) + suite.Require().True(db.Empty(addr2)) + }, + }, + { + "success,suicide commit", + func(db *statedb.StateDB) { + code := []byte("hello world") + db.SetCode(addr2, code) + db.AddBalance(addr2, big.NewInt(1)) + + suite.Require().True(db.Exist(addr2)) + suite.Require().False(db.Empty(addr2)) + + db.Suicide(addr2) + suite.Require().True(db.HasSuicided(addr2)) + suite.Require().True(db.Exist(addr2)) + suite.Require().Equal(new(big.Int), db.GetBalance(addr2)) + + suite.Require().NoError(db.Commit()) + db = statedb.New(db.Context(), db.Keeper(), testTxConfig) + suite.Require().True(db.Empty(addr2)) + }, + }, + { + "success,suicide revert", + func(db *statedb.StateDB) { + code := []byte("hello world") + db.SetCode(addr2, code) + db.AddBalance(addr2, big.NewInt(1)) + + rev := db.Snapshot() + + db.Suicide(addr2) + suite.Require().True(db.HasSuicided(addr2)) + + db.RevertToSnapshot(rev) + + suite.Require().False(db.HasSuicided(addr2)) + suite.Require().Equal(code, db.GetCode(addr2)) + suite.Require().Equal(big.NewInt(1), db.GetBalance(addr2)) + }, + }, + // TODO access lisForEachStorage + // https://github.com/tharsis/ethermint/issues/876 + } + for _, tc := range testCases { + suite.Run(tc.msg, func() { + db := statedb.New( + sdk.Context{}, + NewMockKeeper(), + testTxConfig, + ) + tc.test(db) + }) + } +} + +func TestStateDBTestSuite(t *testing.T) { + suite.Run(t, &StateDBTestSuite{}) +} diff --git a/x/evm/types/key.go b/x/evm/types/key.go index 1b89c260..36e5a8fc 100644 --- a/x/evm/types/key.go +++ b/x/evm/types/key.go @@ -29,15 +29,9 @@ const ( // prefix bytes for the EVM transient store const ( - prefixTransientSuicided = iota + 1 - prefixTransientBloom + prefixTransientBloom = iota + 1 prefixTransientTxIndex - prefixTransientRefund - prefixTransientAccessListAddress - prefixTransientAccessListSlot - prefixTransientTxHash prefixTransientLogSize - prefixTransientTxLogs ) // KVStore key prefixes @@ -48,15 +42,9 @@ var ( // Transient Store key prefixes var ( - KeyPrefixTransientSuicided = []byte{prefixTransientSuicided} - KeyPrefixTransientBloom = []byte{prefixTransientBloom} - KeyPrefixTransientTxIndex = []byte{prefixTransientTxIndex} - KeyPrefixTransientRefund = []byte{prefixTransientRefund} - KeyPrefixTransientAccessListAddress = []byte{prefixTransientAccessListAddress} - KeyPrefixTransientAccessListSlot = []byte{prefixTransientAccessListSlot} - KeyPrefixTransientTxHash = []byte{prefixTransientTxHash} - KeyPrefixTransientLogSize = []byte{prefixTransientLogSize} - KeyPrefixTransientTxLogs = []byte{prefixTransientTxLogs} + KeyPrefixTransientBloom = []byte{prefixTransientBloom} + KeyPrefixTransientTxIndex = []byte{prefixTransientTxIndex} + KeyPrefixTransientLogSize = []byte{prefixTransientLogSize} ) // AddressStoragePrefix returns a prefix to iterate over a given account storage. diff --git a/x/feemarket/keeper/keeper_test.go b/x/feemarket/keeper/keeper_test.go index 7a4a4fc7..94a1fd0a 100644 --- a/x/feemarket/keeper/keeper_test.go +++ b/x/feemarket/keeper/keeper_test.go @@ -90,7 +90,6 @@ func (suite *KeeperTestSuite) DoSetupTest(t require.TestingT) { ConsensusHash: tmhash.Sum([]byte("consensus")), LastResultsHash: tmhash.Sum([]byte("last_result")), }) - suite.app.EvmKeeper.WithContext(suite.ctx) queryHelper := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry()) types.RegisterQueryServer(queryHelper, suite.app.FeeMarketKeeper)