From c252e7f373850066935f67e7be07158948bcbd36 Mon Sep 17 00:00:00 2001 From: VictorTrustyDev Date: Sun, 22 Sep 2024 02:38:00 +0700 Subject: [PATCH 1/6] add Context-based-StateDB --- types/noop_event_manager.go | 37 + x/evm/utils/validation.go | 43 + x/evm/utils/validation_test.go | 157 ++ x/evm/vm/evm_config.go | 48 + x/evm/vm/interfaces.go | 23 + x/evm/vm/state_db.go | 608 +++++++ x/evm/vm/state_db_access_list.go | 143 ++ x/evm/vm/state_db_access_list_geth.go | 136 ++ x/evm/vm/state_db_access_list_test.go | 153 ++ x/evm/vm/state_db_account_tracker.go | 30 + x/evm/vm/state_db_account_tracker_test.go | 52 + x/evm/vm/state_db_geth.go | 68 + x/evm/vm/state_db_it_test.go | 1867 +++++++++++++++++++++ x/evm/vm/state_db_logs.go | 16 + x/evm/vm/state_db_logs_test.go | 53 + x/evm/vm/state_db_snapshot.go | 48 + x/evm/vm/state_db_test_methods.go | 102 ++ x/evm/vm/state_db_transient_store.go | 24 + x/evm/vm/state_db_transient_store_geth.go | 56 + x/evm/vm/state_db_transient_store_test.go | 63 + x/evm/vm/test_utils.go | 32 + 21 files changed, 3759 insertions(+) create mode 100644 types/noop_event_manager.go create mode 100644 x/evm/utils/validation.go create mode 100644 x/evm/utils/validation_test.go create mode 100644 x/evm/vm/evm_config.go create mode 100644 x/evm/vm/interfaces.go create mode 100644 x/evm/vm/state_db.go create mode 100644 x/evm/vm/state_db_access_list.go create mode 100644 x/evm/vm/state_db_access_list_geth.go create mode 100644 x/evm/vm/state_db_access_list_test.go create mode 100644 x/evm/vm/state_db_account_tracker.go create mode 100644 x/evm/vm/state_db_account_tracker_test.go create mode 100644 x/evm/vm/state_db_geth.go create mode 100644 x/evm/vm/state_db_it_test.go create mode 100644 x/evm/vm/state_db_logs.go create mode 100644 x/evm/vm/state_db_logs_test.go create mode 100644 x/evm/vm/state_db_snapshot.go create mode 100644 x/evm/vm/state_db_test_methods.go create mode 100644 x/evm/vm/state_db_transient_store.go create mode 100644 x/evm/vm/state_db_transient_store_geth.go create mode 100644 x/evm/vm/state_db_transient_store_test.go create mode 100644 x/evm/vm/test_utils.go diff --git a/types/noop_event_manager.go b/types/noop_event_manager.go new file mode 100644 index 0000000000..58347dc1ae --- /dev/null +++ b/types/noop_event_manager.go @@ -0,0 +1,37 @@ +package types + +import ( + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + gogoproto "github.com/cosmos/gogoproto/proto" +) + +var _ sdk.EventManagerI = &noOpEventManager{} + +type noOpEventManager struct{} + +func NewNoOpEventManager() sdk.EventManagerI { + return &noOpEventManager{} +} + +func (n noOpEventManager) Events() sdk.Events { + return []sdk.Event{} +} + +func (n noOpEventManager) ABCIEvents() []abci.Event { + return []abci.Event{} +} + +func (n noOpEventManager) EmitTypedEvent(_ gogoproto.Message) error { + return nil +} + +func (n noOpEventManager) EmitTypedEvents(_ ...gogoproto.Message) error { + return nil +} + +func (n noOpEventManager) EmitEvent(_ sdk.Event) { +} + +func (n noOpEventManager) EmitEvents(_ sdk.Events) { +} diff --git a/x/evm/utils/validation.go b/x/evm/utils/validation.go new file mode 100644 index 0000000000..f376320bd9 --- /dev/null +++ b/x/evm/utils/validation.go @@ -0,0 +1,43 @@ +package utils + +import ( + "reflect" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" +) + +// CheckIfAccountIsSuitableForDestroying checking the account is suitable for destroy (EVM) or not. +// +// It returns false and the reason if the account: +// 1. Is a module account. +// 2. Is a vesting account which still not expired. +func CheckIfAccountIsSuitableForDestroying(account sdk.AccountI) (destroyable bool, reason string) { + if account == nil || reflect.ValueOf(account).IsNil() { + panic("account is nil") + } + + if _, isModuleAcc := account.(sdk.ModuleAccountI); isModuleAcc { + reason = "module account is not suitable for destroying" + return + } + + if vestingAcc, ok := account.(*vestingtypes.BaseVestingAccount); ok { + if vestingAcc.GetEndTime() > time.Now().UTC().Unix() { + reason = "unexpired vesting account is not suitable for destroying" + return + } + } + + if vestingAcc, ok := account.(vesting.VestingAccount); ok { + if vestingAcc.GetEndTime() > time.Now().UTC().Unix() { + reason = "unexpired vesting account is not suitable for destroying" + return + } + } + + destroyable = true + return +} diff --git a/x/evm/utils/validation_test.go b/x/evm/utils/validation_test.go new file mode 100644 index 0000000000..8865db6391 --- /dev/null +++ b/x/evm/utils/validation_test.go @@ -0,0 +1,157 @@ +package utils + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/stretchr/testify/require" +) + +func TestCheckIfAccountIsSuitableForDestroying(t *testing.T) { + t.Run("panic if passing nil account", func(t *testing.T) { + nilAccounts := []sdk.AccountI{ + (*authtypes.BaseAccount)(nil), + (sdk.ModuleAccountI)(nil), + (vesting.VestingAccount)(nil), + (*vestingtypes.BaseVestingAccount)(nil), + (*vestingtypes.ContinuousVestingAccount)(nil), + (*vestingtypes.DelayedVestingAccount)(nil), + (*vestingtypes.PeriodicVestingAccount)(nil), + } + for _, nilAccount := range nilAccounts { + require.Panicsf(t, func() { + _, _ = CheckIfAccountIsSuitableForDestroying(nilAccount) + }, "must panic when passing nil account %T", nilAccount) + } + }) + + tests := []struct { + name string + accFunc func() sdk.AccountI + wantDestroyable bool + wantReasonContains string + }{ + { + name: "any base account", + accFunc: func() sdk.AccountI { + return &authtypes.BaseAccount{} + }, + wantDestroyable: true, + }, + { + name: "prohibit destroying module accounts", + accFunc: func() sdk.AccountI { + return &authtypes.ModuleAccount{} + }, + wantDestroyable: false, + wantReasonContains: "module account is not suitable for destroying", + }, + { + name: "prohibit destroying non-expired vesting accounts", + accFunc: func() sdk.AccountI { + return &vestingtypes.BaseVestingAccount{ + EndTime: time.Now().Add(time.Hour).UTC().Unix(), + } + }, + wantDestroyable: false, + wantReasonContains: "unexpired vesting account is not suitable for destroying", + }, + { + name: "prohibit destroying non-expired vesting accounts", + accFunc: func() sdk.AccountI { + return &vestingtypes.ContinuousVestingAccount{ + BaseVestingAccount: &vestingtypes.BaseVestingAccount{ + EndTime: time.Now().Add(time.Hour).UTC().Unix(), + }, + } + }, + wantDestroyable: false, + wantReasonContains: "unexpired vesting account is not suitable for destroying", + }, + { + name: "prohibit destroying non-expired vesting accounts", + accFunc: func() sdk.AccountI { + return &vestingtypes.DelayedVestingAccount{ + BaseVestingAccount: &vestingtypes.BaseVestingAccount{ + EndTime: time.Now().Add(time.Hour).UTC().Unix(), + }, + } + }, + wantDestroyable: false, + wantReasonContains: "unexpired vesting account is not suitable for destroying", + }, + { + name: "prohibit destroying non-expired vesting accounts", + accFunc: func() sdk.AccountI { + return &vestingtypes.PeriodicVestingAccount{ + BaseVestingAccount: &vestingtypes.BaseVestingAccount{ + EndTime: time.Now().Add(time.Hour).UTC().Unix(), + }, + } + }, + wantDestroyable: false, + wantReasonContains: "unexpired vesting account is not suitable for destroying", + }, + { + name: "allow destroying expired vesting accounts", + accFunc: func() sdk.AccountI { + return &vestingtypes.BaseVestingAccount{ + EndTime: time.Now().Add(-1 * time.Hour).UTC().Unix(), + } + }, + wantDestroyable: true, + }, + { + name: "allow destroying expired vesting accounts", + accFunc: func() sdk.AccountI { + return &vestingtypes.ContinuousVestingAccount{ + BaseVestingAccount: &vestingtypes.BaseVestingAccount{ + EndTime: time.Now().Add(-1 * time.Hour).UTC().Unix(), + }, + } + }, + wantDestroyable: true, + }, + { + name: "allow destroying expired vesting accounts", + accFunc: func() sdk.AccountI { + return &vestingtypes.DelayedVestingAccount{ + BaseVestingAccount: &vestingtypes.BaseVestingAccount{ + EndTime: time.Now().Add(-1 * time.Hour).UTC().Unix(), + }, + } + }, + wantDestroyable: true, + }, + { + name: "allow destroying expired vesting accounts", + accFunc: func() sdk.AccountI { + return &vestingtypes.PeriodicVestingAccount{ + BaseVestingAccount: &vestingtypes.BaseVestingAccount{ + EndTime: time.Now().Add(-1 * time.Hour).UTC().Unix(), + }, + } + }, + wantDestroyable: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotDestroyable, gotReason := CheckIfAccountIsSuitableForDestroying(tt.accFunc()) + + if tt.wantDestroyable { + require.True(t, gotDestroyable) + require.Empty(t, gotReason, "reason must be empty if account is destroyable") + } else { + require.False(t, gotDestroyable) + require.NotEmpty(t, gotReason, "reason must be provided if account is not destroyable") + require.NotEmpty(t, tt.wantReasonContains, "bad setup") + require.Contains(t, gotReason, tt.wantReasonContains) + } + }) + } +} diff --git a/x/evm/vm/evm_config.go b/x/evm/vm/evm_config.go new file mode 100644 index 0000000000..7690770c29 --- /dev/null +++ b/x/evm/vm/evm_config.go @@ -0,0 +1,48 @@ +package vm + +import ( + "math/big" + + evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" + ethparams "github.com/ethereum/go-ethereum/params" +) + +// TxConfig read-only information of current tx +type TxConfig struct { + BlockHash common.Hash + TxHash common.Hash + TxIndex uint + LogIndex uint // the index of next log within current block + TxType *uint8 // transaction type, optionally provided +} + +func (m TxConfig) WithTxTypeFromTransaction(ethTx *ethtypes.Transaction) TxConfig { + txType := ethTx.Type() + m.TxType = &txType + return m +} + +func (m TxConfig) WithTxTypeFromMessage(msg core.Message) TxConfig { + var txType uint8 + if msg.GasTipCap() != nil || msg.GasFeeCap() != nil { + txType = ethtypes.DynamicFeeTxType + } else if msg.AccessList() != nil { + txType = ethtypes.AccessListTxType + } else { + txType = ethtypes.LegacyTxType + } + m.TxType = &txType + return m +} + +// EVMConfig contains the needed information to initialize core VM +type EVMConfig struct { + Params evmtypes.Params + ChainConfig *ethparams.ChainConfig + CoinBase common.Address + BaseFee *big.Int + NoBaseFee bool +} diff --git a/x/evm/vm/interfaces.go b/x/evm/vm/interfaces.go new file mode 100644 index 0000000000..43a2897092 --- /dev/null +++ b/x/evm/vm/interfaces.go @@ -0,0 +1,23 @@ +package vm + +import ( + "math/big" + + evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" +) + +type EvmKeeper interface { + GetParams(ctx sdk.Context) (params evmtypes.Params) + ChainID() *big.Int + ForEachStorage(ctx sdk.Context, addr common.Address, cb func(key, value common.Hash) bool) + DeleteCodeHash(ctx sdk.Context, addr []byte) + SetState(ctx sdk.Context, addr common.Address, key common.Hash, value []byte) + GetCodeHash(ctx sdk.Context, addr []byte) common.Hash + GetCode(ctx sdk.Context, codeHash common.Hash) []byte + SetCode(ctx sdk.Context, codeHash, code []byte) + SetCodeHash(ctx sdk.Context, addr common.Address, codeHash common.Hash) + GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash + IsEmptyAccount(ctx sdk.Context, addr common.Address) bool +} diff --git a/x/evm/vm/state_db.go b/x/evm/vm/state_db.go new file mode 100644 index 0000000000..82dd026782 --- /dev/null +++ b/x/evm/vm/state_db.go @@ -0,0 +1,608 @@ +package vm + +import ( + "fmt" + "math/big" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + corevm "github.com/ethereum/go-ethereum/core/vm" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + ethparams "github.com/ethereum/go-ethereum/params" + + evertypes "github.com/EscanBE/evermint/v12/types" + evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" + evmutils "github.com/EscanBE/evermint/v12/x/evm/utils" +) + +var ( + _ corevm.StateDB = &cStateDb{} + _ CStateDB = &cStateDb{} +) + +// CStateDB is an interface that extends the vm.StateDB interface with additional methods. +// The implementation is Context-based, and the name CStateDB stands for Context-based-StateDB. +// +//goland:noinspection GoSnakeCaseUsage +type CStateDB interface { + corevm.StateDB + + GetTransactionLogs() []*ethtypes.Log + + CommitMultiStore(deleteEmptyObjects bool) error + IntermediateRoot(deleteEmptyObjects bool) (common.Hash, error) + + DestroyAccount(acc common.Address) + + // Not yet available in current version of go-ethereum + + Selfdestruct6780(address common.Address) + GetTransientState(addr common.Address, key common.Hash) common.Hash + SetTransientState(addr common.Address, key, value common.Hash) + + // For testing purposes only + + ForTest_AddAddressToSelfDestructedList(addr common.Address) + ForTest_SetLogs(logs Logs) + ForTest_CountRecordsTransientStorage() int + ForTest_CloneTransientStorage() TransientStorage + ForTest_CloneAccessList() *AccessList2 + ForTest_CloneTouched() AccountTracker + ForTest_CloneSelfDestructed() AccountTracker + ForTest_GetSnapshots() []RtStateDbSnapshot + ForTest_ToggleStateDBPreventCommit(prevent bool) + ForTest_GetOriginalContext() sdk.Context + ForTest_GetCurrentContext() sdk.Context + ForTest_GetEvmDenom() string + ForTest_IsCommitted() bool +} + +type cStateDb struct { + originalCtx sdk.Context // the original context that was passed to the constructor, do not change. Reserved for accessing committed state. + currentCtx sdk.Context + snapshots []RtStateDbSnapshot + committed bool + + evmKeeper EvmKeeper + accountKeeper authkeeper.AccountKeeper + bankKeeper bankkeeper.Keeper + + evmDenom string + chainConfig *ethparams.ChainConfig + + // other revertible states + + touched AccountTracker // list of touched address, which then will be considered to remove if empty + refund uint64 + selfDestructed AccountTracker + accessList *AccessList2 + logs Logs + // Legacy TODO UPGRADE check code changes for transientStorage at https://github.com/ethereum/go-ethereum/blob/master/core/state/transient_storage.go + transientStorage TransientStorage +} + +// preventCommit is a flag to prevent committing state changes to the underlying storage. +// This is used for testing purposes to simulate cases where Commit() is failed. +var preventCommit bool + +func (d *cStateDb) PrepareAccessList(sender common.Address, dest *common.Address, precompiles []common.Address, txAccesses ethtypes.AccessList) { + var coinbase common.Address // TODO ES: handle + + rules := d.chainConfig.Rules(big.NewInt(d.currentCtx.BlockHeight()), true) + + d.prepareByGoEthereum(rules, sender, coinbase, dest, precompiles, txAccesses) +} + +func (d *cStateDb) ForEachStorage(address common.Address, f func(common.Hash, common.Hash) bool) error { + d.evmKeeper.ForEachStorage(d.currentCtx, address, f) + return nil +} + +//goland:noinspection GoUnusedParameter +func NewStateDB( + ctx sdk.Context, + ethKeeper EvmKeeper, + accountKeeper authkeeper.AccountKeeper, + bankKeeper bankkeeper.Keeper, +) CStateDB { + // remove the event manager in order to prevent crappy events from being fired during state transition. + ctx = ctx.WithEventManager(evertypes.NewNoOpEventManager()) + + // fetching and cache x/evm params to prevent reload multiple time during execution + evmParams := ethKeeper.GetParams(ctx) + + sdb := &cStateDb{ + originalCtx: ctx, + + evmKeeper: ethKeeper, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + + evmDenom: evmParams.EvmDenom, + chainConfig: evmParams.ChainConfig.EthereumConfig(ethKeeper.ChainID()), + + touched: newAccountTracker(), + refund: 0, + selfDestructed: newAccountTracker(), + accessList: newAccessList2(), + transientStorage: TransientStorage(newTransientStorage()), + } + + firstSnapshot := newStateDbSnapshotFromStateDb(sdb, ctx) + firstSnapshot.id = -1 + sdb.snapshots = append(sdb.snapshots, firstSnapshot) + sdb.currentCtx = firstSnapshot.snapshotCtx + + return sdb +} + +// 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 (d *cStateDb) CreateAccount(address common.Address) { + d.touched.Add(address) + + existingBalance := d.bankKeeper.GetAllBalances(d.currentCtx, address.Bytes()) + + d.DestroyAccount(address) + + d.createAccountIfNotExists(address) + if !existingBalance.IsZero() { // carry over the balance + d.mintCoins(address.Bytes(), existingBalance) + } +} + +// DestroyAccount removes the account from the state. +// It removes the auth account, bank state, and evm state. +func (d *cStateDb) DestroyAccount(addr common.Address) { + // remove auth account + acc := d.accountKeeper.GetAccount(d.currentCtx, addr.Bytes()) + if acc != nil { + destroyable, protectedReason := evmutils.CheckIfAccountIsSuitableForDestroying(acc) + if !destroyable { + panic( + sdkerrors.ErrLogic.Wrapf( + "prohibited to destroy existing account %s with reason: %s", + addr.Hex(), protectedReason, + ), + ) + } + d.accountKeeper.RemoveAccount(d.currentCtx, acc) + } + + // remove bank state + existingBalances := d.bankKeeper.GetAllBalances(d.currentCtx, addr.Bytes()) + if !existingBalances.IsZero() { + d.burnCoins(addr.Bytes(), existingBalances) + } + + // remove evm state + d.evmKeeper.DeleteCodeHash(d.currentCtx, addr.Bytes()) + d.evmKeeper.ForEachStorage(d.currentCtx, addr, func(key, _ common.Hash) bool { + d.evmKeeper.SetState(d.currentCtx, addr, key, nil) + return true + }) +} + +func (d *cStateDb) createAccountIfNotExists(address common.Address) { + if d.accountKeeper.HasAccount(d.currentCtx, address.Bytes()) { + // no-op + return + } + + accountI := d.accountKeeper.NewAccountWithAddress(d.currentCtx, address.Bytes()) + d.accountKeeper.SetAccount(d.currentCtx, accountI) +} + +// SubBalance subtracts amount from the account associated with addr. +func (d *cStateDb) SubBalance(address common.Address, b *big.Int) { + d.touched.Add(address) + + if b.Sign() == 0 { + return + } + + coinsToBurn := sdk.NewCoins(sdk.NewCoin(d.evmDenom, sdkmath.NewIntFromBigInt(b))) + d.burnCoins(address.Bytes(), coinsToBurn) +} + +func (d cStateDb) burnCoins(accAddr sdk.AccAddress, coins sdk.Coins) { + if coins.IsZero() { + return + } + + err := d.bankKeeper.SendCoinsFromAccountToModule(d.currentCtx, accAddr, evmtypes.ModuleName, coins) + if err != nil { + panic(evmtypes.ErrEngineFailure.Wrapf("failed to send coins: %s", err.Error())) + } + err = d.bankKeeper.BurnCoins(d.currentCtx, evmtypes.ModuleName, coins) + if err != nil { + panic(evmtypes.ErrEngineFailure.Wrapf("failed to mint coins: %s", err.Error())) + } +} + +// AddBalance adds amount to the account associated with addr. +func (d *cStateDb) AddBalance(address common.Address, b *big.Int) { + d.touched.Add(address) + + if b.Sign() == 0 { + return + } + + coinsToMint := sdk.NewCoins(sdk.NewCoin(d.evmDenom, sdkmath.NewIntFromBigInt(b))) + d.mintCoins(address.Bytes(), coinsToMint) +} + +func (d cStateDb) mintCoins(accAddr sdk.AccAddress, coins sdk.Coins) { + if coins.IsZero() { + return + } + + err := d.bankKeeper.MintCoins(d.currentCtx, evmtypes.ModuleName, coins) + if err != nil { + panic(evmtypes.ErrEngineFailure.Wrapf("failed to mint coins: %s", err.Error())) + } + err = d.bankKeeper.SendCoinsFromModuleToAccount(d.currentCtx, evmtypes.ModuleName, accAddr, coins) + if err != nil { + panic(evmtypes.ErrEngineFailure.Wrapf("failed to send coins: %s", err.Error())) + } +} + +// GetBalance retrieves the balance from the given address or 0 if object not found +func (d *cStateDb) GetBalance(address common.Address) *big.Int { + return d.bankKeeper.GetBalance(d.currentCtx, address.Bytes(), d.evmDenom).Amount.BigInt() +} + +// GetNonce retrieves the nonce from the given address or 0 if object not found +func (d *cStateDb) GetNonce(address common.Address) uint64 { + acc := d.accountKeeper.GetAccount(d.currentCtx, address.Bytes()) + if acc == nil { + return 0 + } + return acc.GetSequence() +} + +// SetNonce sets the nonce for the given address, if account is not found it will be created +func (d *cStateDb) SetNonce(address common.Address, n uint64) { + d.touched.Add(address) + + d.createAccountIfNotExists(address) + + if !d.accountKeeper.HasAccount(d.currentCtx, address.Bytes()) { + panic(evmtypes.ErrEngineFailure.Wrapf("failed to get account for %s", address.String())) + } + + acc := d.accountKeeper.GetAccount(d.currentCtx, address.Bytes()) + if err := acc.SetSequence(n); err != nil { + panic(evmtypes.ErrEngineFailure.Wrapf("failed to set nonce for %s: %s", address.String(), err.Error())) + } + d.accountKeeper.SetAccount(d.currentCtx, acc) +} + +func (d *cStateDb) GetCodeHash(address common.Address) common.Hash { + return d.evmKeeper.GetCodeHash(d.currentCtx, address.Bytes()) +} + +func (d *cStateDb) GetCode(address common.Address) []byte { + codeHash := d.GetCodeHash(address) + return d.evmKeeper.GetCode(d.currentCtx, codeHash) +} + +func (d *cStateDb) SetCode(address common.Address, code []byte) { + d.touched.Add(address) + + d.createAccountIfNotExists(address) + codeHash := computeCodeHash(code) + + d.evmKeeper.SetCode(d.currentCtx, codeHash.Bytes(), code) + d.evmKeeper.SetCodeHash(d.currentCtx, address, codeHash) +} + +func (d *cStateDb) GetCodeSize(address common.Address) int { + return len(d.GetCode(address)) +} + +// AddRefund adds gas to the refund counter. +// This method will panic if the refund counter goes above max uint64. +func (d *cStateDb) AddRefund(gas uint64) { + newRefund := d.refund + gas + if newRefund < gas { + panic(evmtypes.ErrEngineFailure.Wrapf("gas refund counter overflow")) + } + d.refund = newRefund +} + +// SubRefund removes gas from the refund counter. +// This method will panic if the refund counter goes below zero +func (d *cStateDb) SubRefund(gas uint64) { + if d.refund < gas { + panic(evmtypes.ErrEngineFailure.Wrapf("gas refund greater than remaining refund %d/%d", gas, d.refund)) + } + d.refund -= gas +} + +// GetRefund returns the current value of the refund counter. +func (d *cStateDb) GetRefund() uint64 { + return d.refund +} + +// GetCommittedState retrieves a value from the given account's committed storage trie. +func (d *cStateDb) GetCommittedState(address common.Address, hash common.Hash) common.Hash { + accountAtCurrentCtx := d.accountKeeper.GetAccount(d.currentCtx, address.Bytes()) + if accountAtCurrentCtx == nil { + // short-circuit for destroyed or not exists account + return common.Hash{} + } + + accountAtOriginalCtx := d.accountKeeper.GetAccount(d.originalCtx, address.Bytes()) + if accountAtOriginalCtx == nil { + // short-circuit for new account that not committed + return common.Hash{} + } + + if accountAtCurrentCtx.GetAccountNumber() != accountAtOriginalCtx.GetAccountNumber() { + // short-circuit for remade account + return common.Hash{} + } + + return d.evmKeeper.GetState( + d.originalCtx, /*get from original*/ + address, + hash, + ) +} + +// GetState retrieves a value from the given account's storage trie. +func (d *cStateDb) GetState(address common.Address, hash common.Hash) common.Hash { + return d.evmKeeper.GetState(d.currentCtx, address, hash) +} + +func (d *cStateDb) SetState(address common.Address, key, value common.Hash) { + d.touched.Add(address) + + d.createAccountIfNotExists(address) + + d.evmKeeper.SetState(d.currentCtx, address, key, value.Bytes()) +} + +// GetTransientState gets transient storage for a given account. +func (d *cStateDb) GetTransientState(addr common.Address, key common.Hash) common.Hash { + return d.transientStorage.Get(addr, key) +} + +// SetTransientState sets transient storage for a given account. +func (d *cStateDb) SetTransientState(addr common.Address, key, value common.Hash) { + d.transientStorage.Set(addr, key, value) +} + +// Suicide marks the given account as self-destructed. +// 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 SelfDestruct. +func (d *cStateDb) Suicide(address common.Address) bool { + d.touched.Add(address) + + account := d.accountKeeper.GetAccount(d.currentCtx, address.Bytes()) + if account == nil { + return false + } + + // mark self-destructed + d.selfDestructed.Add(address) + + // clear balance + currentBalance := d.bankKeeper.GetBalance(d.currentCtx, address.Bytes(), d.evmDenom).Amount + if !currentBalance.IsZero() { + d.SubBalance(address, currentBalance.BigInt()) + } + + return true +} + +// HasSuicided returns true if the account was marked as self-destructed. +func (d *cStateDb) HasSuicided(address common.Address) bool { + return d.selfDestructed.Has(address) +} + +// Selfdestruct6780 sames as SelfDestruct but only operate if it was created within the same tx. +// Note: this feature not yet available in go-ethereum v1.10.26. +func (d *cStateDb) Selfdestruct6780(address common.Address) { + accountAtCurrentCtx := d.accountKeeper.GetAccount(d.currentCtx, address.Bytes()) + if accountAtCurrentCtx == nil { + return + } + + var createdWithinSameTx bool + + accountAtOriginalCtx := d.accountKeeper.GetAccount(d.originalCtx, address.Bytes()) + if accountAtOriginalCtx == nil { + createdWithinSameTx = true + } else if accountAtCurrentCtx.GetAccountNumber() != accountAtOriginalCtx.GetAccountNumber() { + createdWithinSameTx = true + } + + if createdWithinSameTx { + d.Suicide(address) + } +} + +// Exist reports whether the given account exists in state. +// Notably this should also return true for self-destructed accounts. +func (d *cStateDb) Exist(address common.Address) bool { + if d.selfDestructed.Has(address) { + return true + } + + return d.accountKeeper.HasAccount(d.currentCtx, address.Bytes()) +} + +// Empty returns whether the given account is empty. Empty +// is defined according to EIP161 (balance = nonce = code = 0). +func (d *cStateDb) Empty(address common.Address) bool { + return d.evmKeeper.IsEmptyAccount(d.currentCtx, address) +} + +// AddressInAccessList returns true if the given address is in the access list. +func (d *cStateDb) AddressInAccessList(addr common.Address) bool { + return d.accessList.ContainsAddress(addr) +} + +// SlotInAccessList returns true if the given (address, slot)-tuple is in the access list. +func (d *cStateDb) SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) { + return d.accessList.Contains(addr, slot) +} + +// AddAddressToAccessList adds the given address to the access list. This operation is safe to perform +// even if the feature/fork is not active yet +func (d *cStateDb) AddAddressToAccessList(addr common.Address) { + d.accessList.AddAddress(addr) +} + +// AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform +// even if the feature/fork is not active yet +func (d *cStateDb) AddSlotToAccessList(addr common.Address, slot common.Hash) { + _, _ = d.accessList.AddSlot(addr, slot) +} + +// Prepare handles the preparatory steps for executing a state transition with. +// This method must be invoked before state transition. +func (d *cStateDb) Prepare(rules ethparams.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses ethtypes.AccessList) { + // Legacy TODO UPGRADE check code changes for method StateDB::Prepare at https://github.com/ethereum/go-ethereum/blob/master/core/state/statedb.go + d.prepareByGoEthereum(rules, sender, coinbase, dest, precompiles, txAccesses) +} + +// RevertToSnapshot reverts all state changes made since the given revision. +func (d *cStateDb) RevertToSnapshot(id int) { + if id < 0 { + panic(evmtypes.ErrEngineFailure.Wrapf("invalid snapshot id: %d, below 0", id)) + } + + snapshotIdx := id + 1 // the first snapshot was created during state db initialization + snapshotState := d.snapshots[snapshotIdx] + + if snapshotState.id != id { + panic(evmtypes.ErrEngineFailure.Wrapf("invalid snapshot id: %d, expected %d", snapshotState.id, id)) + } + + snapshotContext := d.snapshots[snapshotIdx-1] // this snapshot contains all change before snapshot was made + cacheCtx, writeFunc := snapshotContext.snapshotCtx.CacheContext() + snapshotState.snapshotCtx = cacheCtx + snapshotState.writeFunc = writeFunc + + // revert all changes made after the reverted snapshot. + // NOTICE: always copy every backup fields from snapshot, do not direct assign. + + d.currentCtx = cacheCtx + d.touched = snapshotState.touched.Copy() + d.refund = snapshotState.refund + d.selfDestructed = snapshotState.selfDestructed.Copy() + d.accessList = snapshotState.accessList.Copy() + d.logs = snapshotState.logs.Copy() + d.transientStorage = snapshotState.transientStorage.Clone() + + // discard all snapshots after selected till the end + d.snapshots = append(d.snapshots[:snapshotIdx], snapshotState /*ensure changes to this record are applied*/) +} + +// Snapshot returns an identifier for the current revision of the state. +func (d *cStateDb) Snapshot() int { + nextSnapshot := newStateDbSnapshotFromStateDb(d, d.currentCtx) + nextSnapshot.id = len(d.snapshots) - 1 + + d.currentCtx = nextSnapshot.snapshotCtx + d.snapshots = append(d.snapshots, nextSnapshot) + + return nextSnapshot.id +} + +// AddLog adds a log, called by evm. +// +// WARNING: should maintain non-consensus fields externally. +func (d *cStateDb) AddLog(log *ethtypes.Log) { + // TODO LOGIC fill non-consensus fields externally + d.logs = append(d.logs, log) +} + +// GetTransactionLogs returns the logs added. The non-consensus fields should be filled externally. +func (d *cStateDb) GetTransactionLogs() []*ethtypes.Log { + if len(d.logs) < 1 { + return []*ethtypes.Log{} + } + return d.logs[:] +} + +// 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 (d *cStateDb) AddPreimage(_ common.Hash, _ []byte) { + // no-op (same as go-ethereum) +} + +// CommitMultiStore commits branched cache multi-store to the original store, all state-transition will take effect. +func (d *cStateDb) CommitMultiStore(deleteEmptyObjects bool) error { + if d.committed { + panic("called commit twice") + } + + if preventCommit { + return fmt.Errorf("failed to commit state changes") + } + + d.committed = true // prohibit further commit + + for touchedAddress := range d.touched { + _, markedAsDestroy := d.selfDestructed[touchedAddress] + if markedAsDestroy || (deleteEmptyObjects && d.Empty(touchedAddress)) { + d.DestroyAccount(touchedAddress) + } + } + + // When `writeFunc` is invoked, it will write the cache-multi-store branch to the parent cache-multi-store branch, + // so we need to invoke `writeFunc` from the most-recent snapshot to the earliest snapshot + // for the changes to be committed to the left-most cache-multi-store, + // then the left-most cache-multi-store commit to the original store which included into the original context. + for i := len(d.snapshots) - 1; i >= 0; i-- { + d.snapshots[i].WriteChanges() + } + + return nil +} + +// IntermediateRoot computes the current root hash of the state trie. +// It is called in between transactions to get the root hash that +// goes into transaction receipts. +// +// In the current implementation, it is not possible to compute the root hash right away, +// and later there is logic to simulate the root hash, so we will return the empty root hash at this place. +func (d *cStateDb) IntermediateRoot(deleteEmptyObjects bool) (common.Hash, error) { + if err := d.CommitMultiStore(deleteEmptyObjects); err != nil { + return common.Hash{}, err + } + + return ethtypes.EmptyRootHash, nil +} + +// computeCodeHash computes the code hash of the given code. +// If the given code is empty, it returns `ethtypes.EmptyCodeHash`. +func computeCodeHash(code []byte) common.Hash { + if len(code) == 0 { + return common.BytesToHash(evmtypes.EmptyCodeHash) + } + return ethcrypto.Keccak256Hash(code) +} diff --git a/x/evm/vm/state_db_access_list.go b/x/evm/vm/state_db_access_list.go new file mode 100644 index 0000000000..5858a612a7 --- /dev/null +++ b/x/evm/vm/state_db_access_list.go @@ -0,0 +1,143 @@ +package vm + +import "github.com/ethereum/go-ethereum/common" + +// accessListI is for checking compatible of accessList (by go-ethereum) and AccessList2. +// Here we have files: +// 1. `state_db_access_list.go` which is our implementation. +// 2. `state_db_access_list_geth.go` which is go-ethereum implementation, just keeping for compare compatible. +// +// Legacy TODO UPGRADE always copy accessList by go-ethereum implementation for testing new changes. +// https://github.com/ethereum/go-ethereum/blob/master/core/state/access_list.go +type accessListI interface { + ContainsAddress(address common.Address) bool + Contains(address common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) + AddAddress(address common.Address) bool + AddSlot(address common.Address, slot common.Hash) (addrChange bool, slotChange bool) + DeleteSlot(address common.Address, slot common.Hash) + DeleteAddress(address common.Address) +} + +var ( + _ accessListI = (*accessList)(nil) + _ accessListI = (*AccessList2)(nil) +) + +// AccessList2 has the same functionality as the accessList in go-ethereum. +// But different implementation in order to perform a deep copy to match +// with the implementation of Snapshot and RevertToSnapshot. +type AccessList2 struct { + elements map[common.Address]map[common.Hash]bool +} + +// newAccessList2 creates a new AccessList2. +func newAccessList2() *AccessList2 { + return &AccessList2{ + elements: make(map[common.Address]map[common.Hash]bool), + } +} + +// ContainsAddress returns true if the address is in the access list. +func (al *AccessList2) ContainsAddress(address common.Address) bool { + _, ok := al.elements[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 *AccessList2) Contains(address common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { + slots, ok := al.elements[address] + if !ok { + return false, false + } + if len(slots) == 0 { + return true, false + } + _, slotPresent = slots[slot] + return true, slotPresent +} + +// Copy creates an independent copy of an AccessList2. +func (al *AccessList2) Copy() *AccessList2 { + elements := make(map[common.Address]map[common.Hash]bool, len(al.elements)) + for address, existingSlots := range al.elements { + slots := make(map[common.Hash]bool, len(existingSlots)) + + for slot := range existingSlots { + slots[slot] = false // whatever value, not matter + } + + if len(slots) == 0 { + elements[address] = nil + continue + } + + elements[address] = slots + } + + return &AccessList2{ + elements: elements, + } +} + +func (al *AccessList2) CloneElements() map[common.Address]map[common.Hash]bool { + return al.Copy().elements +} + +// 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 *AccessList2) AddAddress(address common.Address) bool { + if _, exists := al.elements[address]; exists { + return false + } + al.elements[address] = nil + return true +} + +// AddSlot adds the specified (addr, slot) combo to the access list. +// Return values are: +// - address added +// - slot added +func (al *AccessList2) AddSlot(address common.Address, slot common.Hash) (addrChange bool, slotChange bool) { + existingSlots, addressExisting := al.elements[address] + if !addressExisting { + al.elements[address] = map[common.Hash]bool{ + slot: false, // whatever value, not matter + } + return true, true + } + + if len(existingSlots) == 0 { + al.elements[address] = map[common.Hash]bool{ + slot: false, // whatever value, not matter + } + return false, true + } + + if _, slotExisting := existingSlots[slot]; !slotExisting { + existingSlots[slot] = false // whatever value, not matter + return false, true + } + + return false, false +} + +// DeleteSlot removes an (address, slot)-tuple from the access list. +func (al *AccessList2) DeleteSlot(address common.Address, slot common.Hash) { + existingSlots, addressExisting := al.elements[address] + if !addressExisting { + panic("reverting slot change, address not present in list") + } + + delete(existingSlots, slot) + + if len(existingSlots) == 0 { + // cleanup + al.elements[address] = nil + } +} + +// DeleteAddress removes an address from the access list. +func (al *AccessList2) DeleteAddress(address common.Address) { + delete(al.elements, address) +} diff --git a/x/evm/vm/state_db_access_list_geth.go b/x/evm/vm/state_db_access_list_geth.go new file mode 100644 index 0000000000..13f9fe61e9 --- /dev/null +++ b/x/evm/vm/state_db_access_list_geth.go @@ -0,0 +1,136 @@ +// 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 vm + +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 (a *accessList) Copy() *accessList { + cp := newAccessList() + for k, v := range a.addresses { + cp.addresses[k] = v + } + cp.slots = make([]map[common.Hash]struct{}, len(a.slots)) + for i, slotMap := range a.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] + // There are two ways this can fail + 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/vm/state_db_access_list_test.go b/x/evm/vm/state_db_access_list_test.go new file mode 100644 index 0000000000..0051ce0d6b --- /dev/null +++ b/x/evm/vm/state_db_access_list_test.go @@ -0,0 +1,153 @@ +package vm + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_AccessList(t *testing.T) { + al2 := newAccessList2() + testAccessList(t, al2) + + alGeth := newAccessList() + testAccessList(t, alGeth) + + t.Run("Copy AccessList2", func(t *testing.T) { + alSrc := newAccessList2() + + fillRandomDataToSrc := func() { + for a := 10; a < 20; a++ { + addr := GenerateAddress() + alSrc.AddAddress(addr) + + if a%2 == 0 { + for s := 0; s < a-8; s++ { + alSrc.AddSlot(addr, GenerateHash()) + } + } + } + } + + fillRandomDataToSrc() + + originalLen := len(alSrc.elements) + originalSumLen := originalLen + for _, v := range alSrc.elements { + originalSumLen += len(v) + } + + alCopy := alSrc.Copy() + require.True(t, reflect.DeepEqual(alSrc.elements, alCopy.elements)) + + fillRandomDataToSrc() + require.False(t, reflect.DeepEqual(alSrc.elements, alCopy.elements)) + + laterLen := len(alSrc.elements) + laterSumLen := laterLen + for _, v := range alSrc.elements { + laterSumLen += len(v) + } + + require.Less(t, originalSumLen, laterSumLen) + require.Less(t, originalLen, laterLen) + + copyLen := len(alCopy.elements) + copySumLen := copyLen + for _, v := range alCopy.elements { + copySumLen += len(v) + } + + require.Equal(t, originalSumLen, copySumLen) + require.Equal(t, originalLen, copyLen) + }) +} + +func testAccessList(t *testing.T, al accessListI) { + require.NotNil(t, al) + + addr1 := GenerateAddress() + addr2 := GenerateAddress() + + require.False(t, al.ContainsAddress(addr1)) + require.False(t, al.ContainsAddress(addr2)) + + al.AddAddress(addr1) + require.True(t, al.ContainsAddress(addr1)) + require.False(t, al.ContainsAddress(addr2)) + + al.AddAddress(addr2) + require.True(t, al.ContainsAddress(addr2)) + + slot1 := GenerateHash() + slot2 := GenerateHash() + + existsAddr, existsSlot := al.Contains(addr1, slot1) + require.True(t, existsAddr) + require.False(t, existsSlot) + + addrChange, slotChange := al.AddSlot(addr1, slot1) + require.False(t, addrChange) + require.True(t, slotChange) + + addrChange, slotChange = al.AddSlot(addr1, slot1) + require.False(t, addrChange) + require.False(t, slotChange) + + existsAddr, existsSlot = al.Contains(addr1, slot1) + require.True(t, existsAddr) + require.True(t, existsSlot) + + existsAddr, existsSlot = al.Contains(addr1, slot2) + require.True(t, existsAddr) + require.False(t, existsSlot) + + existsAddr, existsSlot = al.Contains(GenerateAddress(), slot1) + require.False(t, existsAddr) + require.False(t, existsSlot) + + addr3 := GenerateAddress() + require.False(t, al.ContainsAddress(addr3)) + require.True(t, al.AddAddress(addr3)) + require.True(t, al.ContainsAddress(addr3)) + require.False(t, al.AddAddress(addr3)) + + require.NotPanics(t, func() { + al.DeleteAddress(GenerateAddress()) + }, "should not panic even tho address is not in the access list") + + require.Panics(t, func() { + al.DeleteSlot(GenerateAddress(), GenerateHash()) + }, "should panic if address is not in the access list") + + al.DeleteAddress(addr1) + require.False(t, al.ContainsAddress(addr1)) + + addrChange, slotChange = al.AddSlot(addr1, slot1) + require.True(t, addrChange) + require.True(t, slotChange) + + addrChange, slotChange = al.AddSlot(addr1, slot2) + require.False(t, addrChange) + require.True(t, slotChange) + + require.NotPanics(t, func() { + al.DeleteSlot(addr1, GenerateHash()) + }, "should not panic even slot does not exists") + + require.NotPanics(t, func() { + al.DeleteSlot(addr1, slot1) + }) + if localAl, ok := al.(*AccessList2); ok { + require.Len(t, localAl.elements[addr1], 1) + } + + require.NotPanics(t, func() { + al.DeleteSlot(addr1, slot2) + }) + if localAl, ok := al.(*AccessList2); ok { + require.Len(t, localAl.elements[addr1], 0) + require.Nil(t, localAl.elements[addr1], "map should be erased") + } +} diff --git a/x/evm/vm/state_db_account_tracker.go b/x/evm/vm/state_db_account_tracker.go new file mode 100644 index 0000000000..c92e2f0508 --- /dev/null +++ b/x/evm/vm/state_db_account_tracker.go @@ -0,0 +1,30 @@ +package vm + +import "github.com/ethereum/go-ethereum/common" + +type AccountTracker map[common.Address]bool + +func newAccountTracker() AccountTracker { + return make(AccountTracker) +} + +func (t AccountTracker) Add(addr common.Address) { + t[addr] = false // whatever value, not matter +} + +func (t AccountTracker) Has(addr common.Address) bool { + _, found := t[addr] + return found +} + +func (t AccountTracker) Delete(addr common.Address) { + delete(t, addr) +} + +func (t AccountTracker) Copy() AccountTracker { + tracker := make(AccountTracker) + for k, v := range t { + tracker[k] = v + } + return tracker +} diff --git a/x/evm/vm/state_db_account_tracker_test.go b/x/evm/vm/state_db_account_tracker_test.go new file mode 100644 index 0000000000..cc2bdfe0d4 --- /dev/null +++ b/x/evm/vm/state_db_account_tracker_test.go @@ -0,0 +1,52 @@ +package vm + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_AccountTracker(t *testing.T) { + originalTracker := newAccountTracker() + require.NotNil(t, originalTracker) + require.Empty(t, originalTracker) + + addr1 := GenerateAddress() + addr2 := GenerateAddress() + addr3 := GenerateAddress() + + originalTracker.Add(addr1) + originalTracker.Add(addr2) + require.True(t, originalTracker.Has(addr1)) + require.True(t, originalTracker.Has(addr2)) + require.False(t, originalTracker.Has(addr3)) + + copiedTracker := originalTracker.Copy() + + addr4 := GenerateAddress() + copiedTracker.Add(addr4) + require.True(t, copiedTracker.Has(addr1)) + require.True(t, copiedTracker.Has(addr2)) + require.False(t, copiedTracker.Has(addr3)) + require.True(t, copiedTracker.Has(addr4)) + + // change address 1 in copied tracker + copiedTracker.Delete(addr1) + + // assert original value on the original tracker + require.True(t, originalTracker.Has(addr1)) + require.True(t, originalTracker.Has(addr2)) + require.False(t, originalTracker.Has(addr3)) + require.False(t, originalTracker.Has(addr4)) + + // update on original tracker and expect not to effect copied tracker + originalTracker.Delete(addr1) + originalTracker.Delete(addr2) + originalTracker.Add(addr3) + originalTracker.Add(addr4) + + require.False(t, copiedTracker.Has(addr1)) + require.True(t, copiedTracker.Has(addr2)) + require.False(t, copiedTracker.Has(addr3)) + require.True(t, copiedTracker.Has(addr4)) +} diff --git a/x/evm/vm/state_db_geth.go b/x/evm/vm/state_db_geth.go new file mode 100644 index 0000000000..d324eea22f --- /dev/null +++ b/x/evm/vm/state_db_geth.go @@ -0,0 +1,68 @@ +// Copyright 2014 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 state provides a caching layer atop the Ethereum state trie. + +package vm + +import ( + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + ethparams "github.com/ethereum/go-ethereum/params" +) + +// prepareByGoEthereum is copied from go-ethereum. +// +// geth::Prepare handles the preparatory steps for executing a state transition with. +// This method must be invoked before state transition. +// +// Berlin fork: +// - 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) +// +// Potential EIPs: +// - Reset access list (Berlin) +// - Add coinbase to access list (EIP-3651) +// - Reset transient storage (EIP-1153) +func (d *cStateDb) prepareByGoEthereum(rules ethparams.Rules, sender, coinbase common.Address, dst *common.Address, precompiles []common.Address, list ethtypes.AccessList) { + if rules.IsBerlin { + // Clear out any leftover from previous executions + al := newAccessList2() + d.accessList = al + + al.AddAddress(sender) + if dst != nil { + al.AddAddress(*dst) + // If it's a create-tx, the destination will be added inside evm.create + } + for _, addr := range precompiles { + al.AddAddress(addr) + } + for _, el := range list { + al.AddAddress(el.Address) + for _, key := range el.StorageKeys { + al.AddSlot(el.Address, key) + } + } + if rules.IsShanghai { // EIP-3651: warm coinbase + al.AddAddress(coinbase) + } + } + // Reset transient storage at the beginning of transaction execution + d.transientStorage = newTransientStorage() +} diff --git a/x/evm/vm/state_db_it_test.go b/x/evm/vm/state_db_it_test.go new file mode 100644 index 0000000000..5b43b13267 --- /dev/null +++ b/x/evm/vm/state_db_it_test.go @@ -0,0 +1,1867 @@ +package vm_test + +//goland:noinspection SpellCheckingInspection +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "math/big" + mathrand "math/rand" + "reflect" + "testing" + "time" + + "github.com/ethereum/go-ethereum/crypto" + + sdkmath "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + + "github.com/EscanBE/evermint/v12/integration_test_util" + itutiltypes "github.com/EscanBE/evermint/v12/integration_test_util/types" + evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + corevm "github.com/ethereum/go-ethereum/core/vm" + "github.com/stretchr/testify/suite" +) + +//goland:noinspection GoSnakeCaseUsage,SpellCheckingInspection +type StateDbIntegrationTestSuite struct { + suite.Suite + CITS *integration_test_util.ChainIntegrationTestSuite + StateDB corevm.StateDB + CStateDB evmvm.CStateDB +} + +func (suite *StateDbIntegrationTestSuite) App() itutiltypes.ChainApp { + return suite.CITS.ChainApp +} + +func (suite *StateDbIntegrationTestSuite) Ctx() sdk.Context { + return suite.CITS.CurrentContext. + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) +} + +func (suite *StateDbIntegrationTestSuite) Commit() { + suite.CITS.Commit() +} + +func TestStateDbIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(StateDbIntegrationTestSuite)) +} + +func (suite *StateDbIntegrationTestSuite) SetupSuite() { +} + +func (suite *StateDbIntegrationTestSuite) SetupTest() { + if suite.CITS != nil { + suite.TearDownTest() + } + + suite.CITS = integration_test_util.CreateChainIntegrationTestSuite(suite.T(), suite.Require()) + suite.StateDB = suite.newStateDB() + suite.CStateDB = suite.StateDB.(evmvm.CStateDB) +} + +func (suite *StateDbIntegrationTestSuite) TearDownTest() { + suite.CITS.Cleanup() +} + +func (suite *StateDbIntegrationTestSuite) TearDownSuite() { +} + +func (suite *StateDbIntegrationTestSuite) SkipIfDisabledTendermint() { + if !suite.CITS.HasCometBFT() { + suite.T().Skip("CometBFT is disabled, some methods can not be used, skip") + } +} + +func (suite *StateDbIntegrationTestSuite) newStateDB() corevm.StateDB { + return evmvm.NewStateDB( + suite.Ctx(), + suite.App().EvmKeeper(), + *suite.App().AccountKeeper(), + suite.App().BankKeeper(), + ) +} + +// Begin test + +func (suite *StateDbIntegrationTestSuite) TestNewStateDB() { + suite.Run("context was branched", func() { + // context should be branched to prevent any change to the original provided context + // The state transition should only be effective after the StateDB is committed + + account := suite.CITS.WalletAccounts.Number(1) + originalBalance := suite.CITS.QueryBalance(0, account.GetCosmosAddress().String()).Amount.Int64() + suite.Require().NotZero(originalBalance) + + stateDB := suite.newStateDB() + suite.Require().NotNil(stateDB) + + stateDB.AddBalance(account.GetEthAddress(), suite.CITS.NewBaseCoin(1).Amount.BigInt()) + + suite.Less( + originalBalance, + suite.CITS.ChainApp.BankKeeper().GetBalance(stateDB.(evmvm.CStateDB).ForTest_GetCurrentContext(), account.GetCosmosAddress(), suite.CStateDB.ForTest_GetEvmDenom()).Amount.Int64(), + "balance within StateDB context should be changed", + ) + + // DO NOT COMMIT + + suite.Equal( + originalBalance, suite.CITS.QueryBalance(0, account.GetCosmosAddress().String()).Amount.Int64(), + "balance should not be changed because the commit multi-store within the StateDB is not committed", + ) + }) + + suite.Len(suite.CStateDB.ForTest_GetSnapshots(), 1, "should have exactly one snapshot at the beginning") + suite.False(suite.CStateDB.ForTest_IsCommitted(), "committed flag should be false at the beginning") +} + +func (suite *StateDbIntegrationTestSuite) TestCreateAccount() { + suite.Run("create non-existing account and modified should be kept", func() { + testAcc := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.StateDB.CreateAccount(testAcc.GetEthAddress()) + suite.StateDB.AddBalance(testAcc.GetEthAddress(), big.NewInt(1000)) + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + + suite.CITS.WaitNextBlockOrCommit() + + suite.True( + suite.App().AccountKeeper().HasAccount(suite.Ctx(), testAcc.GetCosmosAddress()), + ) + }) + + suite.Run("create non-existing account but not modify will be removed", func() { + suite.SetupTest() // reset + + emptyWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.StateDB.CreateAccount(emptyWallet.GetEthAddress()) + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + + suite.CITS.WaitNextBlockOrCommit() + + suite.False( + suite.App().AccountKeeper().HasAccount(suite.Ctx(), emptyWallet.GetCosmosAddress()), + ) + }) + + suite.Run("remake existing account", func() { + suite.SetupTest() // reset + + existingWallet := suite.CITS.WalletAccounts.Number(1) + + existingAccount := suite.App().AccountKeeper().GetAccount(suite.CStateDB.ForTest_GetCurrentContext(), existingWallet.GetCosmosAddress()) + + suite.StateDB.CreateAccount(existingWallet.GetEthAddress()) + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + + suite.CITS.WaitNextBlockOrCommit() + + newAccount := suite.App().AccountKeeper().GetAccount(suite.Ctx(), existingWallet.GetCosmosAddress()) + suite.Require().NotNil(newAccount) + suite.Less(existingAccount.GetAccountNumber(), newAccount.GetAccountNumber()) + }) + + suite.Run("override existing account with balance", func() { + suite.SetupTest() // reset + + existingWallet := suite.CITS.WalletAccounts.Number(1) + + existingAccount := suite.App().AccountKeeper().GetAccount(suite.CStateDB.ForTest_GetCurrentContext(), existingWallet.GetCosmosAddress()) + + originalBalance := suite.App().BankKeeper().GetBalance(suite.Ctx(), existingAccount.GetAddress(), suite.CStateDB.ForTest_GetEvmDenom()) + suite.Require().False(originalBalance.IsZero()) + + suite.StateDB.CreateAccount(existingWallet.GetEthAddress()) + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + + suite.CITS.WaitNextBlockOrCommit() + + newAccount := suite.App().AccountKeeper().GetAccount(suite.Ctx(), existingWallet.GetCosmosAddress()) + suite.Require().NotNil(newAccount) + suite.Less(existingAccount.GetAccountNumber(), newAccount.GetAccountNumber()) + + newBalance := suite.App().BankKeeper().GetBalance(suite.Ctx(), newAccount.GetAddress(), suite.CStateDB.ForTest_GetEvmDenom()) + suite.Equal(originalBalance.Denom, newBalance.Denom, "balance should be brought to new account") + suite.Equal(originalBalance.Amount.String(), newBalance.Amount.String(), "balance should be brought to new account") + }) + + suite.Run("clear out previous account code and state", func() { + existingWallet1 := suite.CITS.WalletAccounts.Number(1) + existingWallet2 := suite.CITS.WalletAccounts.Number(2) + + existingAccount1 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), existingWallet1.GetCosmosAddress()) + suite.Require().NotNil(existingAccount1) + + existingAccount2 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), existingWallet2.GetCosmosAddress()) + suite.Require().NotNil(existingAccount2) + + // prepareByGoEthereum data + codeHash, code := RandomContractCode() + hash1 := evmvm.GenerateHash() + hash2 := evmvm.GenerateHash() + + suite.App().EvmKeeper().SetCode(suite.Ctx(), codeHash.Bytes(), code) + suite.App().EvmKeeper().SetCodeHash(suite.Ctx(), existingWallet1.GetEthAddress(), codeHash) + suite.App().EvmKeeper().SetState(suite.Ctx(), existingWallet1.GetEthAddress(), hash1, hash2.Bytes()) + suite.App().EvmKeeper().SetCodeHash(suite.Ctx(), existingWallet2.GetEthAddress(), codeHash) + suite.App().EvmKeeper().SetState(suite.Ctx(), existingWallet2.GetEthAddress(), hash1, hash2.Bytes()) + + suite.Commit() + + suite.Require().Equal(code, suite.App().EvmKeeper().GetCode(suite.Ctx(), codeHash)) + suite.Require().Equal(codeHash, suite.App().EvmKeeper().GetCodeHash(suite.Ctx(), existingAccount1.GetAddress())) + suite.Require().Equal(hash2, suite.App().EvmKeeper().GetState(suite.Ctx(), existingWallet1.GetEthAddress(), hash1)) + suite.Require().Equal(codeHash, suite.App().EvmKeeper().GetCodeHash(suite.Ctx(), existingAccount2.GetAddress())) + suite.Require().Equal(hash2, suite.App().EvmKeeper().GetState(suite.Ctx(), existingWallet2.GetEthAddress(), hash1)) + + suite.Commit() + + // remake one account + stateDB := suite.newStateDB() + stateDB.CreateAccount(existingWallet1.GetEthAddress()) + + err := stateDB.(evmvm.CStateDB).CommitMultiStore(true) + suite.Require().NoError(err) + suite.Commit() + + // ensure account data is wiped + codeHashPrevious := suite.App().EvmKeeper().GetCodeHash(suite.Ctx(), existingAccount1.GetAddress()) + suite.Equalf(common.BytesToHash(evmtypes.EmptyCodeHash), codeHashPrevious, "code hash of account must be wiped (cur %s, origin %s)", codeHashPrevious, codeHash) + suite.Equal(code, suite.App().EvmKeeper().GetCode(suite.Ctx(), codeHash), "mapped code must be kept") + accStatePrevious := suite.App().EvmKeeper().GetState(suite.Ctx(), existingWallet1.GetEthAddress(), hash1) + suite.Equalf(common.Hash{}, accStatePrevious, "account state must be wiped (cur %s, origin %s)", accStatePrevious, hash2) + + // ensure new account has empty data + newAccount := suite.App().AccountKeeper().GetAccount(suite.Ctx(), existingWallet1.GetCosmosAddress()) + suite.Equal(common.BytesToHash(evmtypes.EmptyCodeHash), suite.App().EvmKeeper().GetCodeHash(suite.Ctx(), newAccount.GetAddress()), "code hash of new account must be empty") + suite.Equal(common.Hash{}, suite.App().EvmKeeper().GetState(suite.Ctx(), common.BytesToAddress(newAccount.GetAddress()), hash1), "new account state must be empty") + + // ensure another account has state un-changed + suite.Require().Equal(codeHash.String(), suite.App().EvmKeeper().GetCodeHash(suite.Ctx(), existingAccount2.GetAddress()).String()) + suite.Require().Equal(hash2.String(), suite.App().EvmKeeper().GetState(suite.Ctx(), existingWallet2.GetEthAddress(), hash1).String()) + }) +} + +func (suite *StateDbIntegrationTestSuite) TestDestroyAccount() { + curCtx := suite.CStateDB.ForTest_GetCurrentContext() + + moduleAccount := suite.App().AccountKeeper().GetModuleAccount(curCtx, authtypes.FeeCollectorName) + suite.Require().NotNil(moduleAccount) + suite.Panics(func() { + suite.CStateDB.DestroyAccount(common.BytesToAddress(moduleAccount.GetAddress())) + }, "should not destroy module account") + + originalVestingCoin := suite.CITS.NewBaseCoin(1_000_000) + startVesting := time.Now().UTC().Unix() - 86400*10 + endVesting := time.Now().UTC().Unix() + 86400*10 + + walletVesting := integration_test_util.NewTestAccount(suite.T(), nil) + vestingAccountNotExpired, err := vestingtypes.NewContinuousVestingAccount( + suite.App().AccountKeeper().NewAccountWithAddress(curCtx, walletVesting.GetCosmosAddress()).(*authtypes.BaseAccount), + sdk.NewCoins(originalVestingCoin), + startVesting, + endVesting, + ) + suite.Require().NoError(err) + suite.App().AccountKeeper().SetAccount(curCtx, vestingAccountNotExpired) + vestingAccountINotExpired := suite.App().AccountKeeper().GetAccount(curCtx, walletVesting.GetCosmosAddress()) + + suite.Panics(func() { + suite.CStateDB.DestroyAccount(common.BytesToAddress(vestingAccountINotExpired.GetAddress())) + }, "should not destroy vesting account which still not expired") + + endVesting = time.Now().Unix() - 1 + vestingAccountExpired, err := vestingtypes.NewContinuousVestingAccount( + suite.App().AccountKeeper().NewAccountWithAddress(curCtx, walletVesting.GetCosmosAddress()).(*authtypes.BaseAccount), + sdk.NewCoins(originalVestingCoin), + startVesting, + endVesting, + ) + suite.Require().NoError(err) + suite.App().AccountKeeper().SetAccount(curCtx, vestingAccountExpired) + vestingAccountIExpired := suite.App().AccountKeeper().GetAccount(curCtx, walletVesting.GetCosmosAddress()) + + suite.NotPanics(func() { + suite.CStateDB.DestroyAccount(common.BytesToAddress(vestingAccountIExpired.GetAddress())) + }, "able to destroy vesting account which expired") + + existingWallet := suite.CITS.WalletAccounts.Number(1) + suite.Require().NotNil( + suite.App().AccountKeeper().GetAccount(curCtx, existingWallet.GetCosmosAddress()), + "must exists", + ) + { + // add balance, code hash, and state + suite.StateDB.AddBalance(existingWallet.GetEthAddress(), big.NewInt(1)) + suite.Require().False(suite.App().BankKeeper().GetAllBalances(curCtx, existingWallet.GetCosmosAddress()).IsZero()) + suite.StateDB.SetCode(existingWallet.GetEthAddress(), []byte{1, 2, 3}) + suite.StateDB.SetState(existingWallet.GetEthAddress(), common.BytesToHash([]byte{1, 2, 3}), common.BytesToHash([]byte{4, 5, 6})) + } + + suite.NotPanics(func() { + suite.CStateDB.DestroyAccount(existingWallet.GetEthAddress()) + }, "able to destroy normal account") + + err = suite.CStateDB.CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + suite.Commit() + + suite.Nil(suite.App().AccountKeeper().GetAccount(suite.Ctx(), walletVesting.GetCosmosAddress())) + suite.Nil(suite.App().AccountKeeper().GetAccount(suite.Ctx(), existingWallet.GetCosmosAddress())) + suite.Require().Empty(suite.App().BankKeeper().GetAllBalances(curCtx, existingWallet.GetCosmosAddress())) + suite.Equal(common.Hash{}, suite.App().EvmKeeper().GetCodeHash(curCtx, existingWallet.GetEthAddress().Bytes())) + suite.Equal(common.Hash{}, suite.App().EvmKeeper().GetState(curCtx, existingWallet.GetEthAddress(), common.BytesToHash([]byte{1, 2, 3}))) +} + +func (suite *StateDbIntegrationTestSuite) TestAddSubGetBalance() { + receiver1 := integration_test_util.NewTestAccount(suite.T(), nil) + receiver2 := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(300)) + suite.StateDB.AddBalance(receiver2.GetEthAddress(), big.NewInt(900)) + + suite.StateDB.SubBalance(receiver1.GetEthAddress(), big.NewInt(100)) + + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(300)) + + suite.StateDB.SubBalance(receiver1.GetEthAddress(), big.NewInt(150)) + + suite.Require().Equal(big.NewInt(350), suite.StateDB.GetBalance(receiver1.GetEthAddress())) + suite.Require().Equal(big.NewInt(900), suite.StateDB.GetBalance(receiver2.GetEthAddress())) + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + + suite.CITS.WaitNextBlockOrCommit() + + suite.Equal(int64(350), suite.CITS.QueryBalance(0, receiver1.GetCosmosAddress().String()).Amount.Int64()) + suite.Equal(int64(900), suite.CITS.QueryBalance(0, receiver2.GetCosmosAddress().String()).Amount.Int64()) +} + +func (suite *StateDbIntegrationTestSuite) TestAddBalance() { + receiver1 := integration_test_util.NewTestAccount(suite.T(), nil) + receiver2 := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(300)) + suite.StateDB.AddBalance(receiver2.GetEthAddress(), big.NewInt(900)) + + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(300)) + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + + suite.CITS.WaitNextBlockOrCommit() + + suite.Equal(int64(600), suite.CITS.QueryBalance(0, receiver1.GetCosmosAddress().String()).Amount.Int64()) + suite.Equal(int64(900), suite.CITS.QueryBalance(0, receiver2.GetCosmosAddress().String()).Amount.Int64()) + + suite.Run("add balance for non-existing account", func() { + suite.SetupTest() // reset + + nonExistingAccount := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.StateDB.AddBalance(nonExistingAccount.GetEthAddress(), big.NewInt(300)) + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + + suite.CITS.WaitNextBlockOrCommit() + + suite.Equal(int64(300), suite.CITS.QueryBalance(0, nonExistingAccount.GetCosmosAddress().String()).Amount.Int64()) + }) + + suite.Run("after received balance, account should be created", func() { + suite.SetupTest() // reset + + nonExistingAccount := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.StateDB.AddBalance(nonExistingAccount.GetEthAddress(), big.NewInt(300)) + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + + suite.CITS.WaitNextBlockOrCommit() + + suite.Equal(int64(300), suite.CITS.QueryBalance(0, nonExistingAccount.GetCosmosAddress().String()).Amount.Int64()) + + suite.NotNil(suite.App().AccountKeeper().GetAccount(suite.Ctx(), nonExistingAccount.GetCosmosAddress())) + }) + + /* no longer happens because the input is now uint256 + suite.Run("add negative value", func() { + suite.SetupTest() // reset + suite.Require().Panics(func() { + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(-1)) + }) + }) + + suite.Run("add negative value", func() { + suite.SetupTest() // reset + suite.Require().Panics(func() { + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(-100)) + }) + }) + */ +} + +func (suite *StateDbIntegrationTestSuite) TestSubBalance() { + receiver1 := integration_test_util.NewTestAccount(suite.T(), nil) + receiver2 := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.CITS.MintCoin(receiver1, sdk.NewCoin(suite.CStateDB.ForTest_GetEvmDenom(), sdkmath.NewInt(1000))) + suite.CITS.MintCoin(receiver2, sdk.NewCoin(suite.CStateDB.ForTest_GetEvmDenom(), sdkmath.NewInt(1000))) + + suite.Commit() + + suite.StateDB.SubBalance(receiver1.GetEthAddress(), big.NewInt(100)) + suite.StateDB.SubBalance(receiver2.GetEthAddress(), big.NewInt(100)) + + suite.StateDB.SubBalance(receiver1.GetEthAddress(), big.NewInt(150)) + + suite.Require().Equal(big.NewInt(750), suite.StateDB.GetBalance(receiver1.GetEthAddress())) + suite.Require().Equal(big.NewInt(900), suite.StateDB.GetBalance(receiver2.GetEthAddress())) + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + + suite.CITS.WaitNextBlockOrCommit() + + suite.Equal(int64(750), suite.CITS.QueryBalance(0, receiver1.GetCosmosAddress().String()).Amount.Int64()) + suite.Equal(int64(900), suite.CITS.QueryBalance(0, receiver2.GetCosmosAddress().String()).Amount.Int64()) + + /* no longer happens because the input is now uint256 + suite.Run("sub negative value", func() { + suite.SetupTest() // reset + suite.Require().Panics(func() { + suite.StateDB.SubBalance(receiver1.GetEthAddress(), big.NewInt(-1)) + }) + }) + + suite.Run("sub negative value", func() { + suite.SetupTest() // reset + suite.Require().Panics(func() { + suite.StateDB.SubBalance(receiver1.GetEthAddress(), big.NewInt(-100)) + }) + }) + */ + + suite.Run("sub more than balance", func() { + suite.SetupTest() // reset + + wallet := suite.CITS.WalletAccounts.Number(1) + originalBalance := suite.CITS.QueryBalance(0, wallet.GetCosmosAddress().String()).Amount.BigInt() + suite.Require().Greater(originalBalance.Uint64(), uint64(0)) + + suite.Require().Panics(func() { + suite.StateDB.SubBalance(wallet.GetEthAddress(), new(big.Int).Add(originalBalance, big.NewInt(1))) + }) + }) + + suite.Run("sub exact balance", func() { + suite.SetupTest() // reset + + wallet := suite.CITS.WalletAccounts.Number(2) + originalBalance := suite.CITS.QueryBalance(0, wallet.GetCosmosAddress().String()).Amount.BigInt() + suite.Require().Greater(originalBalance.Uint64(), uint64(0)) + + suite.Require().NotPanics(func() { + suite.StateDB.SubBalance(wallet.GetEthAddress(), originalBalance) + }) + + suite.Zero(suite.StateDB.GetBalance(wallet.GetEthAddress()).Uint64()) + }) + + suite.Run("can not sub more than unlocked coins on vesting account", func() { + suite.SetupTest() // reset + + wallet1 := integration_test_util.NewTestAccount(suite.T(), nil) + wallet2 := integration_test_util.NewTestAccount(suite.T(), nil) + + originalVestingCoin := suite.CITS.NewBaseCoin(1_000_000) + startVesting := time.Now().UTC().Unix() - 86400*10 + endVesting := time.Now().UTC().Unix() + 86400*10 + + baseAccount1 := suite.App().AccountKeeper().NewAccountWithAddress(suite.Ctx(), wallet1.GetCosmosAddress()) + vestingAccount1, err := vestingtypes.NewContinuousVestingAccount( + baseAccount1.(*authtypes.BaseAccount), + sdk.NewCoins(originalVestingCoin), + startVesting, + endVesting, + ) + suite.Require().NoError(err) + + baseAccount2 := suite.App().AccountKeeper().NewAccountWithAddress(suite.Ctx(), wallet2.GetCosmosAddress()) + vestingAccount2, err := vestingtypes.NewContinuousVestingAccount( + baseAccount2.(*authtypes.BaseAccount), + sdk.NewCoins(originalVestingCoin), + startVesting, + endVesting, + ) + suite.Require().NoError(err) + + suite.App().AccountKeeper().SetAccount(suite.Ctx(), vestingAccount1) + suite.App().AccountKeeper().SetAccount(suite.Ctx(), vestingAccount2) + + suite.CITS.MintCoin(wallet1, originalVestingCoin) + suite.CITS.MintCoin(wallet2, originalVestingCoin) + + suite.Commit() + suite.Commit() + + lockedCoins1 := vestingAccount1.LockedCoins(suite.Ctx().BlockTime()) + suite.Require().True(lockedCoins1.IsAllPositive()) + suite.Require().True(lockedCoins1[0].IsLT(originalVestingCoin)) + suite.Require().Len(lockedCoins1, 1) + lockedCoins2 := vestingAccount2.LockedCoins(suite.Ctx().BlockTime()) + suite.Require().True(lockedCoins2.IsAllPositive()) + suite.Require().True(lockedCoins2[0].IsLT(originalVestingCoin)) + suite.Require().Len(lockedCoins2, 1) + + unlockedCoin1 := originalVestingCoin.Sub(lockedCoins1[0]) + suite.Require().True(unlockedCoin1.IsPositive()) + unlockedCoin2 := originalVestingCoin.Sub(lockedCoins2[0]) + suite.Require().True(unlockedCoin2.IsPositive()) + + stateDB := suite.newStateDB() + + // can sub entirely unlocked coins + suite.Require().NotPanics(func() { + stateDB.SubBalance(wallet1.GetEthAddress(), unlockedCoin1.Amount.BigInt()) + }) + + // can not sub more than unlocked coins + suite.Require().Panics(func() { + stateDB.SubBalance(wallet2.GetEthAddress(), unlockedCoin2.Amount.AddRaw(1).BigInt()) + }) + }) +} + +func (suite *StateDbIntegrationTestSuite) TestAddSubBalanceWithRevertBehavior() { + receiver1 := integration_test_util.NewTestAccount(suite.T(), nil) + receiver2 := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(1000)) + suite.StateDB.AddBalance(receiver2.GetEthAddress(), big.NewInt(1000)) + + suite.Require().Equal(big.NewInt(1000), suite.StateDB.GetBalance(receiver1.GetEthAddress())) + suite.Require().Equal(big.NewInt(1000), suite.StateDB.GetBalance(receiver2.GetEthAddress())) + + sid0 := suite.StateDB.Snapshot() + suite.Require().Equal(0, sid0) + + suite.StateDB.SubBalance(receiver1.GetEthAddress(), big.NewInt(500)) + suite.StateDB.SubBalance(receiver2.GetEthAddress(), big.NewInt(500)) + + balance1AtCheckpoint := suite.StateDB.GetBalance(receiver1.GetEthAddress()) + balance2AtCheckpoint := suite.StateDB.GetBalance(receiver2.GetEthAddress()) + suite.Require().Equal(big.NewInt(500), balance1AtCheckpoint) + suite.Require().Equal(big.NewInt(500), balance2AtCheckpoint) + + sidAtCheckpoint := suite.StateDB.Snapshot() + suite.Require().Equal(1, sidAtCheckpoint) + + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(4500)) + suite.StateDB.AddBalance(receiver2.GetEthAddress(), big.NewInt(4500)) + + suite.Require().Equal(big.NewInt(5000), suite.StateDB.GetBalance(receiver1.GetEthAddress())) + suite.Require().Equal(big.NewInt(5000), suite.StateDB.GetBalance(receiver2.GetEthAddress())) + + sid2 := suite.StateDB.Snapshot() + suite.Require().Equal(2, sid2) + + suite.StateDB.SubBalance(receiver1.GetEthAddress(), big.NewInt(2000)) + suite.StateDB.SubBalance(receiver2.GetEthAddress(), big.NewInt(2000)) + + suite.Require().Equal(big.NewInt(3000), suite.StateDB.GetBalance(receiver1.GetEthAddress())) + suite.Require().Equal(big.NewInt(3000), suite.StateDB.GetBalance(receiver2.GetEthAddress())) + + sid3 := suite.StateDB.Snapshot() + suite.Require().Equal(3, sid3) + + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(7000)) + suite.StateDB.AddBalance(receiver2.GetEthAddress(), big.NewInt(7000)) + + suite.Require().Equal(big.NewInt(10000), suite.StateDB.GetBalance(receiver1.GetEthAddress())) + suite.Require().Equal(big.NewInt(10000), suite.StateDB.GetBalance(receiver2.GetEthAddress())) + + sid4 := suite.StateDB.Snapshot() + suite.Require().Equal(4, sid4) + + suite.StateDB.RevertToSnapshot(sidAtCheckpoint) // revert state to checkpoint + + // just for fun + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(8888)) + suite.StateDB.AddBalance(receiver2.GetEthAddress(), big.NewInt(9999)) + + err := suite.CStateDB.CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + + suite.CITS.WaitNextBlockOrCommit() + + suite.Equal(balance1AtCheckpoint.Uint64()+8888, suite.CITS.QueryBalance(0, receiver1.GetCosmosAddress().String()).Amount.Uint64()) + suite.Equal(balance2AtCheckpoint.Uint64()+9999, suite.CITS.QueryBalance(0, receiver2.GetCosmosAddress().String()).Amount.Uint64()) +} + +func (suite *StateDbIntegrationTestSuite) TestGetBalance() { + wallet1 := suite.CITS.WalletAccounts.Number(1) + wallet2 := suite.CITS.WalletAccounts.Number(2) + + suite.CITS.MintCoin(wallet1, suite.CITS.NewBaseCoin(100)) + + suite.Commit() + + balance1 := suite.App().BankKeeper().GetBalance(suite.Ctx(), wallet1.GetCosmosAddress(), suite.CStateDB.ForTest_GetEvmDenom()) + suite.Require().True(balance1.IsPositive()) + + balance2 := suite.App().BankKeeper().GetBalance(suite.Ctx(), wallet2.GetCosmosAddress(), suite.CStateDB.ForTest_GetEvmDenom()) + suite.Require().True(balance2.IsPositive()) + + suite.Require().Equal(balance1.Amount.BigInt().String(), suite.StateDB.GetBalance(wallet1.GetEthAddress()).String()) + suite.Require().Equal(balance2.Amount.BigInt().String(), suite.StateDB.GetBalance(wallet2.GetEthAddress()).String()) +} + +func (suite *StateDbIntegrationTestSuite) TestGetNonce() { + wallet := suite.CITS.WalletAccounts.Number(1) + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + const cosmosSequence = 99 + + // set Cosmos sequence + accountWallet := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet.GetCosmosAddress()) + suite.Require().Equal(uint64(0), accountWallet.GetSequence()) + err := accountWallet.SetSequence(cosmosSequence) + suite.Require().NoError(err) + suite.App().AccountKeeper().SetAccount(suite.Ctx(), accountWallet) + + suite.Commit() + + accountWallet = suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet.GetCosmosAddress()) + suite.Require().Equal(uint64(cosmosSequence), accountWallet.GetSequence(), "sequence must be changed") + suite.Require().Equal(uint64(cosmosSequence), suite.App().EvmKeeper().GetNonce(suite.Ctx(), wallet.GetEthAddress()), "sequence must be changed") + + stateDB := suite.newStateDB() + suite.Equal(uint64(cosmosSequence), stateDB.GetNonce(wallet.GetEthAddress())) + suite.Equal(uint64(0), stateDB.GetNonce(nonExistsWallet.GetEthAddress())) +} + +func (suite *StateDbIntegrationTestSuite) TestSetNonce() { + wallet := suite.CITS.WalletAccounts.Number(1) + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.Commit() + + const cosmosSequence1 = 99 + const cosmosSequence2 = 88 + + stateDB := suite.newStateDB() + + stateDB.SetNonce(wallet.GetEthAddress(), cosmosSequence1) + stateDB.SetNonce(nonExistsWallet.GetEthAddress(), cosmosSequence2) + + err := stateDB.(evmvm.CStateDB).CommitMultiStore(true) + suite.Require().NoError(err) + + suite.Commit() + + accountWallet := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet.GetCosmosAddress()) + suite.Equal(uint64(cosmosSequence1), accountWallet.GetSequence()) + suite.Equal(uint64(cosmosSequence1), suite.App().EvmKeeper().GetNonce(suite.Ctx(), wallet.GetEthAddress())) + + nonExistsAccount := suite.App().AccountKeeper().GetAccount(suite.Ctx(), nonExistsWallet.GetCosmosAddress()) + suite.NotNil(nonExistsAccount, "account should be created") + suite.Equal(uint64(cosmosSequence2), nonExistsAccount.GetSequence()) + suite.Equal(uint64(cosmosSequence2), suite.App().EvmKeeper().GetNonce(suite.Ctx(), nonExistsWallet.GetEthAddress())) +} + +func (suite *StateDbIntegrationTestSuite) TestGetCodeHash() { + wallet := suite.CITS.WalletAccounts.Number(1) + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.Equal(common.BytesToHash(evmtypes.EmptyCodeHash), suite.StateDB.GetCodeHash(wallet.GetEthAddress()), "code hash of non-contract should be empty") + suite.Equal(common.Hash{}, suite.StateDB.GetCodeHash(nonExistsWallet.GetEthAddress()), "code hash of non-contract should be empty") + + codeHash, code := RandomContractCode() + fmt.Println("generated contract code hash:", codeHash.Hex(), "content:", hex.EncodeToString(code)) + + contract := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet.GetCosmosAddress()) + + suite.App().EvmKeeper().SetCode(suite.Ctx(), codeHash.Bytes(), code) + suite.App().EvmKeeper().SetCodeHash(suite.Ctx(), common.BytesToAddress(contract.GetAddress()), codeHash) + + suite.Commit() + + stateDB := suite.newStateDB() + + suite.Equal(codeHash, stateDB.GetCodeHash(common.BytesToAddress(contract.GetAddress().Bytes())), "code hash of contract should be changed") + suite.Equal(common.Hash{}, stateDB.GetCodeHash(nonExistsWallet.GetEthAddress()), "code hash of non-contract should be empty") +} + +func (suite *StateDbIntegrationTestSuite) TestGetCode() { + wallet := suite.CITS.WalletAccounts.Number(1) + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.Empty(suite.StateDB.GetCode(wallet.GetEthAddress()), "code of non-contract should be empty") + suite.Empty(suite.StateDB.GetCode(nonExistsWallet.GetEthAddress()), "code of non-exists-account should be empty") + + codeHash, code := RandomContractCode() + + contract := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet.GetCosmosAddress()) + + suite.App().EvmKeeper().SetCode(suite.Ctx(), codeHash.Bytes(), code) + suite.App().EvmKeeper().SetCodeHash(suite.Ctx(), common.BytesToAddress(contract.GetAddress()), codeHash) + + suite.Commit() + + stateDB := suite.newStateDB() + + suite.Equal(code, stateDB.GetCode(common.BytesToAddress(contract.GetAddress().Bytes())), "code of contract should be changed") + suite.Empty(stateDB.GetCode(nonExistsWallet.GetEthAddress()), "code of non-contract should be empty") +} + +func (suite *StateDbIntegrationTestSuite) TestSetCode() { + wallet := suite.CITS.WalletAccounts.Number(1) + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.Empty(suite.StateDB.GetCode(wallet.GetEthAddress()), "code of non-contract should be empty") + suite.Empty(suite.StateDB.GetCode(nonExistsWallet.GetEthAddress()), "code of non-exists-account should be empty") + + codeHash1, code1 := RandomContractCode() + codeHash2, code2 := RandomContractCode() + + contract1 := suite.App().AccountKeeper().GetAccount(suite.CStateDB.ForTest_GetCurrentContext(), wallet.GetCosmosAddress()) + + suite.StateDB.SetCode(common.BytesToAddress(contract1.GetAddress().Bytes()), code1) + suite.StateDB.SetCode(nonExistsWallet.GetEthAddress(), code2) + + contract2WasNotExistsBefore := suite.App().AccountKeeper().GetAccount(suite.CStateDB.ForTest_GetCurrentContext(), nonExistsWallet.GetCosmosAddress()) // contract created from void + suite.Require().NotNil(contract2WasNotExistsBefore, "contract should be created") + + err := suite.CStateDB.CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + + suite.Commit() + + suite.Equal(codeHash1, suite.App().EvmKeeper().GetCodeHash(suite.Ctx(), contract1.GetAddress()), "code hash mis-match") + suite.Equal(code1, suite.App().EvmKeeper().GetCode(suite.Ctx(), codeHash1), "code mis-match") + + suite.Equal(codeHash2, suite.App().EvmKeeper().GetCodeHash(suite.Ctx(), contract2WasNotExistsBefore.GetAddress()), "code hash mis-match") + suite.Equal(code2, suite.App().EvmKeeper().GetCode(suite.Ctx(), codeHash2), "code mis-match") + + suite.NotNil(suite.App().AccountKeeper().GetAccount(suite.Ctx(), contract2WasNotExistsBefore.GetAddress()), "contract should be created") +} + +func (suite *StateDbIntegrationTestSuite) TestGetCodeSize() { + wallet := suite.CITS.WalletAccounts.Number(1) + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.Zero(suite.StateDB.GetCodeSize(wallet.GetEthAddress()), "code size of non-contract should be 0") + suite.Zero(suite.StateDB.GetCodeSize(nonExistsWallet.GetEthAddress()), "code size of non-exists account should be 0") + + _, code1 := RandomContractCode() + suite.Require().NotEmpty(code1) + _, code2 := RandomContractCode() + suite.Require().NotEmpty(code2) + + contract1 := wallet + contract2WasNotExistsBefore := nonExistsWallet // create contract from void + + suite.StateDB.SetCode(contract1.GetEthAddress(), code1) + suite.StateDB.SetCode(contract2WasNotExistsBefore.GetEthAddress(), code2) + + err := suite.CStateDB.CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + + suite.Commit() + + stateDB := suite.newStateDB() + suite.Equal(len(code1), stateDB.GetCodeSize(contract1.GetEthAddress())) + suite.Equal(len(code2), stateDB.GetCodeSize(contract2WasNotExistsBefore.GetEthAddress())) +} + +func (suite *StateDbIntegrationTestSuite) TestGetRefund() { + suite.CStateDB.AddRefund(1000) + + suite.Equal(uint64(1000), suite.StateDB.GetRefund()) + + suite.CStateDB.AddRefund(1000) + + suite.Equal(uint64(2000), suite.StateDB.GetRefund()) + + suite.StateDB.AddRefund(500) + + suite.Equal(uint64(2500), suite.StateDB.GetRefund()) +} + +func (suite *StateDbIntegrationTestSuite) TestAddRefund() { + suite.CStateDB.AddRefund(1000) + + suite.StateDB.AddRefund(500) + suite.StateDB.AddRefund(500) + + suite.Equal(uint64(2000), suite.StateDB.GetRefund()) +} + +func (suite *StateDbIntegrationTestSuite) TestSubRefund() { + suite.CStateDB.AddRefund(2000) + + suite.StateDB.SubRefund(500) + suite.StateDB.SubRefund(500) + + suite.Equal(uint64(1000), suite.StateDB.GetRefund()) +} + +func (suite *StateDbIntegrationTestSuite) TestGetCommittedState() { + wallet1 := suite.CITS.WalletAccounts.Number(1) + wallet2 := suite.CITS.WalletAccounts.Number(2) + wallet3WithoutState := suite.CITS.WalletAccounts.Number(3) + wallet4 := suite.CITS.WalletAccounts.Number(4) + + account1 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet1.GetCosmosAddress()) + suite.Require().NotNil(account1) + account2 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet2.GetCosmosAddress()) + suite.Require().NotNil(account2) + account3WithoutState := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet3WithoutState.GetCosmosAddress()) + suite.Require().NotNil(account3WithoutState) + account4 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet4.GetCosmosAddress()) + suite.Require().NotNil(account4) + + hash1 := evmvm.GenerateHash() + hash2 := evmvm.GenerateHash() + + suite.App().EvmKeeper().SetState(suite.Ctx(), wallet1.GetEthAddress(), hash1, hash2.Bytes()) + suite.App().EvmKeeper().SetState(suite.Ctx(), wallet2.GetEthAddress(), hash1, hash2.Bytes()) + suite.App().EvmKeeper().SetState(suite.Ctx(), wallet4.GetEthAddress(), hash1, hash2.Bytes()) + + suite.Commit() + + stateDB := suite.newStateDB() + + suite.Equal(hash2, stateDB.GetCommittedState(wallet1.GetEthAddress(), hash1)) + suite.Equal(hash2, stateDB.GetCommittedState(wallet2.GetEthAddress(), hash1)) + suite.Equal(common.Hash{}, stateDB.GetCommittedState(wallet3WithoutState.GetEthAddress(), hash1)) + suite.Equal(hash2, stateDB.GetCommittedState(wallet4.GetEthAddress(), hash1)) + + suite.Run("should returns state in original context, not current one", func() { + suite.App().EvmKeeper().SetState(stateDB.(evmvm.CStateDB).ForTest_GetCurrentContext(), wallet2.GetEthAddress(), hash1, evmvm.GenerateHash().Bytes()) + + suite.Equal(hash2, stateDB.GetCommittedState(wallet2.GetEthAddress(), hash1)) + }) + + suite.Run("should returns empty state for deleted account no matter store data still exists", func() { + currentCtx := stateDB.(evmvm.CStateDB).ForTest_GetCurrentContext() + + getCommittedState := func() common.Hash { + return stateDB.GetCommittedState(wallet2.GetEthAddress(), hash1) + } + + suite.NotEqual(common.Hash{}, getCommittedState()) + + accountI2 := suite.App().AccountKeeper().GetAccount(currentCtx, wallet2.GetCosmosAddress()) + suite.Require().NotNil(accountI2) + suite.App().AccountKeeper().RemoveAccount(currentCtx, accountI2) + + suite.Equal(common.Hash{}, getCommittedState()) + }) + + suite.Run("get state of new account of re-made", func() { + currentCtx := stateDB.(evmvm.CStateDB).ForTest_GetCurrentContext() + + getCommittedState := func() common.Hash { + return stateDB.GetCommittedState(wallet4.GetEthAddress(), hash1) + } + + suite.NotEqual(common.Hash{}, getCommittedState()) + + accountI4 := suite.App().AccountKeeper().GetAccount(currentCtx, wallet4.GetCosmosAddress()) + suite.Require().NotNil(accountI4) + + stateDB.CreateAccount(wallet4.GetEthAddress()) + suite.Equal(common.Hash{}, getCommittedState(), "state of remade account should be empty") + + // reload accountI to fetch new account number + accountI4 = suite.App().AccountKeeper().GetAccount(currentCtx, wallet4.GetCosmosAddress()) + suite.Require().NotNil(accountI4) + + suite.App().EvmKeeper().SetState(currentCtx, wallet4.GetEthAddress(), hash1, evmvm.GenerateHash().Bytes()) + suite.Equal(common.Hash{}, getCommittedState(), "state of remade account should be empty regardless state in current un-persisted context") + }) + + suite.Run("get state of new account", func() { + currentCtx := stateDB.(evmvm.CStateDB).ForTest_GetCurrentContext() + + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + getCommittedState := func() common.Hash { + return stateDB.GetCommittedState(nonExistsWallet.GetEthAddress(), hash1) + } + + suite.Require().Equal(common.Hash{}, getCommittedState(), "state of non-existence account should be empty") + + stateDB.CreateAccount(nonExistsWallet.GetEthAddress()) + suite.Require().Equal(common.Hash{}, getCommittedState(), "state of new account should be empty") + + accountI := suite.App().AccountKeeper().GetAccount(currentCtx, nonExistsWallet.GetCosmosAddress()) + suite.Require().NotNil(accountI) + + suite.App().EvmKeeper().SetState(currentCtx, nonExistsWallet.GetEthAddress(), hash1, evmvm.GenerateHash().Bytes()) + suite.Equal(common.Hash{}, getCommittedState(), "state of new account should be empty regardless state in current un-persisted context") + }) +} + +func (suite *StateDbIntegrationTestSuite) TestGetState() { + wallet1 := suite.CITS.WalletAccounts.Number(1) + wallet2 := suite.CITS.WalletAccounts.Number(2) + wallet3WithoutState := suite.CITS.WalletAccounts.Number(3) + wallet4 := suite.CITS.WalletAccounts.Number(4) + + account1 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet1.GetCosmosAddress()) + suite.Require().NotNil(account1) + account2 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet2.GetCosmosAddress()) + suite.Require().NotNil(account2) + account3WithoutState := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet3WithoutState.GetCosmosAddress()) + suite.Require().NotNil(account3WithoutState) + account4 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet4.GetCosmosAddress()) + suite.Require().NotNil(account4) + + hash1 := evmvm.GenerateHash() + hash2 := evmvm.GenerateHash() + + suite.App().EvmKeeper().SetState(suite.Ctx(), wallet1.GetEthAddress(), hash1, hash2.Bytes()) + suite.App().EvmKeeper().SetState(suite.Ctx(), wallet2.GetEthAddress(), hash1, hash2.Bytes()) + suite.App().EvmKeeper().SetState(suite.Ctx(), wallet4.GetEthAddress(), hash1, hash2.Bytes()) + + suite.Commit() + + stateDB := suite.newStateDB() + + suite.Equal(hash2, stateDB.GetState(wallet1.GetEthAddress(), hash1)) + suite.Equal(hash2, stateDB.GetState(wallet2.GetEthAddress(), hash1)) + suite.Equal(common.Hash{}, stateDB.GetState(wallet3WithoutState.GetEthAddress(), hash1)) + suite.Equal(hash2, stateDB.GetState(wallet4.GetEthAddress(), hash1)) + + suite.Run("should returns state of current context, not original one", func() { + newState := evmvm.GenerateHash() + suite.App().EvmKeeper().SetState(stateDB.(evmvm.CStateDB).ForTest_GetCurrentContext(), wallet2.GetEthAddress(), hash1, newState.Bytes()) + + suite.Equal(newState, stateDB.GetState(wallet2.GetEthAddress(), hash1)) + suite.NotEqual(hash2, stateDB.GetState(wallet2.GetEthAddress(), hash1)) + }) + + /* This test was disabled because this implementation only persist state by address so previous state will being maintained + suite.Run("should returns empty state for deleted account no matter store data still exists", func() { + currentCtx := stateDB.(xethvm.CStateDB).ForTest_GetCurrentContext() + + getState := func() common.Hash { + return stateDB.GetState(wallet2.GetEthAddress(), hash1) + } + + suite.NotEqual(common.Hash{}, getState()) + + accountI2 := suite.App().AccountKeeper().GetAccount(currentCtx, wallet2.GetCosmosAddress()) + suite.Require().NotNil(accountI2) + suite.App().AccountKeeper().RemoveAccount(currentCtx, accountI2) + + suite.Equal(common.Hash{}, getState()) + }) + */ + + suite.Run("get state of new account of re-made", func() { + currentCtx := stateDB.(evmvm.CStateDB).ForTest_GetCurrentContext() + + getState := func() common.Hash { + return stateDB.GetState(wallet4.GetEthAddress(), hash1) + } + + suite.NotEqual(common.Hash{}, getState()) + + accountI4 := suite.App().AccountKeeper().GetAccount(currentCtx, wallet4.GetCosmosAddress()) + suite.Require().NotNil(accountI4) + + stateDB.CreateAccount(wallet4.GetEthAddress()) + suite.Equal(common.Hash{}, getState(), "state of remade account should be empty") + + // reload accountI to fetch new account number + accountI4 = suite.App().AccountKeeper().GetAccount(currentCtx, wallet4.GetCosmosAddress()) + suite.Require().NotNil(accountI4) + + newState := evmvm.GenerateHash() + suite.App().EvmKeeper().SetState(currentCtx, wallet4.GetEthAddress(), hash1, newState.Bytes()) + suite.Equal(newState, getState(), "state of remade account should be the dirty state") + }) + + suite.Run("get state of new account", func() { + currentCtx := stateDB.(evmvm.CStateDB).ForTest_GetCurrentContext() + + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + getState := func() common.Hash { + return stateDB.GetState(nonExistsWallet.GetEthAddress(), hash1) + } + + suite.Require().Equal(common.Hash{}, getState(), "state of non-existence account should be empty") + + stateDB.CreateAccount(nonExistsWallet.GetEthAddress()) + suite.Require().Equal(common.Hash{}, getState(), "state of new account should be empty") + + accountI := suite.App().AccountKeeper().GetAccount(currentCtx, nonExistsWallet.GetCosmosAddress()) + suite.Require().NotNil(accountI) + + newState := evmvm.GenerateHash() + suite.App().EvmKeeper().SetState(currentCtx, nonExistsWallet.GetEthAddress(), hash1, newState.Bytes()) + suite.Equal(newState, getState(), "state of new account should be the dirty state") + }) +} + +func (suite *StateDbIntegrationTestSuite) TestSetState() { + wallet1 := suite.CITS.WalletAccounts.Number(1) + wallet2 := suite.CITS.WalletAccounts.Number(2) + wallet3WithoutState := suite.CITS.WalletAccounts.Number(3) + wallet4 := suite.CITS.WalletAccounts.Number(4) + + account1 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet1.GetCosmosAddress()) + suite.Require().NotNil(account1) + account2 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet2.GetCosmosAddress()) + suite.Require().NotNil(account2) + account3WithoutState := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet3WithoutState.GetCosmosAddress()) + suite.Require().NotNil(account3WithoutState) + account4 := suite.App().AccountKeeper().GetAccount(suite.Ctx(), wallet4.GetCosmosAddress()) + suite.Require().NotNil(account4) + + hash1 := evmvm.GenerateHash() + hash2 := evmvm.GenerateHash() + + suite.StateDB.SetState(wallet1.GetEthAddress(), hash1, hash2) + suite.StateDB.SetState(wallet2.GetEthAddress(), hash1, hash2) + suite.StateDB.SetState(wallet4.GetEthAddress(), hash1, hash2) + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + + suite.Commit() + + stateDB := suite.newStateDB() + + suite.Equal(hash2, stateDB.GetState(wallet1.GetEthAddress(), hash1)) + suite.Equal(hash2, stateDB.GetState(wallet2.GetEthAddress(), hash1)) + suite.Equal(common.Hash{}, stateDB.GetState(wallet3WithoutState.GetEthAddress(), hash1)) + suite.Equal(hash2, stateDB.GetState(wallet4.GetEthAddress(), hash1)) + + suite.Run("should change state of current context, not original one", func() { + newState := evmvm.GenerateHash() + stateDB.SetState(wallet2.GetEthAddress(), hash1, newState) + + suite.Equal(newState, stateDB.GetState(wallet2.GetEthAddress(), hash1)) + suite.NotEqual(hash2, stateDB.GetState(wallet2.GetEthAddress(), hash1)) + + suite.Equal(hash2, suite.App().EvmKeeper().GetState(stateDB.(evmvm.CStateDB).ForTest_GetOriginalContext(), wallet2.GetEthAddress(), hash1), "original state must be unchanged before commit") + }) + + suite.Run("should create new account for deleted account", func() { + currentCtx := stateDB.(evmvm.CStateDB).ForTest_GetCurrentContext() + + getState := func() common.Hash { + return stateDB.GetState(wallet2.GetEthAddress(), hash1) + } + + suite.NotEqual(common.Hash{}, getState()) + + accountI2 := suite.App().AccountKeeper().GetAccount(currentCtx, wallet2.GetCosmosAddress()) + suite.Require().NotNil(accountI2) + suite.App().AccountKeeper().RemoveAccount(currentCtx, accountI2) + + accountI2 = suite.App().AccountKeeper().GetAccount(currentCtx, wallet2.GetCosmosAddress()) + suite.Require().Nil(accountI2) + // suite.Equal(common.Hash{}, getState()) + + newState := evmvm.GenerateHash() + stateDB.SetState(wallet2.GetEthAddress(), hash1, newState) + suite.Equal(newState, getState()) + + accountI2 = suite.App().AccountKeeper().GetAccount(currentCtx, wallet2.GetCosmosAddress()) + suite.NotNil(accountI2, "account should be re-created") + }) + + suite.Run("state of new account of re-made", func() { + currentCtx := stateDB.(evmvm.CStateDB).ForTest_GetCurrentContext() + + getState := func() common.Hash { + return stateDB.GetState(wallet4.GetEthAddress(), hash1) + } + + suite.NotEqual(common.Hash{}, getState()) + + accountI4 := suite.App().AccountKeeper().GetAccount(currentCtx, wallet4.GetCosmosAddress()) + suite.Require().NotNil(accountI4) + + stateDB.CreateAccount(wallet4.GetEthAddress()) + suite.Equal(common.Hash{}, getState(), "state of remade account should be empty") + + // reload accountI to fetch new account number + accountI4 = suite.App().AccountKeeper().GetAccount(currentCtx, wallet4.GetCosmosAddress()) + suite.Require().NotNil(accountI4) + + newState := evmvm.GenerateHash() + stateDB.SetState(wallet4.GetEthAddress(), hash1, newState) + suite.Equal(newState, getState(), "state of remade account should be the dirty state") + }) +} + +func (suite *StateDbIntegrationTestSuite) TestGetSetTransientState() { + addr := evmvm.GenerateAddress() + key1 := evmvm.GenerateHash() + val1 := evmvm.GenerateHash() + key2 := evmvm.GenerateHash() + val2 := evmvm.GenerateHash() + + addr2 := evmvm.GenerateAddress() + + suite.CStateDB.SetTransientState(addr, key1, val1) + suite.CStateDB.SetTransientState(addr, key2, val2) + suite.CStateDB.SetTransientState(addr2, key1, val1) + + suite.Equal(val1, suite.CStateDB.GetTransientState(addr, key1)) + suite.Equal(val2, suite.CStateDB.GetTransientState(addr, key2)) + suite.Equal(val1, suite.CStateDB.GetTransientState(addr2, key1)) + suite.Equal(common.Hash{}, suite.CStateDB.GetTransientState(addr2, key2)) +} + +func (suite *StateDbIntegrationTestSuite) TestSelfDestruct() { + existingAccount1 := suite.CITS.WalletAccounts.Number(1) + existingAccount2 := suite.CITS.WalletAccounts.Number(2) + nonExistsAccount1 := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.Require().True(suite.App().AccountKeeper().HasAccount(suite.Ctx(), existingAccount1.GetCosmosAddress())) + suite.Require().False(suite.App().AccountKeeper().HasAccount(suite.Ctx(), nonExistsAccount1.GetCosmosAddress())) + + suite.StateDB.(evmvm.CStateDB).Selfdestruct6780(existingAccount1.GetEthAddress()) + suite.False(suite.CStateDB.HasSuicided(existingAccount1.GetEthAddress()), "Selfdestruct6780 should not marking self-destructed for existing account") + suite.Equal( + suite.CStateDB.HasSuicided(existingAccount1.GetEthAddress()), + suite.StateDB.HasSuicided(existingAccount1.GetEthAddress()), + ) + + suite.StateDB.(evmvm.CStateDB).Selfdestruct6780(nonExistsAccount1.GetEthAddress()) + suite.False(suite.CStateDB.HasSuicided(nonExistsAccount1.GetEthAddress()), "Selfdestruct6780 should not marking self-destructed for non-exists account") + suite.Equal( + suite.CStateDB.HasSuicided(nonExistsAccount1.GetEthAddress()), + suite.StateDB.HasSuicided(nonExistsAccount1.GetEthAddress()), + ) + + err := suite.CStateDB.CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + suite.Commit() + + suite.Require().True(suite.App().AccountKeeper().HasAccount(suite.Ctx(), existingAccount1.GetCosmosAddress()), "existing account should not be deleted because it has not been marked as self-destructed") + + stateDB := suite.newStateDB() + cbDB := stateDB.(evmvm.CStateDB) + + stateDB.Suicide(existingAccount1.GetEthAddress()) + suite.True(stateDB.HasSuicided(existingAccount1.GetEthAddress()), "Selfdestruct should marking self-destructed for existing account") + suite.Equal( + stateDB.HasSuicided(existingAccount1.GetEthAddress()), + stateDB.HasSuicided(existingAccount1.GetEthAddress()), + ) + + stateDB.Suicide(nonExistsAccount1.GetEthAddress()) + suite.False(stateDB.HasSuicided(nonExistsAccount1.GetEthAddress()), "Selfdestruct should not marking self-destructed for non-exists account") + suite.Equal( + stateDB.HasSuicided(nonExistsAccount1.GetEthAddress()), + stateDB.HasSuicided(nonExistsAccount1.GetEthAddress()), + ) + + err = cbDB.CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + suite.Commit() + + suite.Require().False(suite.App().AccountKeeper().HasAccount(suite.Ctx(), existingAccount1.GetCosmosAddress()), "existing account should be deleted because it has been marked as self-destructed") + + stateDB = suite.newStateDB() + cbDB = stateDB.(evmvm.CStateDB) + wasExistingAccountButDeletedBefore := existingAccount1 + + stateDB.CreateAccount(wasExistingAccountButDeletedBefore.GetEthAddress()) + cbDB.Selfdestruct6780(wasExistingAccountButDeletedBefore.GetEthAddress()) + suite.True(stateDB.HasSuicided(wasExistingAccountButDeletedBefore.GetEthAddress()), "Selfdestruct6780 should marking self-destructed because it has just been created within the same tx") + suite.Equal( + stateDB.HasSuicided(wasExistingAccountButDeletedBefore.GetEthAddress()), + stateDB.HasSuicided(wasExistingAccountButDeletedBefore.GetEthAddress()), + ) + + stateDB.CreateAccount(existingAccount2.GetEthAddress()) + cbDB.Selfdestruct6780(existingAccount2.GetEthAddress()) + suite.True(stateDB.HasSuicided(existingAccount2.GetEthAddress()), "Selfdestruct6780 should marking self-destructed because it has just been re-created within the same tx") + suite.Equal( + stateDB.HasSuicided(existingAccount2.GetEthAddress()), + stateDB.HasSuicided(existingAccount2.GetEthAddress()), + ) + + stateDB.CreateAccount(nonExistsAccount1.GetEthAddress()) + cbDB.Selfdestruct6780(nonExistsAccount1.GetEthAddress()) + suite.True(stateDB.HasSuicided(nonExistsAccount1.GetEthAddress()), "Selfdestruct6780 should marking self-destructed because it has just been created within the same tx") + suite.Equal( + stateDB.HasSuicided(nonExistsAccount1.GetEthAddress()), + stateDB.HasSuicided(nonExistsAccount1.GetEthAddress()), + ) + + err = cbDB.CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + suite.Commit() + + suite.False(suite.App().AccountKeeper().HasAccount(suite.Ctx(), wasExistingAccountButDeletedBefore.GetCosmosAddress()), "existing account should be deleted because it has been marked as self-destructed") + suite.False(suite.App().AccountKeeper().HasAccount(suite.Ctx(), existingAccount2.GetCosmosAddress()), "existing account should be deleted because it has been marked as self-destructed") + suite.False(suite.App().AccountKeeper().HasAccount(suite.Ctx(), nonExistsAccount1.GetCosmosAddress()), "should be deleted because it has been marked as self-destructed") + + suite.Run("HasSuicided", func() { + stateDB := suite.newStateDB() + + for i := 0; i < 10; i++ { + yes := i%2 == 0 + addr := evmvm.GenerateAddress() + if yes { + stateDB.CreateAccount(addr) + stateDB.(evmvm.CStateDB).Selfdestruct6780(addr) + suite.True(stateDB.HasSuicided(addr)) + stateDB.Suicide(addr) + suite.True(stateDB.HasSuicided(addr)) + } else { + stateDB.(evmvm.CStateDB).Selfdestruct6780(addr) + suite.False(stateDB.HasSuicided(addr)) + stateDB.Suicide(addr) + suite.False(stateDB.HasSuicided(addr)) + } + } + }) + + suite.Run("State before and after self destruct", func() { + stateDB := suite.newStateDB() + + wallet1 := suite.CITS.WalletAccounts.Number(1) + wallet2 := suite.CITS.WalletAccounts.Number(2) + + key := evmvm.GenerateHash() + val := evmvm.GenerateHash() + + codeHash, code := RandomContractCode() + + stateDB.CreateAccount(wallet1.GetEthAddress()) + stateDB.SetState(wallet1.GetEthAddress(), key, val) + stateDB.SetCode(wallet1.GetEthAddress(), code) + + suite.Require().Equal(val, stateDB.GetState(wallet1.GetEthAddress(), key)) + suite.Require().Equal(codeHash, stateDB.GetCodeHash(wallet1.GetEthAddress())) + + err := stateDB.(evmvm.CStateDB).CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + suite.Commit() + + suite.Require().Equal(val, suite.App().EvmKeeper().GetState(suite.Ctx(), wallet1.GetEthAddress(), key)) + + stateDB = suite.newStateDB() + + // assign for account that only available in current context + stateDB.CreateAccount(wallet2.GetEthAddress()) + stateDB.SetState(wallet2.GetEthAddress(), key, val) + stateDB.SetCode(wallet2.GetEthAddress(), code) + + suite.Require().Equal(val, stateDB.GetState(wallet2.GetEthAddress(), key)) + suite.Require().Equal(codeHash, stateDB.GetCodeHash(wallet2.GetEthAddress())) + + stateDB.Suicide(wallet1.GetEthAddress()) + stateDB.Suicide(wallet2.GetEthAddress()) + + suite.Require().True(stateDB.HasSuicided(wallet1.GetEthAddress())) + suite.Require().True(stateDB.HasSuicided(wallet2.GetEthAddress())) + + suite.Equal(val, stateDB.GetState(wallet1.GetEthAddress(), key), "state must be still accessible before commit") + suite.Equal(val, stateDB.GetState(wallet2.GetEthAddress(), key), "state must be still accessible before commit") + + suite.Equal(codeHash, stateDB.GetCodeHash(wallet1.GetEthAddress()), "code hash must be still accessible before commit") + suite.Equal(codeHash, stateDB.GetCodeHash(wallet2.GetEthAddress()), "code hash must be still accessible before commit") + + // backup account + cbDB := stateDB.(evmvm.CStateDB) + account1 := suite.App().AccountKeeper().GetAccount(cbDB.ForTest_GetCurrentContext(), wallet1.GetCosmosAddress()) + suite.Require().NotNil(account1) + account2 := suite.App().AccountKeeper().GetAccount(cbDB.ForTest_GetCurrentContext(), wallet2.GetCosmosAddress()) + suite.Require().NotNil(account2) + + err = stateDB.(evmvm.CStateDB).CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + suite.Commit() + + suite.False(suite.App().AccountKeeper().HasAccount(suite.Ctx(), wallet1.GetCosmosAddress()), "existing account should be deleted because it has been marked as self-destructed") + suite.False(suite.App().AccountKeeper().HasAccount(suite.Ctx(), wallet2.GetCosmosAddress()), "existing account should be deleted because it has been marked as self-destructed") + + // set back account for accessing code hash and state + suite.App().AccountKeeper().SetAccount(suite.Ctx(), account1) + suite.App().AccountKeeper().SetAccount(suite.Ctx(), account2) + + suite.Commit() + + suite.Equal(common.Hash{}, suite.App().EvmKeeper().GetState(suite.Ctx(), wallet1.GetEthAddress(), key), "state must be removed") + suite.Equal(common.Hash{}, suite.App().EvmKeeper().GetState(suite.Ctx(), wallet2.GetEthAddress(), key), "state must be removed") + suite.Equal(common.BytesToHash(evmtypes.EmptyCodeHash), suite.App().EvmKeeper().GetCodeHash(suite.Ctx(), account1.GetAddress()), "code hash must be removed") + suite.Equal(common.BytesToHash(evmtypes.EmptyCodeHash), suite.App().EvmKeeper().GetCodeHash(suite.Ctx(), account2.GetAddress()), "code hash must be removed") + }) +} + +func (suite *StateDbIntegrationTestSuite) TestExist() { + existingWallet1 := suite.CITS.WalletAccounts.Number(1) + existingWallet2 := suite.CITS.WalletAccounts.Number(2) + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.True(suite.StateDB.Exist(existingWallet1.GetEthAddress())) + suite.True(suite.StateDB.Exist(existingWallet2.GetEthAddress())) + suite.False(suite.StateDB.Exist(nonExistsWallet.GetEthAddress())) + + suite.StateDB.CreateAccount(nonExistsWallet.GetEthAddress()) + nonExistsWalletNowExists := nonExistsWallet + suite.True(suite.StateDB.Exist(nonExistsWalletNowExists.GetEthAddress())) + + suite.StateDB.Suicide(existingWallet1.GetEthAddress()) + suite.True(suite.StateDB.Exist(existingWallet1.GetEthAddress()), "existing account should be still exist because it has not been committed yet") + + suite.StateDB.(evmvm.CStateDB).Selfdestruct6780(nonExistsWalletNowExists.GetEthAddress()) + suite.True(suite.StateDB.Exist(nonExistsWalletNowExists.GetEthAddress()), "existing account should be still exist because it has not been committed yet") + + err := suite.CStateDB.CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + suite.Commit() + + stateDB := suite.newStateDB() + + suite.False(stateDB.Exist(existingWallet1.GetEthAddress()), "existing account should be deleted because it has been marked as self-destructed") + suite.True(stateDB.Exist(existingWallet2.GetEthAddress())) + suite.False(stateDB.Exist(nonExistsWalletNowExists.GetEthAddress()), "existing account should be deleted because it has been marked as self-destructed") +} + +func (suite *StateDbIntegrationTestSuite) TestEmpty() { + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + + suite.True(suite.StateDB.Empty(nonExistsWallet.GetEthAddress()), "non exists account should be treated as empty") + + suite.StateDB.CreateAccount(nonExistsWallet.GetEthAddress()) + nonExistsWalletNowExists := nonExistsWallet + suite.True(suite.StateDB.Empty(nonExistsWalletNowExists.GetEthAddress()), "newly created account should be empty") + + suite.StateDB.AddBalance(nonExistsWalletNowExists.GetEthAddress(), big.NewInt(1)) + suite.False(suite.StateDB.Empty(nonExistsWalletNowExists.GetEthAddress()), "account with balance should not be empty") + + suite.StateDB.SubBalance(nonExistsWalletNowExists.GetEthAddress(), big.NewInt(1)) + suite.Require().True(suite.StateDB.Empty(nonExistsWalletNowExists.GetEthAddress())) + + suite.StateDB.SetNonce(nonExistsWalletNowExists.GetEthAddress(), 1) + suite.False(suite.StateDB.Empty(nonExistsWalletNowExists.GetEthAddress()), "account with nonce should not be empty") + + suite.StateDB.SetNonce(nonExistsWalletNowExists.GetEthAddress(), 0) + suite.Require().True(suite.StateDB.Empty(nonExistsWalletNowExists.GetEthAddress())) + + suite.StateDB.SetCode(nonExistsWalletNowExists.GetEthAddress(), []byte{1}) + suite.False(suite.StateDB.Empty(nonExistsWalletNowExists.GetEthAddress()), "account with code should not be empty") + + suite.StateDB.SetCode(nonExistsWalletNowExists.GetEthAddress(), []byte{}) + suite.Require().True(suite.StateDB.Empty(nonExistsWalletNowExists.GetEthAddress())) +} + +func (suite *StateDbIntegrationTestSuite) TestAddressInAccessList() { + wallet1 := suite.CITS.WalletAccounts.Number(1) + wallet2 := suite.CITS.WalletAccounts.Number(2) + + suite.False(suite.StateDB.AddressInAccessList(wallet1.GetEthAddress())) + suite.False(suite.StateDB.AddressInAccessList(wallet2.GetEthAddress())) + + suite.CStateDB.AddAddressToAccessList(wallet1.GetEthAddress()) + suite.True(suite.StateDB.AddressInAccessList(wallet1.GetEthAddress())) + suite.False(suite.StateDB.AddressInAccessList(wallet2.GetEthAddress())) +} + +func (suite *StateDbIntegrationTestSuite) TestSlotInAccessList() { + wallet1 := suite.CITS.WalletAccounts.Number(1) + wallet2 := suite.CITS.WalletAccounts.Number(2) + + slot1 := evmvm.GenerateHash() + slot2 := evmvm.GenerateHash() + + suite.CStateDB.AddSlotToAccessList(wallet1.GetEthAddress(), slot1) + suite.CStateDB.AddSlotToAccessList(wallet1.GetEthAddress(), slot2) + suite.CStateDB.AddSlotToAccessList(wallet2.GetEthAddress(), slot1) + + var addrExists, slotExists bool + + addrExists, slotExists = suite.StateDB.SlotInAccessList(wallet1.GetEthAddress(), slot1) + suite.True(addrExists) + suite.True(slotExists) + + addrExists, slotExists = suite.StateDB.SlotInAccessList(wallet1.GetEthAddress(), slot2) + suite.True(addrExists) + suite.True(slotExists) + + addrExists, slotExists = suite.StateDB.SlotInAccessList(wallet2.GetEthAddress(), slot1) + suite.True(addrExists) + suite.True(slotExists) + + addrExists, slotExists = suite.StateDB.SlotInAccessList(wallet2.GetEthAddress(), slot2) + suite.True(addrExists) + suite.False(slotExists) + + addrExists, slotExists = suite.StateDB.SlotInAccessList(evmvm.GenerateAddress(), slot1) + suite.False(addrExists) + suite.False(slotExists) + + addrExists, slotExists = suite.StateDB.SlotInAccessList(evmvm.GenerateAddress(), slot2) + suite.False(addrExists) + suite.False(slotExists) +} + +func (suite *StateDbIntegrationTestSuite) TestAddAddressToAccessList() { + wallet1 := suite.CITS.WalletAccounts.Number(1) + wallet2 := suite.CITS.WalletAccounts.Number(2) + + suite.StateDB.AddAddressToAccessList(wallet1.GetEthAddress()) + + suite.True(suite.CStateDB.AddressInAccessList(wallet1.GetEthAddress())) + suite.False(suite.CStateDB.AddressInAccessList(wallet2.GetEthAddress())) +} + +func (suite *StateDbIntegrationTestSuite) TestAddSlotToAccessList() { + wallet1 := suite.CITS.WalletAccounts.Number(1) + wallet2 := suite.CITS.WalletAccounts.Number(2) + + slot1 := evmvm.GenerateHash() + slot2 := evmvm.GenerateHash() + + suite.StateDB.AddSlotToAccessList(wallet1.GetEthAddress(), slot1) + suite.StateDB.AddSlotToAccessList(wallet1.GetEthAddress(), slot2) + suite.StateDB.AddSlotToAccessList(wallet2.GetEthAddress(), slot1) + + var addrExists, slotExists bool + + addrExists, slotExists = suite.CStateDB.SlotInAccessList(wallet1.GetEthAddress(), slot1) + suite.True(addrExists) + suite.True(slotExists) + + addrExists, slotExists = suite.CStateDB.SlotInAccessList(wallet1.GetEthAddress(), slot2) + suite.True(addrExists) + suite.True(slotExists) + + addrExists, slotExists = suite.CStateDB.SlotInAccessList(wallet2.GetEthAddress(), slot1) + suite.True(addrExists) + suite.True(slotExists) + + addrExists, slotExists = suite.CStateDB.SlotInAccessList(wallet2.GetEthAddress(), slot2) + suite.True(addrExists) + suite.False(slotExists) + + addrExists, slotExists = suite.CStateDB.SlotInAccessList(evmvm.GenerateAddress(), slot1) + suite.False(addrExists) + suite.False(slotExists) + + addrExists, slotExists = suite.CStateDB.SlotInAccessList(evmvm.GenerateAddress(), slot2) + suite.False(addrExists) + suite.False(slotExists) +} + +func (suite *StateDbIntegrationTestSuite) TestPrepareAccessList() { + sender := evmvm.GenerateAddress() + // coinBase := evmvm.GenerateAddress() + dst := evmvm.GenerateAddress() + precompiles := []common.Address{evmvm.GenerateAddress(), evmvm.GenerateAddress()} + accessList := ethtypes.AccessList{ + { + Address: evmvm.GenerateAddress(), + StorageKeys: []common.Hash{evmvm.GenerateHash(), evmvm.GenerateHash()}, + }, + { + Address: evmvm.GenerateAddress(), + StorageKeys: []common.Hash{evmvm.GenerateHash(), evmvm.GenerateHash()}, + }, + } + logs := evmvm.Logs{ + { + Address: evmvm.GenerateAddress(), + Topics: nil, + Data: nil, + BlockNumber: 1, + TxHash: evmvm.GenerateHash(), + TxIndex: 1, + BlockHash: evmvm.GenerateHash(), + Index: 1, + Removed: false, + }, + } + + // dirty storages + var originalRefund uint64 = 5 + suite.CStateDB.AddRefund(originalRefund) + suite.CStateDB.ForTest_AddAddressToSelfDestructedList(evmvm.GenerateAddress()) + suite.CStateDB.AddSlotToAccessList(evmvm.GenerateAddress(), evmvm.GenerateHash()) + suite.CStateDB.ForTest_SetLogs(logs) + suite.CStateDB.SetTransientState(evmvm.GenerateAddress(), evmvm.GenerateHash(), evmvm.GenerateHash()) + + // execute + suite.StateDB.PrepareAccessList(sender, &dst, precompiles, accessList) + + // check + suite.Equal(originalRefund, suite.CStateDB.GetRefund(), "refund should be kept") + suite.NotEmpty(suite.CStateDB.ForTest_CloneSelfDestructed(), "self destructed should be kept") + suite.True(suite.StateDB.AddressInAccessList(sender)) + suite.True(suite.StateDB.AddressInAccessList(dst)) + for _, precompile := range precompiles { + suite.True(suite.StateDB.AddressInAccessList(precompile)) + } + for _, access := range accessList { + suite.True(suite.StateDB.AddressInAccessList(access.Address)) + for _, slot := range access.StorageKeys { + suite.True(suite.StateDB.SlotInAccessList(access.Address, slot)) + } + } + suite.Equal(2 /*sender+dst*/ +1 /*coinbase*/ +len(precompiles)+len(accessList), len(suite.CStateDB.ForTest_CloneAccessList().CloneElements())) + suite.Equal([]*ethtypes.Log(logs), suite.CStateDB.GetTransactionLogs(), "logs should be kept") + suite.Zero(suite.CStateDB.ForTest_CountRecordsTransientStorage(), "transient storage should be cleared") +} + +func (suite *StateDbIntegrationTestSuite) TestAddLog() { + for i := 0; i < 5; i++ { + suite.Require().Equal(i, len(suite.CStateDB.GetTransactionLogs())) + suite.StateDB.AddLog(ðtypes.Log{ + Address: evmvm.GenerateAddress(), + Topics: nil, + Data: nil, + BlockNumber: uint64(i) + 1, + TxHash: evmvm.GenerateHash(), + TxIndex: uint(i), + BlockHash: evmvm.GenerateHash(), + Index: uint(i), + Removed: false, + }) + suite.Require().Equal(i+1, len(suite.CStateDB.GetTransactionLogs())) + } +} + +func (suite *StateDbIntegrationTestSuite) TestGetTransactionLogs() { + suite.Run("nil logs list returns empty list", func() { + suite.CStateDB.ForTest_SetLogs(nil) + + output := suite.CStateDB.GetTransactionLogs() + suite.NotNil(output) + suite.Empty(output) + }) + + for i := 0; i < 5; i++ { + suite.Require().Len(suite.CStateDB.GetTransactionLogs(), i) + suite.CStateDB.AddLog(ðtypes.Log{ + Address: evmvm.GenerateAddress(), + Topics: nil, + Data: nil, + BlockNumber: uint64(i) + 1, + TxHash: evmvm.GenerateHash(), + TxIndex: uint(i), + BlockHash: evmvm.GenerateHash(), + Index: uint(i), + Removed: false, + }) + } + + suite.Equal(len(suite.CStateDB.GetTransactionLogs()), len(suite.CStateDB.GetTransactionLogs())) +} + +func (suite *StateDbIntegrationTestSuite) TestSnapshotAndRevert() { + receiver1 := integration_test_util.NewTestAccount(suite.T(), nil) + receiver2 := integration_test_util.NewTestAccount(suite.T(), nil) + nonExistsWalletBeforeCheckpoint := integration_test_util.NewTestAccount(suite.T(), nil) + existingWalletBeforeCheckpoint := suite.CITS.WalletAccounts.Number(1) + zeroNonceWalletBeforeCheckPoint := integration_test_util.NewTestAccount(suite.T(), nil) + emptyWallet := integration_test_util.NewTestAccount(suite.T(), nil) + contract := integration_test_util.NewTestAccount(suite.T(), nil) + codeHash1, code1 := RandomContractCode() + stateKey := evmvm.GenerateHash() + stateVal := evmvm.GenerateHash() + + const checkAtSnapshot = 5 + + var touchedAtCheckpoint evmvm.AccountTracker + var selfStructAtCheckpoint evmvm.AccountTracker + var accessListAtCheckpoint *evmvm.AccessList2 + var transientStoreAtCheckpoint evmvm.TransientStorage + var logsAtCheckpoint evmvm.Logs + + for r := 1; r < 10; r++ { + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(1000)) + suite.StateDB.AddBalance(receiver2.GetEthAddress(), big.NewInt(1000)) + + suite.Require().Equal(big.NewInt(int64(1000*r)), suite.StateDB.GetBalance(receiver1.GetEthAddress())) + suite.Require().Equal(big.NewInt(int64(1000*r)), suite.StateDB.GetBalance(receiver2.GetEthAddress())) + + suite.StateDB.SetNonce(zeroNonceWalletBeforeCheckPoint.GetEthAddress(), uint64(r)) + suite.Require().Equal(uint64(r), suite.StateDB.GetNonce(zeroNonceWalletBeforeCheckPoint.GetEthAddress())) + + suite.StateDB.AddRefund(1) + suite.Require().Equal(uint64(r), suite.CStateDB.GetRefund()) + + suite.CStateDB.ForTest_AddAddressToSelfDestructedList(evmvm.GenerateAddress()) + suite.Require().Len(suite.CStateDB.ForTest_CloneSelfDestructed(), r) + + suite.StateDB.AddAddressToAccessList(evmvm.GenerateAddress()) + suite.Require().Len(suite.CStateDB.ForTest_CloneAccessList().CloneElements(), r) + + lenTransientStoreBefore := suite.CStateDB.ForTest_CountRecordsTransientStorage() + suite.CStateDB.SetTransientState(evmvm.GenerateAddress(), evmvm.GenerateHash(), evmvm.GenerateHash()) + suite.Require().Less(lenTransientStoreBefore, suite.CStateDB.ForTest_CountRecordsTransientStorage()) + + suite.StateDB.AddLog(ðtypes.Log{ + Address: evmvm.GenerateAddress(), + TxHash: evmvm.GenerateHash(), + BlockHash: evmvm.GenerateHash(), + }) + suite.Require().Len(suite.CStateDB.GetTransactionLogs(), r) + + if r <= checkAtSnapshot { + suite.StateDB.SetCode(contract.GetEthAddress(), code1) + suite.Require().Equal(codeHash1, suite.StateDB.GetCodeHash(contract.GetEthAddress())) + suite.Require().Equal(code1, suite.StateDB.GetCode(contract.GetEthAddress())) + + suite.StateDB.SetState(contract.GetEthAddress(), stateKey, stateVal) + suite.Require().Equal(stateVal, suite.StateDB.GetState(contract.GetEthAddress(), stateKey)) + + suite.CStateDB.SetTransientState(contract.GetEthAddress(), stateKey, stateVal) + suite.Require().Equal(stateVal, suite.CStateDB.GetTransientState(contract.GetEthAddress(), stateKey)) + + if r == checkAtSnapshot { + touchedAtCheckpoint = suite.CStateDB.ForTest_CloneTouched() + suite.Require().NotEmpty(touchedAtCheckpoint) + + selfStructAtCheckpoint = suite.CStateDB.ForTest_CloneSelfDestructed() + suite.Require().Len(selfStructAtCheckpoint, r) + + accessListAtCheckpoint = suite.CStateDB.ForTest_CloneAccessList() + suite.Require().Len(accessListAtCheckpoint.CloneElements(), r) + + transientStoreAtCheckpoint = suite.CStateDB.ForTest_CloneTransientStorage() + + logsAtCheckpoint = evmvm.Logs(suite.CStateDB.GetTransactionLogs()).Copy() + suite.Require().Len(logsAtCheckpoint, r) + } + + suite.Require().True(suite.StateDB.Empty(emptyWallet.GetEthAddress())) + } else if r == checkAtSnapshot+1 { + suite.StateDB.CreateAccount(nonExistsWalletBeforeCheckpoint.GetEthAddress()) + suite.Require().True(suite.StateDB.Exist(nonExistsWalletBeforeCheckpoint.GetEthAddress())) + + existingAccountBeforeCheckpoint := suite.App().AccountKeeper().GetAccount(suite.CStateDB.ForTest_GetCurrentContext(), existingWalletBeforeCheckpoint.GetCosmosAddress()) + suite.Require().NotNil(existingAccountBeforeCheckpoint) + suite.CStateDB.DestroyAccount(common.BytesToAddress(existingAccountBeforeCheckpoint.GetAddress())) + + suite.StateDB.SetCode(contract.GetEthAddress(), []byte{}) + suite.Require().Equal(common.BytesToHash(evmtypes.EmptyCodeHash), suite.StateDB.GetCodeHash(contract.GetEthAddress())) + suite.Require().Zero(suite.StateDB.GetCodeSize(contract.GetEthAddress())) + + suite.StateDB.SetState(contract.GetEthAddress(), stateKey, evmvm.GenerateHash()) + suite.Require().NotEqual(stateVal, suite.StateDB.GetState(contract.GetEthAddress(), stateKey)) + + suite.CStateDB.SetTransientState(contract.GetEthAddress(), stateKey, evmvm.GenerateHash()) + suite.Require().NotEqual(stateVal, suite.CStateDB.GetTransientState(contract.GetEthAddress(), stateKey)) + + suite.StateDB.SetNonce(emptyWallet.GetEthAddress(), 1) + suite.Require().False(suite.StateDB.Empty(emptyWallet.GetEthAddress())) + } + + // create snapshot + sid := suite.StateDB.Snapshot() + suite.Require().Equal(r-1, sid) + } + + assertContentAfterRevert := func() { + snapshots := suite.CStateDB.ForTest_GetSnapshots() + suite.Require().Len(snapshots, checkAtSnapshot+1) + suite.Require().Equal(checkAtSnapshot-1, snapshots[len(snapshots)-1].GetID()) + + suite.Equal(big.NewInt(checkAtSnapshot*1000), suite.StateDB.GetBalance(receiver1.GetEthAddress())) + suite.Equal(big.NewInt(checkAtSnapshot*1000), suite.StateDB.GetBalance(receiver2.GetEthAddress())) + + suite.False(suite.StateDB.Exist(nonExistsWalletBeforeCheckpoint.GetEthAddress())) + suite.True(suite.StateDB.Exist(existingWalletBeforeCheckpoint.GetEthAddress())) + + suite.Equal(uint64(checkAtSnapshot), suite.StateDB.GetNonce(zeroNonceWalletBeforeCheckPoint.GetEthAddress())) + + if suite.Equal(codeHash1, suite.StateDB.GetCodeHash(contract.GetEthAddress())) { + suite.Equal(code1, suite.StateDB.GetCode(contract.GetEthAddress())) + } + + suite.Equal(uint64(checkAtSnapshot), suite.CStateDB.GetRefund()) + + suite.Equal(stateVal, suite.StateDB.GetState(contract.GetEthAddress(), stateKey)) + + suite.Equal(stateVal, suite.CStateDB.GetTransientState(contract.GetEthAddress(), stateKey)) + + if suite.Equal(len(touchedAtCheckpoint), len(suite.CStateDB.ForTest_CloneTouched())) { + suite.True(reflect.DeepEqual(touchedAtCheckpoint, suite.CStateDB.ForTest_CloneTouched())) + } + + if suite.Equal(len(selfStructAtCheckpoint), len(suite.CStateDB.ForTest_CloneSelfDestructed())) { + suite.True(reflect.DeepEqual(selfStructAtCheckpoint, suite.CStateDB.ForTest_CloneSelfDestructed())) + } + + if suite.Equal(len(accessListAtCheckpoint.CloneElements()), len(suite.CStateDB.ForTest_CloneAccessList().CloneElements())) { + suite.True(reflect.DeepEqual(accessListAtCheckpoint.CloneElements(), suite.CStateDB.ForTest_CloneAccessList().CloneElements())) + } + + if suite.Equal(transientStoreAtCheckpoint.Size(), suite.CStateDB.ForTest_CountRecordsTransientStorage()) { + suite.True(reflect.DeepEqual(transientStoreAtCheckpoint, suite.CStateDB.ForTest_CloneTransientStorage())) + } + + if suite.Equal(len(logsAtCheckpoint), len(suite.CStateDB.GetTransactionLogs())) { + suite.True(reflect.DeepEqual([]*ethtypes.Log(logsAtCheckpoint), suite.CStateDB.GetTransactionLogs())) + } + + suite.True(suite.StateDB.Empty(emptyWallet.GetEthAddress())) + } + + snapshotId := checkAtSnapshot - 1 + suite.StateDB.RevertToSnapshot(snapshotId) + assertContentAfterRevert() + + // do some change and revert then test again + for i := 0; i < 5; i++ { + suite.StateDB.AddBalance(receiver1.GetEthAddress(), big.NewInt(1000)) + suite.StateDB.AddBalance(receiver2.GetEthAddress(), big.NewInt(1000)) + suite.StateDB.SetNonce(zeroNonceWalletBeforeCheckPoint.GetEthAddress(), 9900+uint64(i)) + suite.StateDB.AddRefund(1) + suite.CStateDB.ForTest_AddAddressToSelfDestructedList(evmvm.GenerateAddress()) + suite.StateDB.AddAddressToAccessList(evmvm.GenerateAddress()) + suite.CStateDB.SetTransientState(evmvm.GenerateAddress(), evmvm.GenerateHash(), evmvm.GenerateHash()) + suite.StateDB.AddLog(ðtypes.Log{ + Address: evmvm.GenerateAddress(), + TxHash: evmvm.GenerateHash(), + BlockHash: evmvm.GenerateHash(), + }) + } + // end of change + + suite.StateDB.RevertToSnapshot(snapshotId) + assertContentAfterRevert() // second test + + // commit + + err := suite.CStateDB.CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + suite.Commit() + + suite.Equal(int64(checkAtSnapshot*1000), suite.CITS.QueryBalance(0, receiver1.GetCosmosAddress().String()).Amount.Int64()) + suite.Equal(int64(checkAtSnapshot*1000), suite.CITS.QueryBalance(0, receiver2.GetCosmosAddress().String()).Amount.Int64()) + + suite.False(suite.App().AccountKeeper().HasAccount(suite.Ctx(), nonExistsWalletBeforeCheckpoint.GetCosmosAddress())) + suite.True(suite.App().AccountKeeper().HasAccount(suite.Ctx(), existingWalletBeforeCheckpoint.GetCosmosAddress())) + + suite.Equal(uint64(checkAtSnapshot), suite.App().EvmKeeper().GetNonce(suite.Ctx(), zeroNonceWalletBeforeCheckPoint.GetEthAddress())) + + contractAccount := suite.App().AccountKeeper().GetAccount(suite.Ctx(), contract.GetCosmosAddress()) + suite.Equal(codeHash1, suite.App().EvmKeeper().GetCodeHash(suite.Ctx(), contractAccount.GetAddress())) + + suite.Equal(stateVal, suite.App().EvmKeeper().GetState(suite.Ctx(), contract.GetEthAddress(), stateKey)) + + suite.False(suite.App().AccountKeeper().HasAccount(suite.Ctx(), emptyWallet.GetCosmosAddress())) +} + +func (suite *StateDbIntegrationTestSuite) TestAddPreimage() { + suite.Require().NotPanics(func() { + suite.StateDB.AddPreimage(evmvm.GenerateHash(), evmvm.GenerateHash().Bytes()) + }) +} + +func (suite *StateDbIntegrationTestSuite) TestCommitMultiStore() { + existingWallet1 := suite.CITS.WalletAccounts.Number(1) + existingWallet2 := suite.CITS.WalletAccounts.Number(2) + nonExistsWallet := integration_test_util.NewTestAccount(suite.T(), nil) + nonBalanceWallet := integration_test_util.NewTestAccount(suite.T(), nil) + existingWallet3 := suite.CITS.WalletAccounts.Number(3) + + // transfer all non-EVM coins out to procedure case empty account + for _, coin := range suite.App(). + BankKeeper(). + GetAllBalances(suite.CStateDB.ForTest_GetCurrentContext(), existingWallet3.GetCosmosAddress()) { + if coin.GetDenom() == suite.CStateDB.ForTest_GetEvmDenom() { + continue + } + + err := suite.App().BankKeeper().SendCoinsFromAccountToModule(suite.CStateDB.ForTest_GetCurrentContext(), existingWallet3.GetCosmosAddress(), evmtypes.ModuleName, sdk.NewCoins(coin)) + suite.Require().NoError(err) + } + + suite.Require().True(suite.StateDB.Exist(existingWallet1.GetEthAddress())) + suite.Require().True(suite.StateDB.Exist(existingWallet2.GetEthAddress())) + suite.Require().False(suite.StateDB.Exist(nonExistsWallet.GetEthAddress())) + suite.Require().Zero(suite.StateDB.GetBalance(nonBalanceWallet.GetEthAddress()).Sign()) + suite.Require().True(suite.StateDB.Exist(existingWallet3.GetEthAddress())) + + suite.StateDB.CreateAccount(nonExistsWallet.GetEthAddress()) + nonExistsWalletNowExists := nonExistsWallet + suite.Require().True(suite.StateDB.Exist(nonExistsWalletNowExists.GetEthAddress())) + + suite.StateDB.Suicide(existingWallet1.GetEthAddress()) + suite.Require().True(suite.StateDB.Exist(existingWallet1.GetEthAddress())) + + suite.StateDB.(evmvm.CStateDB).Selfdestruct6780(nonExistsWalletNowExists.GetEthAddress()) + suite.Require().True(suite.StateDB.Exist(nonExistsWalletNowExists.GetEthAddress())) + + suite.StateDB.AddBalance(nonBalanceWallet.GetEthAddress(), big.NewInt(1)) + + curBalance := suite.StateDB.GetBalance(existingWallet3.GetEthAddress()) + suite.Require().NotZero(curBalance.Sign()) + suite.StateDB.SubBalance(existingWallet3.GetEthAddress(), curBalance) + + err := suite.CStateDB.CommitMultiStore(true) // commit cache multi-store within the StateDB + suite.Require().NoError(err) + suite.Commit() + + suite.False( + suite.App().BankKeeper().GetBalance(suite.Ctx(), nonBalanceWallet.GetCosmosAddress(), suite.CStateDB.ForTest_GetEvmDenom()).IsZero(), + "changes should be committed", + ) + suite.False( + suite.App().AccountKeeper().HasAccount(suite.Ctx(), existingWallet1.GetCosmosAddress()), + "destroy account should be committed", + ) + suite.False( + suite.App().AccountKeeper().HasAccount(suite.Ctx(), existingWallet3.GetCosmosAddress()), + "touched to be empty account should be removed", + ) + suite.Panics(func() { + suite.Require().True(suite.CStateDB.ForTest_IsCommitted()) // ensure flag is set + + err := suite.CStateDB.CommitMultiStore(true) + suite.Require().NoError(err) + }, "can not commits twice") +} + +func RandomContractCode() (codeHash common.Hash, code []byte) { + codeLen := mathrand.Uint32()%1000 + 100 + buffer := make([]byte, codeLen) + _, err := rand.Read(buffer) + if err != nil { + panic(err) + } + + code = buffer[:] + codeHash = common.BytesToHash(crypto.Keccak256(code)) + + return +} diff --git a/x/evm/vm/state_db_logs.go b/x/evm/vm/state_db_logs.go new file mode 100644 index 0000000000..b8edc7d89c --- /dev/null +++ b/x/evm/vm/state_db_logs.go @@ -0,0 +1,16 @@ +package vm + +import ethtypes "github.com/ethereum/go-ethereum/core/types" + +type Logs []*ethtypes.Log + +func (l Logs) Copy() Logs { + if l == nil { + return nil + } + + copied := make(Logs, len(l)) + copy(copied, l) + + return copied +} diff --git a/x/evm/vm/state_db_logs_test.go b/x/evm/vm/state_db_logs_test.go new file mode 100644 index 0000000000..5fffb11ea2 --- /dev/null +++ b/x/evm/vm/state_db_logs_test.go @@ -0,0 +1,53 @@ +package vm + +import ( + "testing" + + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +func Test_logs(t *testing.T) { + originalLogs := Logs{} + for i := 0; i < 5; i++ { + originalLogs = append(originalLogs, ðtypes.Log{ + Address: GenerateAddress(), + Topics: nil, + Data: nil, + BlockNumber: uint64(i) + 1, + TxHash: GenerateHash(), + TxIndex: uint(i), + BlockHash: GenerateHash(), + Index: uint(i), + Removed: false, + }) + } + + originalLen := len(originalLogs) + + copiedLogs := originalLogs.Copy() + require.Len(t, copiedLogs, originalLen) + + const add = 5 + + for i := originalLen; i < originalLen+add; i++ { + copiedLogs = append(copiedLogs, ðtypes.Log{ + Address: GenerateAddress(), + Topics: nil, + Data: nil, + BlockNumber: uint64(i) + 1, + TxHash: GenerateHash(), + TxIndex: uint(i), + BlockHash: GenerateHash(), + Index: uint(i), + Removed: false, + }) + } + + require.Len(t, originalLogs, originalLen) + require.Len(t, copiedLogs, originalLen+add) + + copiedLogs = copiedLogs[:originalLen-1] + require.Len(t, copiedLogs, originalLen-1) + require.Len(t, originalLogs, originalLen) +} diff --git a/x/evm/vm/state_db_snapshot.go b/x/evm/vm/state_db_snapshot.go new file mode 100644 index 0000000000..0e823af814 --- /dev/null +++ b/x/evm/vm/state_db_snapshot.go @@ -0,0 +1,48 @@ +package vm + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// RtStateDbSnapshot is a snapshot of the state of the entire state database at a particular point in time. +// +// CONTRACT: snapshot & revert must perform deep clone of the underlying state. +type RtStateDbSnapshot struct { + id int + + // context + snapshotCtx sdk.Context + writeFunc func() + + // other states + touched AccountTracker + refund uint64 + selfDestructed AccountTracker + accessList *AccessList2 + logs Logs + transientStorage TransientStorage +} + +func newStateDbSnapshotFromStateDb(stateDb *cStateDb, workingCtx sdk.Context) RtStateDbSnapshot { + cacheCtxForWorking, writeFuncForWorking := workingCtx.CacheContext() + + return RtStateDbSnapshot{ + snapshotCtx: cacheCtxForWorking, + writeFunc: writeFuncForWorking, + touched: stateDb.touched.Copy(), + refund: stateDb.refund, + selfDestructed: stateDb.selfDestructed.Copy(), + accessList: stateDb.accessList.Copy(), + logs: stateDb.logs.Copy(), + transientStorage: stateDb.transientStorage.Clone(), + } +} + +func (s RtStateDbSnapshot) GetID() int { + return s.id +} + +// WriteChanges commit any changes made to the snapshot cache-multi-store to the parent cache-multi-store. +func (s RtStateDbSnapshot) WriteChanges() { + s.writeFunc() +} diff --git a/x/evm/vm/state_db_test_methods.go b/x/evm/vm/state_db_test_methods.go new file mode 100644 index 0000000000..e2159e9c83 --- /dev/null +++ b/x/evm/vm/state_db_test_methods.go @@ -0,0 +1,102 @@ +package vm + +import ( + "github.com/EscanBE/evermint/v12/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" +) + +// ForTest_AddAddressToSelfDestructedList is a test-only method that adds an address to the self-destructed list. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_AddAddressToSelfDestructedList(addr common.Address) { + d.selfDestructed.Add(addr) +} + +// ForTest_SetLogs is a test-only method that override the logs. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_SetLogs(logs Logs) { + d.logs = logs +} + +// ForTest_CountRecordsTransientStorage is a test-only method that returns the number of records in the transient storage. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_CountRecordsTransientStorage() int { + return d.transientStorage.Size() +} + +// ForTest_CloneTransientStorage is a test-only method that clone then returns a copy of the transient storage. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_CloneTransientStorage() TransientStorage { + return d.transientStorage.Clone() +} + +// ForTest_CloneAccessList is a test-only method that clone then returns a copy of the access list. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_CloneAccessList() *AccessList2 { + return d.accessList.Copy() +} + +// ForTest_CloneTouched is a test-only method that clone then returns a copy of the 'touched' list. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_CloneTouched() AccountTracker { + return d.touched.Copy() +} + +// ForTest_CloneSelfDestructed is a test-only method that clone then returns a copy of the 'selfDestructed' list. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_CloneSelfDestructed() AccountTracker { + return d.selfDestructed.Copy() +} + +// ForTest_GetSnapshots is a test-only method that expose the snapshot list +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_GetSnapshots() []RtStateDbSnapshot { + return d.snapshots[:] +} + +// ForTest_ToggleStateDBPreventCommit toggles the flag to prevent committing state changes to the underlying storage. +// This is used for testing purposes to simulate cases where Commit() is failed. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_ToggleStateDBPreventCommit(prevent bool) { + if !utils.IsTestnet(d.currentCtx.ChainID()) { + panic("can only be called during testing") + } + preventCommit = prevent +} + +// ForTest_GetOriginalContext is a test-only method that returns the original context. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_GetOriginalContext() sdk.Context { + return d.originalCtx +} + +// ForTest_GetCurrentContext is a test-only method that returns the current context. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_GetCurrentContext() sdk.Context { + return d.currentCtx +} + +// ForTest_GetEvmDenom is a test-only method that returns the EVM denomination. +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_GetEvmDenom() string { + return d.evmDenom +} + +// ForTest_IsCommitted is a test-only method that returns true if the state was committed before +// +//goland:noinspection GoSnakeCaseUsage +func (d *cStateDb) ForTest_IsCommitted() bool { + return d.committed +} diff --git a/x/evm/vm/state_db_transient_store.go b/x/evm/vm/state_db_transient_store.go new file mode 100644 index 0000000000..b8e0502ba6 --- /dev/null +++ b/x/evm/vm/state_db_transient_store.go @@ -0,0 +1,24 @@ +package vm + +import "github.com/ethereum/go-ethereum/common" + +// TransientStorage is an interface for the transient storage. +// We use this interface instead of export the un-exported type transientStorage +// because we would like to avoid changing much of the existing code so that we can compare the code change easily. +type TransientStorage interface { + Set(addr common.Address, key, value common.Hash) + Get(addr common.Address, key common.Hash) common.Hash + Clone() TransientStorage + Size() int +} + +var _ TransientStorage = transientStorage{} + +// Clone is an alias of Copy. The Copy is defined by g-eth, returns the un-exported type transientStorage. +func (t transientStorage) Clone() TransientStorage { + return t.Copy() +} + +func (t transientStorage) Size() int { + return len(t) +} diff --git a/x/evm/vm/state_db_transient_store_geth.go b/x/evm/vm/state_db_transient_store_geth.go new file mode 100644 index 0000000000..4dfa897881 --- /dev/null +++ b/x/evm/vm/state_db_transient_store_geth.go @@ -0,0 +1,56 @@ +// Copyright 2022 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 vm + +import ( + "github.com/ethereum/go-ethereum/common" + ethstate "github.com/ethereum/go-ethereum/core/state" +) + +// transientStorage is a representation of EIP-1153 "Transient Storage". +type transientStorage map[common.Address]ethstate.Storage + +// newTransientStorage creates a new instance of a transientStorage. +func newTransientStorage() transientStorage { + return make(transientStorage) +} + +// Set sets the transient-storage `value` for `key` at the given `addr`. +func (t transientStorage) Set(addr common.Address, key, value common.Hash) { + if _, ok := t[addr]; !ok { + t[addr] = make(ethstate.Storage) + } + t[addr][key] = value +} + +// Get gets the transient storage for `key` at the given `addr`. +func (t transientStorage) Get(addr common.Address, key common.Hash) common.Hash { + val, ok := t[addr] + if !ok { + return common.Hash{} + } + return val[key] +} + +// Copy does a deep copy of the transientStorage +func (t transientStorage) Copy() transientStorage { + storage := make(transientStorage) + for key, value := range t { + storage[key] = value.Copy() + } + return storage +} diff --git a/x/evm/vm/state_db_transient_store_test.go b/x/evm/vm/state_db_transient_store_test.go new file mode 100644 index 0000000000..e389669768 --- /dev/null +++ b/x/evm/vm/state_db_transient_store_test.go @@ -0,0 +1,63 @@ +package vm + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func Test_TransientStorage(t *testing.T) { + originalStore := newTransientStorage() + require.NotNil(t, originalStore) + require.Empty(t, originalStore) + + addr1 := GenerateAddress() + key1 := GenerateHash() + val1 := GenerateHash() + + require.Equal(t, common.Hash{}, originalStore.Get(addr1, key1), "non-exists address should returns empty hash") + + originalStore.Set(addr1, key1, val1) + require.Equal(t, val1, originalStore.Get(addr1, key1)) + + key2 := GenerateHash() + val2 := GenerateHash() + originalStore.Set(addr1, key2, val2) + require.Equal(t, val2, originalStore.Get(addr1, key2)) + + copiedStore := originalStore.Copy() + + key3 := GenerateHash() + val3 := GenerateHash() + copiedStore.Set(addr1, key3, val3) + require.Equal(t, val3, copiedStore.Get(addr1, key3)) + + addr2 := GenerateAddress() + key4 := GenerateHash() + val4 := GenerateHash() + copiedStore.Set(addr2, key4, val4) + require.Equal(t, val4, copiedStore.Get(addr2, key4)) + + // change address 1 storage in copied store + copiedStore.Set(addr1, key1, val2) + copiedStore.Set(addr1, key2, val3) + copiedStore.Set(addr1, key3, val4) + copiedStore.Set(addr1, key4, val1) + + // assert original value on the original store + require.Equal(t, val1, originalStore.Get(addr1, key1), "original store should not be modified") + require.Equal(t, val2, originalStore.Get(addr1, key2), "original store should not be modified") + require.Equal(t, common.Hash{}, originalStore.Get(addr1, key3), "original store should not have this value") + require.Equal(t, common.Hash{}, originalStore.Get(addr1, key4), "original store should not have this value") + + // update on original store and expect not to effect copied store + originalStore.Set(addr1, key1, val4) + originalStore.Set(addr1, key2, val4) + originalStore.Set(addr1, key3, val4) + + require.Equal(t, val2, copiedStore.Get(addr1, key1), "copied store should reflect changes from original store") + require.Equal(t, val3, copiedStore.Get(addr1, key2), "copied store should reflect changes from original store") + require.Equal(t, val4, copiedStore.Get(addr1, key3), "copied store should reflect changes from original store") + require.Equal(t, val1, copiedStore.Get(addr1, key4), "copied store should reflect changes from original store") +} diff --git a/x/evm/vm/test_utils.go b/x/evm/vm/test_utils.go new file mode 100644 index 0000000000..c75edd666a --- /dev/null +++ b/x/evm/vm/test_utils.go @@ -0,0 +1,32 @@ +package vm + +import ( + "github.com/EscanBE/evermint/v12/crypto/ethsecp256k1" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// NewAddrKey generates an Ethereum address and its corresponding private key. +func NewAddrKey() (common.Address, *ethsecp256k1.PrivKey) { + privkey, _ := ethsecp256k1.GenerateKey() + key, err := privkey.ToECDSA() + if err != nil { + return common.Address{}, nil + } + + addr := crypto.PubkeyToAddress(key.PublicKey) + + return addr, privkey +} + +// GenerateAddress generates an Ethereum address. +func GenerateAddress() common.Address { + addr, _ := NewAddrKey() + return addr +} + +// GenerateHash generates an Ethereum hash. +func GenerateHash() common.Hash { + _, pk := NewAddrKey() + return common.BytesToHash(pk.Bytes()) +} From 671819700c2dfe581d0a50d250374e597a6d0c1a Mon Sep 17 00:00:00 2001 From: VictorTrustyDev Date: Sun, 22 Sep 2024 02:42:03 +0700 Subject: [PATCH 2/6] refactor and integrate Context-based-StateDB to `x/evm` --- x/evm/genesis_test.go | 6 +- x/evm/handler_test.go | 53 +- x/evm/keeper/config.go | 63 +- x/evm/keeper/fees.go | 117 ---- x/evm/keeper/fees_test.go | 568 ------------------ x/evm/keeper/grpc_query.go | 54 +- x/evm/keeper/grpc_query_test.go | 40 +- x/evm/keeper/keeper.go | 65 +- x/evm/keeper/keeper_test.go | 52 +- x/evm/keeper/msg_server_test.go | 4 +- x/evm/keeper/state_transition.go | 88 ++- .../keeper/state_transition_benchmark_test.go | 46 +- x/evm/keeper/state_transition_core.go | 14 +- x/evm/keeper/state_transition_test.go | 88 ++- x/evm/keeper/statedb.go | 209 +------ x/evm/keeper/statedb_test.go | 423 ++++--------- x/evm/keeper/utils_test.go | 6 +- x/evm/types/errors.go | 4 + x/evm/types/params.go | 6 +- x/evm/types/tracer.go | 16 +- x/evm/types/tx.go | 4 +- x/evm/types/utils.go | 2 +- 22 files changed, 445 insertions(+), 1483 deletions(-) delete mode 100644 x/evm/keeper/fees.go delete mode 100644 x/evm/keeper/fees_test.go diff --git a/x/evm/genesis_test.go b/x/evm/genesis_test.go index 434a58539a..8660a397b4 100644 --- a/x/evm/genesis_test.go +++ b/x/evm/genesis_test.go @@ -1,6 +1,7 @@ package evm_test import ( + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" "math/big" vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" @@ -9,7 +10,6 @@ import ( "github.com/EscanBE/evermint/v12/crypto/ethsecp256k1" "github.com/EscanBE/evermint/v12/x/evm" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) @@ -20,7 +20,7 @@ func (suite *EvmTestSuite) TestInitGenesis() { address := common.HexToAddress(privkey.PubKey().Address().String()) - var vmdb *statedb.StateDB + var vmdb evmvm.CStateDB testCases := []struct { name string @@ -148,7 +148,7 @@ func (suite *EvmTestSuite) TestInitGenesis() { vmdb = suite.StateDB() tc.malleate() - err := vmdb.Commit() + err := vmdb.CommitMultiStore(false) suite.Require().NoError(err) if tc.expPanic { diff --git a/x/evm/handler_test.go b/x/evm/handler_test.go index 9a20a12721..3db09b35a1 100644 --- a/x/evm/handler_test.go +++ b/x/evm/handler_test.go @@ -1,6 +1,7 @@ package evm_test import ( + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" "math/big" "testing" "time" @@ -13,8 +14,6 @@ import ( "github.com/EscanBE/evermint/v12/app/helpers" "github.com/EscanBE/evermint/v12/constants" - evmkeeper "github.com/EscanBE/evermint/v12/x/evm/keeper" - sdkmath "cosmossdk.io/math" abci "github.com/cometbft/cometbft/abci/types" tmjson "github.com/cometbft/cometbft/libs/json" @@ -33,7 +32,6 @@ import ( chainapp "github.com/EscanBE/evermint/v12/app" "github.com/EscanBE/evermint/v12/crypto/ethsecp256k1" utiltx "github.com/EscanBE/evermint/v12/testutil/tx" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" "github.com/cometbft/cometbft/crypto/tmhash" @@ -152,8 +150,8 @@ func (suite *EvmTestSuite) SignTx(tx *evmtypes.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()))) +func (suite *EvmTestSuite) StateDB() evmvm.CStateDB { + return evmvm.NewStateDB(suite.ctx, suite.app.EvmKeeper, suite.app.AccountKeeper, suite.app.BankKeeper) } func TestEvmTestSuite(t *testing.T) { @@ -590,20 +588,19 @@ func (suite *EvmTestSuite) deployERC20Contract() common.Address { return crypto.CreateAddress(suite.from, nonce) } -// TestERC20TransferReverted checks: -// - when transaction reverted, gas refund works. -// - when transaction reverted, nonce is still increased. func (suite *EvmTestSuite) TestERC20TransferReverted() { intrinsicGas := uint64(21572) testCases := []struct { - name string - gasLimit uint64 - expErr string + name string + gasLimit uint64 + expErr string + wantGasConsumed uint64 }{ { - name: "default", - gasLimit: intrinsicGas, // enough for intrinsicGas, but not enough for execution - expErr: "out of gas", + name: "default", + gasLimit: intrinsicGas, // enough for intrinsicGas, but not enough for execution + expErr: "out of gas", + wantGasConsumed: intrinsicGas, }, } @@ -612,10 +609,6 @@ func (suite *EvmTestSuite) TestERC20TransferReverted() { suite.SetupTest() k := suite.app.EvmKeeper - // add some fund to pay gas fee - err := k.SetBalance(suite.ctx, suite.from, big.NewInt(1000000000000001)) - suite.Require().NoError(err) - suite.zeroFeeMarket() contract := suite.deployERC20Contract() @@ -638,17 +631,6 @@ func (suite *EvmTestSuite) TestERC20TransferReverted() { tx := evmtypes.NewTx(ethTxParams) suite.SignTx(tx) - before := k.GetBalance(suite.ctx, suite.from) - - baseFee := suite.app.EvmKeeper.GetBaseFee(suite.ctx) - - ethTx := tx.AsTransaction() - - fees, err := evmkeeper.VerifyFee(ethTx, constants.BaseDenom, baseFee, suite.ctx.IsCheckTx()) - suite.Require().NoError(err) - err = k.DeductTxCostsFromUserBalance(suite.ctx, fees, sdk.MustAccAddressFromBech32(tx.From)) - suite.Require().NoError(err) - res, err := k.EthereumTx(suite.ctx, tx) suite.Require().NoError(err) @@ -662,16 +644,7 @@ func (suite *EvmTestSuite) TestERC20TransferReverted() { suite.Require().Empty(receipt.Logs) suite.Require().Equal(evmtypes.EmptyBlockBloom, receipt.Bloom) - after := k.GetBalance(suite.ctx, suite.from) - - if tc.expErr == "out of gas" { - suite.Require().Equal(tc.gasLimit, res.GasUsed) - } else { - suite.Require().Greater(tc.gasLimit, res.GasUsed) - } - - // tx fee should be 100% consumed - suite.Require().Equal(new(big.Int).Sub(before, fees[0].Amount.BigInt()), after) + suite.Require().Equal(tc.gasLimit, res.GasUsed) // nonce should be increased. nonceLater := k.GetNonce(suite.ctx, suite.from) @@ -715,7 +688,7 @@ func (suite *EvmTestSuite) TestContractDeploymentRevert() { // simulate nonce increment and flag set in ante handler db := suite.StateDB() db.SetNonce(suite.from, nonce+1) - suite.Require().NoError(db.Commit()) + suite.Require().NoError(db.CommitMultiStore(false)) suite.app.EvmKeeper.SetFlagSenderNonceIncreasedByAnteHandle(suite.ctx, true) rsp, err := k.EthereumTx(suite.ctx, tx) diff --git a/x/evm/keeper/config.go b/x/evm/keeper/config.go index 2af12b2e49..3f1a885b96 100644 --- a/x/evm/keeper/config.go +++ b/x/evm/keeper/config.go @@ -4,15 +4,17 @@ import ( "math/big" errorsmod "cosmossdk.io/errors" - "github.com/EscanBE/evermint/v12/x/evm/statedb" - evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" ) // EVMConfig creates the EVMConfig based on current state -func (k *Keeper) EVMConfig(ctx sdk.Context, proposerAddress sdk.ConsAddress, chainID *big.Int) (*statedb.EVMConfig, error) { +func (k *Keeper) EVMConfig(ctx sdk.Context, proposerAddress sdk.ConsAddress, chainID *big.Int) (*evmvm.EVMConfig, error) { params := k.GetParams(ctx) ethCfg := params.ChainConfig.EthereumConfig(chainID) @@ -23,7 +25,7 @@ func (k *Keeper) EVMConfig(ctx sdk.Context, proposerAddress sdk.ConsAddress, cha } baseFee := k.GetBaseFee(ctx) - return &statedb.EVMConfig{ + return &evmvm.EVMConfig{ Params: params, ChainConfig: ethCfg, CoinBase: coinbase, @@ -32,30 +34,39 @@ func (k *Keeper) EVMConfig(ctx sdk.Context, proposerAddress sdk.ConsAddress, cha }, nil } -// TxConfig loads `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.GetTxCountTransient(ctx)-1), // TxIndex - uint(k.GetCumulativeLogCountTransient(ctx, true)), // LogIndex - ) +// NewTxConfig loads `TxConfig` from current transient storage. +// Note: if tx is nil, the tx hash and tx type is not set in the TxConfig. +func (k *Keeper) NewTxConfig(ctx sdk.Context, tx *ethtypes.Transaction) evmvm.TxConfig { + txConfig := evmvm.TxConfig{ + BlockHash: common.BytesToHash(ctx.HeaderHash()), + TxHash: common.Hash{}, + TxIndex: uint(k.GetTxCountTransient(ctx) - 1), + LogIndex: uint(k.GetCumulativeLogCountTransient(ctx, true)), + TxType: nil, + } + + if tx != nil { + txConfig.TxHash = tx.Hash() + txConfig = txConfig.WithTxTypeFromTransaction(tx) + } + + return txConfig } -// 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(ctx sdk.Context, cfg *statedb.EVMConfig, tracer vm.EVMLogger) vm.Config { - var debug bool - if tracer != nil { - if _, ok := tracer.(evmtypes.NoOpTracer); !ok { - debug = true - } +// NewTxConfigFromMessage loads `TxConfig` from current transient storage, based on the input core message. +// Note: since the message does not contain the tx hash, it is not set in the TxConfig. +func (k *Keeper) NewTxConfigFromMessage(ctx sdk.Context, msg core.Message) evmvm.TxConfig { + txConfig := evmvm.TxConfig{ + BlockHash: common.BytesToHash(ctx.HeaderHash()), + TxHash: common.Hash{}, + TxIndex: uint(k.GetTxCountTransient(ctx) - 1), + LogIndex: uint(k.GetCumulativeLogCountTransient(ctx, true)), + TxType: nil, } - return vm.Config{ - Debug: debug, - Tracer: tracer, - NoBaseFee: cfg.NoBaseFee || k.IsNoBaseFeeEnabled(ctx), - ExtraEips: cfg.Params.EIPs(), + if msg != nil { + txConfig = txConfig.WithTxTypeFromMessage(msg) } + + return txConfig } diff --git a/x/evm/keeper/fees.go b/x/evm/keeper/fees.go deleted file mode 100644 index f4f6d1ee16..0000000000 --- a/x/evm/keeper/fees.go +++ /dev/null @@ -1,117 +0,0 @@ -package keeper - -import ( - "math/big" - - evmutils "github.com/EscanBE/evermint/v12/x/evm/utils" - "github.com/ethereum/go-ethereum/core" - ethtypes "github.com/ethereum/go-ethereum/core/types" - - errorsmod "cosmossdk.io/errors" - sdkmath "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" - errortypes "github.com/cosmos/cosmos-sdk/types/errors" - authante "github.com/cosmos/cosmos-sdk/x/auth/ante" -) - -// 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( - balance sdkmath.Int, - ethTx *ethtypes.Transaction, -) error { - cost := new(big.Int).Add(evmutils.EthTxFee(ethTx), ethTx.Value()) - - if cost.Sign() < 0 { - return errorsmod.Wrapf( - errortypes.ErrInvalidCoins, - "tx cost (%s) is negative and invalid", cost, - ) - } - - if balance.IsNegative() || balance.BigInt().Cmp(cost) < 0 { - return errorsmod.Wrapf( - errortypes.ErrInsufficientFunds, - "sender balance < tx cost (%s < %s)", balance, cost, - ) - } - return nil -} - -// DeductTxCostsFromUserBalance deducts the fees from the user balance. Returns an -// error if the specified sender address does not exist or the account balance is not sufficient. -func (k *Keeper) DeductTxCostsFromUserBalance( - ctx sdk.Context, - fees sdk.Coins, - from sdk.AccAddress, -) error { - // fetch sender account - signerAcc, err := authante.GetSignerAcc(ctx, k.accountKeeper, from) - if err != nil { - return errorsmod.Wrapf(err, "account not found for sender %s", from) - } - - // deduct the full gas cost from the user balance - if err := authante.DeductFees(k.bankKeeper, ctx, signerAcc, fees); err != nil { - return errorsmod.Wrapf(err, "failed to deduct full gas cost %s from the user %s balance", fees, from) - } - - return nil -} - -// VerifyFee is used to return the fee for the given transaction data in sdk.Coins. -// It checks: -// - Gas limit vs intrinsic gas -// - Base fee vs gas fee cap -// -// TODO ES: remove? -func VerifyFee( - ethTx *ethtypes.Transaction, - denom string, - baseFee sdkmath.Int, - isCheckTx bool, -) (sdk.Coins, error) { - isContractCreation := ethTx.To() == nil - - gasLimit := ethTx.Gas() - - var accessList ethtypes.AccessList - if ethTx.AccessList() != nil { - accessList = ethTx.AccessList() - } - - // intrinsic gas verification during CheckTx - if isCheckTx { - const homestead = true - const istanbul = true - intrinsicGas, err := core.IntrinsicGas(ethTx.Data(), accessList, isContractCreation, homestead, istanbul) - if err != nil { - return nil, errorsmod.Wrapf( - err, - "failed to retrieve intrinsic gas, contract creation = %t", isContractCreation, - ) - } - - if gasLimit < intrinsicGas { - return nil, errorsmod.Wrapf( - errortypes.ErrOutOfGas, - "gas limit too low: %d (gas limit) < %d (intrinsic gas)", gasLimit, intrinsicGas, - ) - } - } - - if ethTx.GasFeeCap().Cmp(baseFee.BigInt()) < 0 { - return nil, errorsmod.Wrapf(errortypes.ErrInsufficientFee, - "the tx gasfeecap is lower than the tx baseFee: %s (gasfeecap), %s (basefee) ", - ethTx.GasFeeCap(), - baseFee) - } - - feeAmt := evmutils.EthTxEffectiveFee(ethTx, baseFee) - if feeAmt.Sign() == 0 { - // zero fee, no need to deduct - return sdk.Coins{}, nil - } - - return sdk.Coins{{Denom: denom, Amount: sdkmath.NewIntFromBigInt(feeAmt)}}, nil -} diff --git a/x/evm/keeper/fees_test.go b/x/evm/keeper/fees_test.go deleted file mode 100644 index 6ecdf97d29..0000000000 --- a/x/evm/keeper/fees_test.go +++ /dev/null @@ -1,568 +0,0 @@ -package keeper_test - -import ( - "fmt" - "math/big" - - evmutils "github.com/EscanBE/evermint/v12/x/evm/utils" - - sdkmath "cosmossdk.io/math" - evmkeeper "github.com/EscanBE/evermint/v12/x/evm/keeper" - evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" - ethparams "github.com/ethereum/go-ethereum/params" -) - -func (suite *KeeperTestSuite) TestCheckSenderBalance() { - hundredInt := sdkmath.NewInt(100) - zeroInt := sdkmath.ZeroInt() - oneInt := sdkmath.OneInt() - fiveInt := sdkmath.NewInt(5) - fiftyInt := sdkmath.NewInt(50) - negInt := sdkmath.NewInt(-10) - - testCases := []struct { - name string - to string - gasLimit uint64 - gasPrice *sdkmath.Int - gasFeeCap *big.Int - gasTipCap *big.Int - cost *sdkmath.Int - from string - accessList *ethtypes.AccessList - expectPass bool - expectPanic bool - enableFeemarket bool - }{ - { - name: "Enough balance", - to: suite.address.String(), - gasLimit: 10, - gasPrice: &oneInt, - cost: &oneInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: true, - }, - { - name: "Equal balance", - to: suite.address.String(), - gasLimit: 99, - gasPrice: &oneInt, - cost: &oneInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: true, - }, - { - name: "negative cost", - to: suite.address.String(), - gasLimit: 1, - gasPrice: &oneInt, - cost: &negInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPanic: true, - }, - { - name: "Higher gas limit, not enough balance", - to: suite.address.String(), - gasLimit: 100, - gasPrice: &oneInt, - cost: &oneInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: false, - }, - { - name: "Higher gas price, enough balance", - to: suite.address.String(), - gasLimit: 10, - gasPrice: &fiveInt, - cost: &oneInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: true, - }, - { - name: "Higher gas price, not enough balance", - to: suite.address.String(), - gasLimit: 20, - gasPrice: &fiveInt, - cost: &oneInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: false, - }, - { - name: "Higher cost, enough balance", - to: suite.address.String(), - gasLimit: 10, - gasPrice: &fiveInt, - cost: &fiftyInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: true, - }, - { - name: "Higher cost, not enough balance", - to: suite.address.String(), - gasLimit: 10, - gasPrice: &fiveInt, - cost: &hundredInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: false, - }, - { - name: "Enough balance w/ enableFeemarket", - to: suite.address.String(), - gasLimit: 10, - gasFeeCap: big.NewInt(1), - cost: &oneInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: true, - enableFeemarket: true, - }, - { - name: "Equal balance w/ enableFeemarket", - to: suite.address.String(), - gasLimit: 99, - gasFeeCap: big.NewInt(1), - cost: &oneInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: true, - enableFeemarket: true, - }, - { - name: "negative cost w/ enableFeemarket", - to: suite.address.String(), - gasLimit: 1, - gasFeeCap: big.NewInt(1), - cost: &negInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPanic: true, - enableFeemarket: true, - }, - { - name: "Higher gas limit, not enough balance w/ enableFeemarket", - to: suite.address.String(), - gasLimit: 100, - gasFeeCap: big.NewInt(1), - cost: &oneInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: false, - enableFeemarket: true, - }, - { - name: "Higher gas price, enough balance w/ enableFeemarket", - to: suite.address.String(), - gasLimit: 10, - gasFeeCap: big.NewInt(5), - cost: &oneInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: true, - enableFeemarket: true, - }, - { - name: "Higher gas price, not enough balance w/ enableFeemarket", - to: suite.address.String(), - gasLimit: 20, - gasFeeCap: big.NewInt(5), - cost: &oneInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: false, - enableFeemarket: true, - }, - { - name: "Higher cost, enough balance w/ enableFeemarket", - to: suite.address.String(), - gasLimit: 10, - gasFeeCap: big.NewInt(5), - cost: &fiftyInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: true, - enableFeemarket: true, - }, - { - name: "Higher cost, not enough balance w/ enableFeemarket", - to: suite.address.String(), - gasLimit: 10, - gasFeeCap: big.NewInt(5), - cost: &hundredInt, - from: suite.address.String(), - accessList: ðtypes.AccessList{}, - expectPass: false, - enableFeemarket: true, - }, - } - - vmdb := suite.StateDB() - vmdb.AddBalance(suite.address, hundredInt.BigInt()) - balance := vmdb.GetBalance(suite.address) - suite.Require().Equal(balance, hundredInt.BigInt()) - err := vmdb.Commit() - suite.Require().NoError(err, "Unexpected error while committing to vmdb: %d", err) - - for _, tc := range testCases { - suite.Run(tc.name, func() { - to := common.HexToAddress(tc.from) - - var amount, gasPrice, gasFeeCap, gasTipCap *big.Int - if tc.cost != nil { - amount = tc.cost.BigInt() - } - - if tc.enableFeemarket { - gasFeeCap = tc.gasFeeCap - if tc.gasTipCap == nil { - gasTipCap = oneInt.BigInt() - } else { - gasTipCap = tc.gasTipCap - } - } else if tc.gasPrice != nil { - gasPrice = tc.gasPrice.BigInt() - } - - ethTxParams := &evmtypes.EvmTxArgs{ - From: common.HexToAddress(tc.from), - ChainID: zeroInt.BigInt(), - Nonce: 1, - To: &to, - Amount: amount, - GasLimit: tc.gasLimit, - GasPrice: gasPrice, - GasFeeCap: gasFeeCap, - GasTipCap: gasTipCap, - Accesses: tc.accessList, - } - - if tc.expectPanic { - suite.Require().Panics(func() { - _ = evmtypes.NewTx(ethTxParams) - }) - return - } - - tx := evmtypes.NewTx(ethTxParams) - ethTx := tx.AsTransaction() - - acct := suite.app.EvmKeeper.GetAccountOrEmpty(suite.ctx, suite.address) - err := evmkeeper.CheckSenderBalance( - sdkmath.NewIntFromBigInt(acct.Balance), - ethTx, - ) - - if tc.expectPass { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } -} - -// TestVerifyFeeAndDeductTxCostsFromUserBalance is a test method for both the VerifyFee -// function and the DeductTxCostsFromUserBalance method. -// -// NOTE: This method combines testing for both functions, because these used to be -// in one function and share a lot of the same setup. -// In practice, the two tested functions will also be sequentially executed. -func (suite *KeeperTestSuite) TestVerifyFeeAndDeductTxCostsFromUserBalance() { - hundredInt := sdkmath.NewInt(100) - zeroInt := sdkmath.ZeroInt() - oneInt := sdkmath.NewInt(1) - fiveInt := sdkmath.NewInt(5) - fiftyInt := sdkmath.NewInt(50) - - // should be enough to cover all test cases - initBalance := sdkmath.NewInt((ethparams.InitialBaseFee + 10) * 105) - - testCases := []struct { - name string - gasLimit uint64 - gasPrice *sdkmath.Int - gasFeeCap *big.Int - gasTipCap *big.Int - cost *sdkmath.Int - accessList *ethtypes.AccessList - expectPassVerify bool - expectPassDeduct bool - enableFeemarket bool - from string - malleate func() - }{ - { - name: "Enough balance", - gasLimit: 10, - gasPrice: &oneInt, - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: true, - from: suite.address.String(), - }, - { - name: "zero fee without fee market enable", - gasLimit: 10, - gasPrice: &zeroInt, - cost: &zeroInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: true, - enableFeemarket: false, - from: suite.address.String(), - }, - { - name: "Equal balance", - gasLimit: 100, - gasPrice: &oneInt, - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: true, - from: suite.address.String(), - }, - { - name: "Higher gas limit, not enough balance", - gasLimit: 105, - gasPrice: &oneInt, - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: false, - from: suite.address.String(), - }, - { - name: "Higher gas price, enough balance", - gasLimit: 20, - gasPrice: &fiveInt, - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: true, - from: suite.address.String(), - }, - { - name: "Higher gas price, not enough balance", - gasLimit: 20, - gasPrice: &fiftyInt, - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: false, - from: suite.address.String(), - }, - // This case is expected to be true because the fees can be deducted, but the tx - // execution is going to fail because there is no more balance to pay the cost - { - name: "Higher cost, enough balance", - gasLimit: 100, - gasPrice: &oneInt, - cost: &fiftyInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: true, - from: suite.address.String(), - }, - // testcases with enableFeemarket enabled. - { - name: "Invalid gasFeeCap w/ enableFeemarket", - gasLimit: 10, - gasFeeCap: big.NewInt(1), - gasTipCap: big.NewInt(1), - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: false, - expectPassDeduct: true, - enableFeemarket: true, - from: suite.address.String(), - }, - { - name: "empty tip fee is valid to deduct", - gasLimit: 10, - gasFeeCap: big.NewInt(ethparams.InitialBaseFee), - gasTipCap: big.NewInt(1), - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: true, - enableFeemarket: true, - from: suite.address.String(), - }, - { - name: "zero fee with fee market enabled", - gasLimit: 10, - gasFeeCap: big.NewInt(ethparams.InitialBaseFee), - gasPrice: &zeroInt, - cost: &zeroInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: true, - enableFeemarket: true, - from: suite.address.String(), - }, - { - name: "effectiveTip equal to gasTipCap", - gasLimit: 100, - gasFeeCap: big.NewInt(ethparams.InitialBaseFee + 2), - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: true, - enableFeemarket: true, - from: suite.address.String(), - }, - { - name: "effectiveTip equal to (gasFeeCap - baseFee)", - gasLimit: 105, - gasFeeCap: big.NewInt(ethparams.InitialBaseFee + 1), - gasTipCap: big.NewInt(2), - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: true, - enableFeemarket: true, - from: suite.address.String(), - }, - { - name: "Invalid from address", - gasLimit: 10, - gasPrice: &oneInt, - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: true, - expectPassDeduct: false, - from: "abcdef", - }, - { - name: "Enough balance - with access list", - gasLimit: 10, - gasPrice: &oneInt, - cost: &oneInt, - accessList: ðtypes.AccessList{ - ethtypes.AccessTuple{ - Address: suite.address, - StorageKeys: []common.Hash{}, - }, - }, - expectPassVerify: true, - expectPassDeduct: true, - from: suite.address.String(), - }, - { - name: "gasLimit < intrinsicGas during IsCheckTx", - gasLimit: 1, - gasPrice: &oneInt, - cost: &oneInt, - accessList: ðtypes.AccessList{}, - expectPassVerify: false, - expectPassDeduct: true, - from: suite.address.String(), - malleate: func() { - suite.ctx = suite.ctx.WithIsCheckTx(true) - }, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.enableFeemarket = tc.enableFeemarket - suite.SetupTest() - vmdb := suite.StateDB() - - if tc.malleate != nil { - tc.malleate() - } - var amount, gasPrice, gasFeeCap, gasTipCap *big.Int - if tc.cost != nil { - amount = tc.cost.BigInt() - } - - if suite.enableFeemarket { - if tc.gasFeeCap != nil { - gasFeeCap = tc.gasFeeCap - } - if tc.gasTipCap == nil { - gasTipCap = oneInt.BigInt() - } else { - gasTipCap = tc.gasTipCap - } - 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() - } - - vmdb.AddBalance(suite.address, hundredInt.BigInt()) - balance := vmdb.GetBalance(suite.address) - suite.Require().Equal(balance, hundredInt.BigInt()) - } - err := vmdb.Commit() - suite.Require().NoError(err) - - ethTxParams := &evmtypes.EvmTxArgs{ - From: common.HexToAddress(tc.from), - ChainID: zeroInt.BigInt(), - Nonce: 1, - To: &suite.address, - Amount: amount, - GasLimit: tc.gasLimit, - GasPrice: gasPrice, - GasFeeCap: gasFeeCap, - GasTipCap: gasTipCap, - Accesses: tc.accessList, - } - tx := evmtypes.NewTx(ethTxParams) - ethTx := tx.AsTransaction() - - baseFee := suite.app.EvmKeeper.GetBaseFee(suite.ctx) - priority := evmutils.EthTxPriority(ethTx, baseFee) - suite.Require().Equal(evmutils.EthTxEffectiveGasPrice(ethTx, baseFee).String(), fmt.Sprintf("%d", priority)) - - fees, err := evmkeeper.VerifyFee(ethTx, evmtypes.DefaultEVMDenom, baseFee, suite.ctx.IsCheckTx()) - if tc.expectPassVerify { - suite.Require().NoError(err) - if tc.enableFeemarket { - baseFee := suite.app.FeeMarketKeeper.GetBaseFee(suite.ctx) - suite.Require().Equal( - fees, - sdk.NewCoins( - sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewIntFromBigInt(evmutils.EthTxEffectiveFee(ethTx, baseFee))), - ), - ) - } else { - suite.Require().Equal( - fees, - sdk.NewCoins( - sdk.NewCoin(evmtypes.DefaultEVMDenom, tc.gasPrice.Mul(sdkmath.NewIntFromUint64(tc.gasLimit))), - ), - ) - } - } else { - suite.Require().Error(err) - suite.Require().Nil(fees) - } - - err = suite.app.EvmKeeper.DeductTxCostsFromUserBalance(suite.ctx, fees, sdk.MustAccAddressFromBech32(tx.From)) - if tc.expectPassDeduct { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } - suite.enableFeemarket = false // reset flag -} diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index 42561cf714..bccd40c3bb 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" "math/big" "time" @@ -25,11 +26,10 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" ethparams "github.com/ethereum/go-ethereum/params" evertypes "github.com/EscanBE/evermint/v12/types" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" ) @@ -51,15 +51,18 @@ func (k Keeper) Account(c context.Context, req *evmtypes.QueryAccountRequest) (* ) } + ctx := sdk.UnwrapSDKContext(c) addr := common.HexToAddress(req.Address) - ctx := sdk.UnwrapSDKContext(c) - acct := k.GetAccountOrEmpty(ctx, addr) + var nonce uint64 + if acc := k.accountKeeper.GetAccount(ctx, addr.Bytes()); acc != nil { + nonce = acc.GetSequence() + } return &evmtypes.QueryAccountResponse{ - Balance: acct.Balance.String(), - CodeHash: common.BytesToHash(acct.CodeHash).Hex(), - Nonce: acct.Nonce, + Nonce: nonce, + Balance: k.GetBalance(ctx, addr).String(), + CodeHash: k.GetCodeHash(ctx, addr.Bytes()).Hex(), }, nil } @@ -146,10 +149,10 @@ func (k Keeper) Balance(c context.Context, req *evmtypes.QueryBalanceRequest) (* ctx := sdk.UnwrapSDKContext(c) - balanceInt := k.GetBalance(ctx, common.HexToAddress(req.Address)) + balance := k.GetBalance(ctx, common.HexToAddress(req.Address)) return &evmtypes.QueryBalanceResponse{ - Balance: balanceInt.String(), + Balance: balance.String(), }, nil } @@ -195,11 +198,11 @@ func (k Keeper) Code(c context.Context, req *evmtypes.QueryCodeRequest) (*evmtyp ctx := sdk.UnwrapSDKContext(c) address := common.HexToAddress(req.Address) - acct := k.GetAccountWithoutBalance(ctx, address) + codeHash := k.GetCodeHash(ctx, address.Bytes()) var code []byte - if acct != nil && acct.IsContract() { - code = k.GetCode(ctx, common.BytesToHash(acct.CodeHash)) + if !evmtypes.IsEmptyCodeHash(codeHash) { + code = k.GetCode(ctx, codeHash) } return &evmtypes.QueryCodeResponse{ @@ -250,7 +253,7 @@ func (k Keeper) EthCall(c context.Context, req *evmtypes.EthCallRequest) (*evmty return nil, status.Error(codes.InvalidArgument, err.Error()) } - txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash())).WithTxTypeFromMessage(msg) + txConfig := k.NewTxConfigFromMessage(ctx, msg) // pass false to not commit StateDB res, err := k.ApplyMessageWithConfig(ctx, msg, nil, false, cfg, txConfig) @@ -323,8 +326,6 @@ func (k Keeper) EstimateGas(c context.Context, req *evmtypes.EthCallRequest) (*e nonce := k.GetNonce(ctx, args.GetFrom()) args.Nonce = (*hexutil.Uint64)(&nonce) - txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash())) - // convert the tx args to an ethereum message msg, err := args.ToMessage(req.GasCap, cfg.BaseFee) if err != nil { @@ -350,7 +351,8 @@ func (k Keeper) EstimateGas(c context.Context, req *evmtypes.EthCallRequest) (*e msg.AccessList(), msg.IsFake(), ) - txConfig = txConfig.WithTxTypeFromMessage(msg) + + txConfig := k.NewTxConfigFromMessage(ctx, msg) // pass false to not commit StateDB rsp, err = k.ApplyMessageWithConfig(ctx, msg, nil, false, cfg, txConfig) @@ -377,8 +379,8 @@ func (k Keeper) EstimateGas(c context.Context, req *evmtypes.EthCallRequest) (*e } if failed { - if result != nil && result.VmError != vm.ErrOutOfGas.Error() { - if result.VmError == vm.ErrExecutionReverted.Error() { + if result != nil && result.VmError != corevm.ErrOutOfGas.Error() { + if result.VmError == corevm.ErrExecutionReverted.Error() { return nil, evmtypes.NewExecErrorWithReason(result.Ret) } return nil, errors.New(result.VmError) @@ -434,8 +436,7 @@ func (k Keeper) TraceTx(c context.Context, req *evmtypes.QueryTraceTxRequest) (* cfg.BaseFee = k.feeMarketKeeper.GetBaseFee(ctx).BigInt() - txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash())) - + txConfig := k.NewTxConfig(ctx, nil) for i, tx := range req.Predecessors { ethTx := tx.AsTransaction() msg, err := ethTx.AsMessage(signer, cfg.BaseFee) @@ -444,7 +445,7 @@ func (k Keeper) TraceTx(c context.Context, req *evmtypes.QueryTraceTxRequest) (* } txConfig.TxHash = ethTx.Hash() txConfig.TxIndex = uint(i) - txConfig = txConfig.WithTxTypeFromMessage(msg) + txConfig = txConfig.WithTxTypeFromTransaction(ethTx) // reset gas meter per transaction to avoid stacking the gas used of every predecessor in the same gas meter ctx = ctx.WithGasMeter(evertypes.NewInfiniteGasMeterWithLimit(msg.Gas())) @@ -538,12 +539,13 @@ func (k Keeper) TraceBlock(c context.Context, req *evmtypes.QueryTraceBlockReque txsLength := len(req.Txs) results := make([]*evmtypes.TxTraceResult, 0, txsLength) - txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash())) + txConfig := k.NewTxConfig(ctx, nil) for i, tx := range req.Txs { result := evmtypes.TxTraceResult{} ethTx := tx.AsTransaction() txConfig.TxHash = ethTx.Hash() txConfig.TxIndex = uint(i) + txConfig = txConfig.WithTxTypeFromTransaction(ethTx) traceResult, logIndex, err := k.traceTx(ctx, cfg, txConfig, signer, ethTx, req.TraceConfig, true, nil) if err != nil { result.Error = err.Error() @@ -567,8 +569,8 @@ func (k Keeper) TraceBlock(c context.Context, req *evmtypes.QueryTraceBlockReque // traceTx do trace on one transaction, it returns a tuple: (traceResult, nextLogIndex, error). func (k *Keeper) traceTx( ctx sdk.Context, - cfg *statedb.EVMConfig, - txConfig statedb.TxConfig, + cfg *evmvm.EVMConfig, + txConfig evmvm.TxConfig, signer ethtypes.Signer, tx *ethtypes.Transaction, traceConfig *evmtypes.TraceConfig, @@ -582,11 +584,13 @@ func (k *Keeper) traceTx( err error timeout = defaultTraceTimeout ) + + txConfig = txConfig.WithTxTypeFromTransaction(tx) + msg, err := tx.AsMessage(signer, cfg.BaseFee) if err != nil { return nil, 0, status.Error(codes.Internal, err.Error()) } - txConfig = txConfig.WithTxTypeFromMessage(msg) if traceConfig == nil { traceConfig = &evmtypes.TraceConfig{} diff --git a/x/evm/keeper/grpc_query_test.go b/x/evm/keeper/grpc_query_test.go index 73a772c001..a1f25a655a 100644 --- a/x/evm/keeper/grpc_query_test.go +++ b/x/evm/keeper/grpc_query_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "encoding/json" + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" "math/big" storetypes "cosmossdk.io/store/types" @@ -13,14 +14,13 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" ethlogger "github.com/ethereum/go-ethereum/eth/tracers/logger" ethparams "github.com/ethereum/go-ethereum/params" "github.com/EscanBE/evermint/v12/server/config" utiltx "github.com/EscanBE/evermint/v12/testutil/tx" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" ) @@ -245,12 +245,12 @@ func (suite *KeeperTestSuite) TestQueryStorage() { testCases := []struct { name string - malleate func(vm.StateDB) + malleate func(corevm.StateDB) expPass bool }{ { name: "fail - invalid address", - malleate: func(vm.StateDB) { + malleate: func(corevm.StateDB) { req = &evmtypes.QueryStorageRequest{ Address: invalidAddress, } @@ -259,7 +259,7 @@ func (suite *KeeperTestSuite) TestQueryStorage() { }, { name: "pass", - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { key := common.BytesToHash([]byte("key")) value := common.BytesToHash([]byte("value")) expValue = value.String() @@ -279,7 +279,7 @@ func (suite *KeeperTestSuite) TestQueryStorage() { vmdb := suite.StateDB() tc.malleate(vmdb) - suite.Require().NoError(vmdb.Commit()) + suite.Require().NoError(vmdb.CommitMultiStore(false)) res, err := suite.queryClient.Storage(suite.ctx, req) @@ -303,12 +303,12 @@ func (suite *KeeperTestSuite) TestQueryCode() { testCases := []struct { name string - malleate func(vm.StateDB) + malleate func(corevm.StateDB) expPass bool }{ { name: "fail - invalid address", - malleate: func(vm.StateDB) { + malleate: func(corevm.StateDB) { req = &evmtypes.QueryCodeRequest{ Address: invalidAddress, } @@ -319,7 +319,7 @@ func (suite *KeeperTestSuite) TestQueryCode() { }, { name: "pass", - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { expCode = []byte("code") vmdb.SetCode(suite.address, expCode) @@ -337,7 +337,7 @@ func (suite *KeeperTestSuite) TestQueryCode() { vmdb := suite.StateDB() tc.malleate(vmdb) - suite.Require().NoError(vmdb.Commit()) + suite.Require().NoError(vmdb.CommitMultiStore(false)) res, err := suite.queryClient.Code(suite.ctx, req) @@ -361,17 +361,17 @@ func (suite *KeeperTestSuite) TestQueryTxLogs() { testCases := []struct { name string - malleate func(vm.StateDB) + malleate func(corevm.StateDB) }{ { name: "pass - empty logs", - malleate: func(vm.StateDB) { - expLogs = nil + malleate: func(corevm.StateDB) { + expLogs = []*ethtypes.Log{} }, }, { name: "pass - correct log", - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { expLogs = []*ethtypes.Log{ { Address: suite.address, @@ -397,11 +397,11 @@ func (suite *KeeperTestSuite) TestQueryTxLogs() { suite.Run(tc.name, func() { suite.SetupTest() // reset - vmdb := statedb.New(suite.ctx, suite.app.EvmKeeper, statedb.NewTxConfig(common.BytesToHash(suite.ctx.HeaderHash()), txHash, txIndex, logIndex)) + vmdb := evmvm.NewStateDB(suite.ctx, suite.app.EvmKeeper, suite.app.AccountKeeper, suite.app.BankKeeper) tc.malleate(vmdb) - suite.Require().NoError(vmdb.Commit()) + suite.Require().NoError(vmdb.CommitMultiStore(false)) - logs := vmdb.Logs() + logs := vmdb.GetTransactionLogs() suite.Require().Equal(expLogs, logs) }) } @@ -859,7 +859,7 @@ func (suite *KeeperTestSuite) TestTraceTx() { // increase nonce to avoid address collision vmdb := suite.StateDB() vmdb.SetNonce(suite.address, vmdb.GetNonce(suite.address)+1) - suite.Require().NoError(vmdb.Commit()) + suite.Require().NoError(vmdb.CommitMultiStore(false)) contractAddr := suite.DeployTestContract(suite.T(), suite.address, sdkmath.NewIntWithDecimal(1000, 18).BigInt()) suite.Commit() @@ -927,7 +927,7 @@ func (suite *KeeperTestSuite) TestTraceTx() { // increase nonce to avoid address collision vmdb := suite.StateDB() vmdb.SetNonce(suite.address, vmdb.GetNonce(suite.address)+1) - suite.Require().NoError(vmdb.Commit()) + suite.Require().NoError(vmdb.CommitMultiStore(false)) chainID := suite.app.EvmKeeper.ChainID() nonce := suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address) @@ -1101,7 +1101,7 @@ func (suite *KeeperTestSuite) TestTraceBlock() { // increase nonce to avoid address collision vmdb := suite.StateDB() vmdb.SetNonce(suite.address, vmdb.GetNonce(suite.address)+1) - suite.Require().NoError(vmdb.Commit()) + suite.Require().NoError(vmdb.CommitMultiStore(false)) contractAddr := suite.DeployTestContract(suite.T(), suite.address, sdkmath.NewIntWithDecimal(1000, 18).BigInt()) suite.Commit() diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index ce0a66ce05..ccd88bd47b 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -14,19 +14,14 @@ import ( "cosmossdk.io/log" storetypes "cosmossdk.io/store/types" + evertypes "github.com/EscanBE/evermint/v12/types" "github.com/EscanBE/evermint/v12/utils" + evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" paramstypes "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" - ethparams "github.com/ethereum/go-ethereum/params" - - evertypes "github.com/EscanBE/evermint/v12/types" - "github.com/EscanBE/evermint/v12/x/evm/statedb" - evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" ) // Keeper grants access to the EVM module state and implements the go-ethereum StateDB interface. @@ -206,13 +201,6 @@ func (k Keeper) SetLogCountForCurrentTxTransient(ctx sdk.Context, count uint64) store.Set(evmtypes.TxLogCountTransientKey(txIdx), sdk.Uint64ToBigEndian(count)) } -// GetLogCountForTdxIndexTransient returns log count for tx by index from the transient store. -func (k Keeper) GetLogCountForTdxIndexTransient(ctx sdk.Context, txIdx uint64) uint64 { - store := ctx.TransientStore(k.transientKey) - bz := store.Get(evmtypes.TxLogCountTransientKey(txIdx)) - return sdk.BigEndianToUint64(bz) -} - // GetCumulativeLogCountTransient returns the total log count for all transactions in the current block. func (k Keeper) GetCumulativeLogCountTransient(ctx sdk.Context, exceptCurrent bool) uint64 { store := ctx.TransientStore(k.transientKey) @@ -335,44 +323,29 @@ func (k Keeper) GetAccountStorage(ctx sdk.Context, address common.Address) evmty // Account // ---------------------------------------------------------------------------- -// Tracer return a default vm.Tracer based on current keeper state -func (k Keeper) Tracer(ctx sdk.Context, msg core.Message, ethCfg *ethparams.ChainConfig) vm.EVMLogger { - return evmtypes.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 { - cosmosAddr := sdk.AccAddress(addr.Bytes()) - acct := k.accountKeeper.GetAccount(ctx, cosmosAddr) - if acct == nil { - return nil +// IsEmptyAccount returns true if the account is empty, decided by: +// - Nonce is zero +// - Balance is zero +// - CodeHash is empty +func (k *Keeper) IsEmptyAccount(ctx sdk.Context, addr common.Address) bool { + if codeHash := k.GetCodeHash(ctx, addr.Bytes()); !evmtypes.IsEmptyCodeHash(codeHash) { + return false } - return &statedb.Account{ - Nonce: acct.GetSequence(), - CodeHash: k.GetCodeHash(ctx, addr.Bytes()).Bytes(), + if coins := k.bankKeeper.GetAllBalances(ctx, addr.Bytes()); !coins.IsZero() { + return false } -} -// GetAccountOrEmpty returns empty account if not exist -func (k *Keeper) GetAccountOrEmpty(ctx sdk.Context, addr common.Address) statedb.Account { - acct := k.GetAccount(ctx, addr) - if acct != nil { - return *acct + if acc := k.accountKeeper.GetAccount(ctx, addr.Bytes()); acc != nil && acc.GetSequence() > 0 { + return false } - // empty account - return statedb.Account{ - Balance: new(big.Int), - CodeHash: evmtypes.EmptyCodeHash, - } + return true } // 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) + acct := k.accountKeeper.GetAccount(ctx, addr.Bytes()) if acct == nil { return 0 } @@ -380,17 +353,15 @@ func (k *Keeper) GetNonce(ctx sdk.Context, addr common.Address) uint64 { return acct.GetSequence() } -// GetBalance load account's balance of gas token +// GetBalance returns account token balance based on EVM denom. func (k *Keeper) GetBalance(ctx sdk.Context, addr common.Address) *big.Int { cosmosAddr := sdk.AccAddress(addr.Bytes()) evmParams := k.GetParams(ctx) - evmDenom := evmParams.GetEvmDenom() // if node is pruned, params is empty. Return invalid value - if evmDenom == "" { + if evmParams.EvmDenom == "" { return big.NewInt(-1) } - coin := k.bankKeeper.GetBalance(ctx, cosmosAddr, evmDenom) - return coin.Amount.BigInt() + return k.bankKeeper.GetBalance(ctx, cosmosAddr, evmParams.EvmDenom).Amount.BigInt() } // GetBaseFee returns current base fee. diff --git a/x/evm/keeper/keeper_test.go b/x/evm/keeper/keeper_test.go index 9aec8dc280..7efc21bff9 100644 --- a/x/evm/keeper/keeper_test.go +++ b/x/evm/keeper/keeper_test.go @@ -2,6 +2,8 @@ package keeper_test import ( _ "embed" + "github.com/EscanBE/evermint/v12/testutil" + utiltx "github.com/EscanBE/evermint/v12/testutil/tx" "math/big" storetypes "cosmossdk.io/store/types" @@ -12,7 +14,6 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" evmkeeper "github.com/EscanBE/evermint/v12/x/evm/keeper" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" "github.com/ethereum/go-ethereum/common" @@ -158,22 +159,19 @@ func (suite *KeeperTestSuite) TestGetAccountStorage() { } } -func (suite *KeeperTestSuite) TestGetAccountOrEmpty() { - empty := statedb.Account{ - Balance: new(big.Int), - CodeHash: evmtypes.EmptyCodeHash, - } - - supply := big.NewInt(100) - contractAddr := suite.DeployTestContract(suite.T(), suite.address, supply) +func (suite *KeeperTestSuite) TestIsEmptyAccount() { + contractAddr := suite.DeployTestContract(suite.T(), suite.address, common.Big1) + randomAddr1 := utiltx.GenerateAddress() + randomAddr2 := utiltx.GenerateAddress() testCases := []struct { name string addr common.Address + malleate func() expEmpty bool }{ { - name: "unexisting account - get empty", + name: "non-existing account", addr: common.Address{}, expEmpty: true, }, @@ -182,16 +180,40 @@ func (suite *KeeperTestSuite) TestGetAccountOrEmpty() { addr: contractAddr, expEmpty: false, }, + { + name: "account with balance", + addr: randomAddr1, + malleate: func() { + err := testutil.FundAccount( + suite.ctx, + suite.app.BankKeeper, + randomAddr1.Bytes(), + sdk.NewCoins(sdk.NewInt64Coin(constants.BaseDenom, 1)), + ) + suite.Require().NoError(err) + }, + expEmpty: false, + }, + { + name: "account with nonce", + addr: randomAddr2, + malleate: func() { + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, randomAddr2.Bytes()) + err := acc.SetSequence(1) + suite.Require().NoError(err) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + }, + expEmpty: false, + }, } for _, tc := range testCases { suite.Run(tc.name, func() { - res := suite.app.EvmKeeper.GetAccountOrEmpty(suite.ctx, tc.addr) - if tc.expEmpty { - suite.Require().Equal(empty, res) - } else { - suite.Require().NotEqual(empty, res) + if tc.malleate != nil { + tc.malleate() } + gotEmpty := suite.app.EvmKeeper.IsEmptyAccount(suite.ctx, tc.addr) + suite.Require().Equal(tc.expEmpty, gotEmpty) }) } } diff --git a/x/evm/keeper/msg_server_test.go b/x/evm/keeper/msg_server_test.go index c44f831e3c..2e1b9d81e7 100644 --- a/x/evm/keeper/msg_server_test.go +++ b/x/evm/keeper/msg_server_test.go @@ -1,12 +1,12 @@ package keeper_test import ( + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" "math/big" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" ethtypes "github.com/ethereum/go-ethereum/core/types" ethparams "github.com/ethereum/go-ethereum/params" @@ -17,7 +17,7 @@ func (suite *KeeperTestSuite) TestEthereumTx() { err error msg *evmtypes.MsgEthereumTx signer ethtypes.Signer - vmdb *statedb.StateDB + vmdb evmvm.CStateDB expectedGasUsed uint64 ) diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go index cdee478b5a..90eba9a120 100644 --- a/x/evm/keeper/state_transition.go +++ b/x/evm/keeper/state_transition.go @@ -1,22 +1,23 @@ package keeper import ( - "math" + "fmt" "math/big" + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" + cmttypes "github.com/cometbft/cometbft/types" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" evertypes "github.com/EscanBE/evermint/v12/types" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/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" + corevm "github.com/ethereum/go-ethereum/core/vm" ) // NewEVM generates a go-ethereum VM from the provided Message fields and the chain parameters @@ -27,15 +28,15 @@ import ( // NOTE: the RANDOM opcode is currently not supported since it requires // RANDAO implementation. See https://github.com/evmos/ethermint/pull/1520#pullrequestreview-1200504697 // for more information. - +// TODO ES: support RANDOM opcode func (k *Keeper) NewEVM( ctx sdk.Context, msg core.Message, - cfg *statedb.EVMConfig, - tracer vm.EVMLogger, - stateDB vm.StateDB, -) *vm.EVM { - blockCtx := vm.BlockContext{ + cfg *evmvm.EVMConfig, + tracer corevm.EVMLogger, + stateDB corevm.StateDB, +) *corevm.EVM { + blockCtx := corevm.BlockContext{ CanTransfer: core.CanTransfer, Transfer: core.Transfer, GetHash: k.GetHashFn(ctx), @@ -50,17 +51,33 @@ func (k *Keeper) NewEVM( txCtx := core.NewEVMTxContext(msg) if tracer == nil { - tracer = k.Tracer(ctx, msg, cfg.ChainConfig) + tracer = evmtypes.NewTracer(k.tracer, msg, cfg.ChainConfig, ctx.BlockHeight()) + } + + coreVmConfig := corevm.Config{ + Debug: func() bool { + if tracer != nil { + if _, ok := tracer.(evmtypes.NoOpTracer); !ok { + return true + } + } + return false + }(), + Tracer: tracer, + NoBaseFee: cfg.NoBaseFee || k.IsNoBaseFeeEnabled(ctx), + ExtraEips: cfg.Params.EIPs(), } - vmConfig := k.VMConfig(ctx, cfg, tracer) - return vm.NewEVM(blockCtx, txCtx, stateDB, cfg.ChainConfig, vmConfig) + + return corevm.NewEVM(blockCtx, txCtx, stateDB, cfg.ChainConfig, coreVmConfig) } -// GetHashFn implements vm.GetHashFunc for Ethermint. It handles 3 cases: +// GetHashFn implements vm.GetHashFunc for Evermint. It handles 3 cases: // 1. The requested height matches the current height from context (and thus same epoch number) -// 2. The requested height is from an previous height from the same chain epoch +// 2. The requested height is from a previous height from the same chain epoch // 3. The requested height is from a height greater than the latest one -func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { +// +// TODO ES: improve this +func (k Keeper) GetHashFn(ctx sdk.Context) corevm.GetHashFunc { return func(height uint64) common.Hash { h, err := evertypes.SafeInt64(height) if err != nil { @@ -70,8 +87,8 @@ func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { switch { case ctx.BlockHeight() == h: - // Case 1: The requested height matches the one from the context so we can retrieve the header - // hash directly from the context. + // Case 1: The requested height matches the one from the context, + // so we can retrieve the header hash directly from the context. // Note: The headerHash is only set at begin block, it will be nil in case of a query context headerHash := ctx.HeaderHash() if len(headerHash) != 0 { @@ -134,7 +151,7 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*e if err != nil { return nil, errorsmod.Wrap(err, "failed to load evm config") } - txConfig := k.TxConfig(ctx, tx.Hash()) + txConfig := k.NewTxConfig(ctx, tx) // get the signer according to the chain rules from the config and block height signer := ethtypes.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) @@ -162,14 +179,13 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*e } // ApplyMessage calls ApplyMessageWithConfig with an empty TxConfig. -func (k *Keeper) ApplyMessage(ctx sdk.Context, msg core.Message, tracer vm.EVMLogger, commit bool) (*evmtypes.MsgEthereumTxResponse, error) { +func (k *Keeper) ApplyMessage(ctx sdk.Context, msg core.Message, tracer corevm.EVMLogger, commit bool) (*evmtypes.MsgEthereumTxResponse, error) { cfg, err := k.EVMConfig(ctx, sdk.ConsAddress(ctx.BlockHeader().ProposerAddress), k.eip155ChainID) if err != nil { return nil, errorsmod.Wrap(err, "failed to load evm config") } - txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash())) - txConfig = txConfig.WithTxTypeFromMessage(msg) + txConfig := k.NewTxConfigFromMessage(ctx, msg) return k.ApplyMessageWithConfig(ctx, msg, tracer, commit, cfg, txConfig) } @@ -179,7 +195,7 @@ func (k *Keeper) ApplyMessage(ctx sdk.Context, msg core.Message, tracer vm.EVMLo // // # Reverted state // -// The snapshot and rollback are supported by the `statedb.StateDB`. +// The snapshot and rollback are supported by the `legacy_statedb.StateDB`. // // # Different Callers // @@ -213,10 +229,10 @@ func (k *Keeper) ApplyMessage(ctx sdk.Context, msg core.Message, tracer vm.EVMLo // If commit is true, the `StateDB` will be committed, otherwise discarded. func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, msg core.Message, - tracer vm.EVMLogger, + tracer corevm.EVMLogger, commit bool, - cfg *statedb.EVMConfig, - txConfig statedb.TxConfig, + cfg *evmvm.EVMConfig, + txConfig evmvm.TxConfig, ) (*evmtypes.MsgEthereumTxResponse, error) { // return error if contract creation or call are disabled through governance if !cfg.Params.EnableCreate && msg.To() == nil { @@ -225,7 +241,8 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, return nil, errorsmod.Wrap(evmtypes.ErrCallDisabled, "failed to call contract") } - stateDB := statedb.New(ctx, k, txConfig) + stateDB := evmvm.NewStateDB(ctx, k, k.accountKeeper, k.bankKeeper) + // stateDB := statedb.New(ctx, k, txConfig) evm := k.NewEVM(ctx, msg, cfg, tracer, stateDB) var gasPool core.GasPool @@ -249,10 +266,18 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, } var txType uint8 - if txConfig.TxType < 0 || txConfig.TxType > math.MaxUint8 { + if txConfig.TxType == nil { panic("require tx type set") - } else { - txType = uint8(txConfig.TxType) + } + switch *txConfig.TxType { + case ethtypes.DynamicFeeTxType: + txType = ethtypes.DynamicFeeTxType + case ethtypes.AccessListTxType: + txType = ethtypes.AccessListTxType + case ethtypes.LegacyTxType: + txType = ethtypes.LegacyTxType + default: + panic(fmt.Sprintf("invalid tx type: %d", *txConfig.TxType)) } receipt := ethtypes.Receipt{ @@ -262,7 +287,7 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, Status: 0, // to be filled below CumulativeGasUsed: cumulativeGasUsed, Bloom: ethtypes.Bloom{}, // compute bellow - Logs: stateDB.Logs(), + Logs: stateDB.GetTransactionLogs(), } if execResult.Err == nil { receipt.Status = ethtypes.ReceiptStatusSuccessful @@ -273,7 +298,8 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, // The dirty states in `StateDB` is either committed or discarded after return if commit { - if err := stateDB.Commit(); err != nil { + // TODO ES: enable delete empty object? + if err := stateDB.CommitMultiStore(false); err != nil { return nil, errorsmod.Wrap(err, "failed to commit stateDB") } } diff --git a/x/evm/keeper/state_transition_benchmark_test.go b/x/evm/keeper/state_transition_benchmark_test.go index 432995d54d..2ef89019dc 100644 --- a/x/evm/keeper/state_transition_benchmark_test.go +++ b/x/evm/keeper/state_transition_benchmark_test.go @@ -11,9 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" - ethparams "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" ) @@ -131,30 +129,21 @@ func newEthMsgTx( return msg, baseFee, msg.Sign(ethSigner, krSigner) } -func newNativeMessage( +func newNativeTransaction( nonce uint64, - blockHeight int64, address common.Address, - cfg *ethparams.ChainConfig, krSigner keyring.Signer, ethSigner ethtypes.Signer, txType byte, data []byte, accessList ethtypes.AccessList, -) (core.Message, error) { - msgSigner := ethtypes.MakeSigner(cfg, big.NewInt(blockHeight)) - +) (*ethtypes.Transaction, *big.Int, error) { msg, baseFee, err := newEthMsgTx(nonce, address, krSigner, ethSigner, txType, data, accessList) if err != nil { - return nil, err - } - - m, err := msg.AsMessage(msgSigner, baseFee) - if err != nil { - return nil, err + return nil, nil, err } - return m, nil + return msg.AsTransaction(), baseFee, nil } func BenchmarkApplyTransaction(b *testing.B) { @@ -243,8 +232,6 @@ func BenchmarkApplyMessage(b *testing.B) { suite := KeeperTestSuite{} suite.SetupTestWithT(b) - params := suite.app.EvmKeeper.GetParams(suite.ctx) - ethCfg := params.ChainConfig.EthereumConfig(suite.app.EvmKeeper.ChainID()) signer := ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID()) b.ResetTimer() @@ -252,11 +239,9 @@ func BenchmarkApplyMessage(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - m, err := newNativeMessage( + ethTx, baseFee, err := newNativeTransaction( suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address), - suite.ctx.BlockHeight(), suite.address, - ethCfg, suite.signer, signer, ethtypes.AccessListTxType, @@ -265,6 +250,9 @@ func BenchmarkApplyMessage(b *testing.B) { ) require.NoError(b, err) + m, err := ethTx.AsMessage(signer, baseFee) + require.NoError(b, err) + b.StartTimer() resp, err := suite.app.EvmKeeper.ApplyMessage(suite.ctx, m, nil, true) b.StopTimer() @@ -279,8 +267,6 @@ func BenchmarkApplyMessageWithLegacyTx(b *testing.B) { suite := KeeperTestSuite{} suite.SetupTestWithT(b) - params := suite.app.EvmKeeper.GetParams(suite.ctx) - ethCfg := params.ChainConfig.EthereumConfig(suite.app.EvmKeeper.ChainID()) signer := ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID()) b.ResetTimer() @@ -288,11 +274,9 @@ func BenchmarkApplyMessageWithLegacyTx(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - m, err := newNativeMessage( + ethTx, baseFee, err := newNativeTransaction( suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address), - suite.ctx.BlockHeight(), suite.address, - ethCfg, suite.signer, signer, ethtypes.LegacyTxType, @@ -301,6 +285,9 @@ func BenchmarkApplyMessageWithLegacyTx(b *testing.B) { ) require.NoError(b, err) + m, err := ethTx.AsMessage(signer, baseFee) + require.NoError(b, err) + b.StartTimer() resp, err := suite.app.EvmKeeper.ApplyMessage(suite.ctx, m, nil, true) b.StopTimer() @@ -314,8 +301,6 @@ func BenchmarkApplyMessageWithDynamicFeeTx(b *testing.B) { suite := KeeperTestSuite{enableFeemarket: true} suite.SetupTestWithT(b) - params := suite.app.EvmKeeper.GetParams(suite.ctx) - ethCfg := params.ChainConfig.EthereumConfig(suite.app.EvmKeeper.ChainID()) signer := ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID()) b.ResetTimer() @@ -323,11 +308,9 @@ func BenchmarkApplyMessageWithDynamicFeeTx(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - m, err := newNativeMessage( + ethTx, baseFee, err := newNativeTransaction( suite.app.EvmKeeper.GetNonce(suite.ctx, suite.address), - suite.ctx.BlockHeight(), suite.address, - ethCfg, suite.signer, signer, ethtypes.DynamicFeeTxType, @@ -336,6 +319,9 @@ func BenchmarkApplyMessageWithDynamicFeeTx(b *testing.B) { ) require.NoError(b, err) + m, err := ethTx.AsMessage(signer, baseFee) + require.NoError(b, err) + b.StartTimer() resp, err := suite.app.EvmKeeper.ApplyMessage(suite.ctx, m, nil, true) b.StopTimer() diff --git a/x/evm/keeper/state_transition_core.go b/x/evm/keeper/state_transition_core.go index ed1a1cdfac..75c1fdfeb3 100644 --- a/x/evm/keeper/state_transition_core.go +++ b/x/evm/keeper/state_transition_core.go @@ -25,7 +25,7 @@ import ( evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" ) @@ -58,8 +58,8 @@ type StateTransition struct { initialGas uint64 value *big.Int data []byte - state vm.StateDB - evm *vm.EVM + state corevm.StateDB + evm *corevm.EVM // extra fields for Evermint @@ -71,7 +71,7 @@ type StateTransition struct { } // NewStateTransition initialises and returns a new state transition object. -func NewStateTransition(evm *vm.EVM, msg core.Message, gp *core.GasPool) *StateTransition { +func NewStateTransition(evm *corevm.EVM, msg core.Message, gp *core.GasPool) *StateTransition { return &StateTransition{ gp: gp, evm: evm, @@ -92,7 +92,7 @@ func NewStateTransition(evm *vm.EVM, msg core.Message, gp *core.GasPool) *StateT // the gas used (which includes gas refunds) and an error if it failed. An error always // indicates a core error meaning that the message would always fail for that particular // state and would never be accepted within a block. -func ApplyMessage(evm *vm.EVM, msg core.Message, gp *core.GasPool, beforeRun func(transition *StateTransition)) (*core.ExecutionResult, error) { +func ApplyMessage(evm *corevm.EVM, msg core.Message, gp *core.GasPool, beforeRun func(transition *StateTransition)) (*core.ExecutionResult, error) { st := NewStateTransition(evm, msg, gp) if beforeRun != nil { beforeRun(st) @@ -224,7 +224,7 @@ func (st *StateTransition) TransitionDb() (*core.ExecutionResult, error) { var ( msg = st.msg - sender = vm.AccountRef(msg.From()) + sender = corevm.AccountRef(msg.From()) rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil) contractCreation = msg.To() == nil ) @@ -246,7 +246,7 @@ func (st *StateTransition) TransitionDb() (*core.ExecutionResult, error) { // Set up the initial access list. if rules.IsBerlin { - st.state.PrepareAccessList(msg.From(), msg.To(), vm.ActivePrecompiles(rules), msg.AccessList()) + st.state.PrepareAccessList(msg.From(), msg.To(), corevm.ActivePrecompiles(rules), msg.AccessList()) } var ( ret []byte diff --git a/x/evm/keeper/state_transition_test.go b/x/evm/keeper/state_transition_test.go index 1135872f78..f0d169aca7 100644 --- a/x/evm/keeper/state_transition_test.go +++ b/x/evm/keeper/state_transition_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "fmt" + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" "math" "math/big" @@ -12,7 +13,6 @@ import ( utiltx "github.com/EscanBE/evermint/v12/testutil/tx" evertypes "github.com/EscanBE/evermint/v12/types" evmkeeper "github.com/EscanBE/evermint/v12/x/evm/keeper" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" "github.com/cometbft/cometbft/crypto/tmhash" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" @@ -443,8 +443,9 @@ func (suite *KeeperTestSuite) TestApplyTransaction() { suite.ctx = suite.ctx.WithGasMeter(evertypes.NewInfiniteGasMeterWithLimit(ethTx.Gas())) if tc.simulateCommitDbError { - suite.StateDB().ToggleStateDBPreventCommit(true) - defer suite.StateDB().ToggleStateDBPreventCommit(false) + stateDb := suite.StateDB() + stateDb.ForTest_ToggleStateDBPreventCommit(true) + defer stateDb.ForTest_ToggleStateDBPreventCommit(false) } res, err := suite.app.EvmKeeper.ApplyTransaction(suite.ctx, ethTx) @@ -481,7 +482,8 @@ func (suite *KeeperTestSuite) TestApplyTransaction() { func (suite *KeeperTestSuite) TestApplyMessage() { var ( - msg core.Message + ethTx *ethtypes.Transaction + baseFee *big.Int err error keeperParams evmtypes.Params signer ethtypes.Signer @@ -500,18 +502,15 @@ func (suite *KeeperTestSuite) TestApplyMessage() { { name: "message applied ok", malleate: func() { - msg, err = newNativeMessage( + ethTx, baseFee, err = newNativeTransaction( getNonce(suite.address.Bytes()), - suite.ctx.BlockHeight(), suite.address, - chainCfg, suite.signer, signer, ethtypes.AccessListTxType, nil, nil, ) - suite.Require().NoError(err) }, expErr: false, expGasUsed: ethparams.TxGas, @@ -543,9 +542,7 @@ func (suite *KeeperTestSuite) TestApplyMessage() { err = ethMsg.Sign(msgSigner, suite.signer) suite.Require().NoError(err) - var err error - msg, err = ethMsg.AsMessage(msgSigner, nil) - suite.Require().NoError(err) + ethTx = ethMsg.AsTransaction() }, expErr: false, expGasUsed: 21000, @@ -577,8 +574,7 @@ func (suite *KeeperTestSuite) TestApplyMessage() { err := ethMsg.Sign(msgSigner, suite.signer) suite.Require().NoError(err) - msg, err = ethMsg.AsMessage(msgSigner, nil) - suite.Require().NoError(err) + ethTx = ethMsg.AsTransaction() }, expErr: false, expGasUsed: 21_000, // consume just enough gas @@ -611,8 +607,7 @@ func (suite *KeeperTestSuite) TestApplyMessage() { err = ethMsg.Sign(msgSigner, suite.signer) suite.Require().NoError(err) - msg, err = ethMsg.AsMessage(msgSigner, nil) - suite.Require().NoError(err) + ethTx = ethMsg.AsTransaction() }, expErr: true, expErrContains: core.ErrIntrinsicGas.Error(), @@ -645,8 +640,7 @@ func (suite *KeeperTestSuite) TestApplyMessage() { err = ethMsg.Sign(msgSigner, suite.signer) suite.Require().NoError(err) - msg, err = ethMsg.AsMessage(msgSigner, nil) - suite.Require().NoError(err) + ethTx = ethMsg.AsTransaction() }, expErr: true, expErrContains: "failed to commit stateDB", @@ -660,15 +654,19 @@ func (suite *KeeperTestSuite) TestApplyMessage() { keeperParams = suite.app.EvmKeeper.GetParams(suite.ctx) chainCfg = keeperParams.ChainConfig.EthereumConfig(suite.app.EvmKeeper.ChainID()) signer = ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID()) + baseFee = nil tc.malleate() - suite.ctx = suite.ctx.WithGasMeter(evertypes.NewInfiniteGasMeterWithLimit(msg.Gas())) + suite.ctx = suite.ctx.WithGasMeter(evertypes.NewInfiniteGasMeterWithLimit(ethTx.Gas())) if tc.simulateCommitDbError { - suite.StateDB().ToggleStateDBPreventCommit(true) - defer suite.StateDB().ToggleStateDBPreventCommit(false) + stateDb := suite.StateDB() + stateDb.ForTest_ToggleStateDBPreventCommit(true) + defer stateDb.ForTest_ToggleStateDBPreventCommit(false) } + msg, err := ethTx.AsMessage(signer, baseFee) + suite.Require().NoError(err) res, err := suite.app.EvmKeeper.ApplyMessage(suite.ctx, msg, nil, true) if tc.expErr { @@ -697,13 +695,15 @@ func (suite *KeeperTestSuite) TestApplyMessage() { func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { var ( - msg core.Message + ethTx *ethtypes.Transaction + //msg core.Message err error - config *statedb.EVMConfig + config *evmvm.EVMConfig keeperParams evmtypes.Params signer ethtypes.Signer - txConfig statedb.TxConfig + txConfig evmvm.TxConfig chainCfg *ethparams.ChainConfig + baseFee *big.Int ) ptr := func(i int64) *int64 { return &i } @@ -720,11 +720,9 @@ func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { { name: "pass - message applied ok", malleate: func() { - msg, err = newNativeMessage( + ethTx, baseFee, err = newNativeTransaction( getNonce(suite.address.Bytes()), - suite.ctx.BlockHeight(), suite.address, - chainCfg, suite.signer, signer, ethtypes.AccessListTxType, @@ -740,11 +738,9 @@ func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { name: "fail - call contract tx with config param EnableCall = false", malleate: func() { config.Params.EnableCall = false - msg, err = newNativeMessage( + ethTx, baseFee, err = newNativeTransaction( getNonce(suite.address.Bytes()), - suite.ctx.BlockHeight(), suite.address, - chainCfg, suite.signer, signer, ethtypes.AccessListTxType, @@ -759,7 +755,7 @@ func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { { name: "fail - create contract tx with config param EnableCreate = false", malleate: func() { - msg, err = suite.createContractGethMsg(getNonce(suite.address.Bytes()), signer, chainCfg, big.NewInt(1)) + ethTx, err = suite.createContractGethMsg(getNonce(suite.address.Bytes()), signer, big.NewInt(1)) suite.Require().NoError(err) config.Params.EnableCreate = false }, @@ -793,8 +789,7 @@ func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { err = ethMsg.Sign(msgSigner, suite.signer) suite.Require().NoError(err) - msg, err = ethMsg.AsMessage(msgSigner, nil) - suite.Require().NoError(err) + ethTx = ethMsg.AsTransaction() }, expErr: false, expGasUsed: 21000, @@ -827,8 +822,7 @@ func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { err = ethMsg.Sign(msgSigner, suite.signer) suite.Require().NoError(err) - msg, err = ethMsg.AsMessage(msgSigner, nil) - suite.Require().NoError(err) + ethTx = ethMsg.AsTransaction() }, expErr: false, expGasUsed: 21_000, // consume just enough gas @@ -859,8 +853,7 @@ func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { err = ethMsg.Sign(msgSigner, suite.signer) suite.Require().NoError(err) - msg, err = ethMsg.AsMessage(msgSigner, nil) - suite.Require().NoError(err) + ethTx = ethMsg.AsTransaction() }, expErr: false, expGasUsed: 21000, @@ -893,8 +886,7 @@ func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { err = ethMsg.Sign(msgSigner, suite.signer) suite.Require().NoError(err) - msg, err = ethMsg.AsMessage(msgSigner, nil) - suite.Require().NoError(err) + ethTx = ethMsg.AsTransaction() }, expErr: true, expErrContains: core.ErrIntrinsicGas.Error(), @@ -928,8 +920,7 @@ func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { err = ethMsg.Sign(msgSigner, suite.signer) suite.Require().NoError(err) - msg, err = ethMsg.AsMessage(msgSigner, nil) - suite.Require().NoError(err) + ethTx = ethMsg.AsTransaction() }, expErr: true, expErrContains: "failed to commit stateDB", @@ -948,16 +939,20 @@ func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { keeperParams = suite.app.EvmKeeper.GetParams(suite.ctx) chainCfg = keeperParams.ChainConfig.EthereumConfig(suite.app.EvmKeeper.ChainID()) signer = ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID()) + baseFee = nil tc.malleate() - txConfig = suite.app.EvmKeeper.TxConfig(suite.ctx, common.Hash{}).WithTxTypeFromMessage(msg) - suite.ctx = suite.ctx.WithGasMeter(evertypes.NewInfiniteGasMeterWithLimit(msg.Gas())) + txConfig = suite.app.EvmKeeper.NewTxConfig(suite.ctx, ethTx) + suite.ctx = suite.ctx.WithGasMeter(evertypes.NewInfiniteGasMeterWithLimit(ethTx.Gas())) if tc.simulateCommitDbError { - suite.StateDB().ToggleStateDBPreventCommit(true) - defer suite.StateDB().ToggleStateDBPreventCommit(false) + stateDb := suite.StateDB() + stateDb.ForTest_ToggleStateDBPreventCommit(true) + defer stateDb.ForTest_ToggleStateDBPreventCommit(false) } + msg, err := ethTx.AsMessage(signer, baseFee) + suite.Require().NoError(err) res, err := suite.app.EvmKeeper.ApplyMessageWithConfig(suite.ctx, msg, nil, true, config, txConfig) defer func() { @@ -990,14 +985,13 @@ func (suite *KeeperTestSuite) TestApplyMessageWithConfig() { } } -func (suite *KeeperTestSuite) createContractGethMsg(nonce uint64, signer ethtypes.Signer, cfg *ethparams.ChainConfig, gasPrice *big.Int) (core.Message, error) { +func (suite *KeeperTestSuite) createContractGethMsg(nonce uint64, signer ethtypes.Signer, gasPrice *big.Int) (*ethtypes.Transaction, error) { ethMsg, err := suite.createContractMsgTx(nonce, signer, gasPrice) if err != nil { return nil, err } - msgSigner := ethtypes.MakeSigner(cfg, big.NewInt(suite.ctx.BlockHeight())) - return ethMsg.AsMessage(msgSigner, nil) + return ethMsg.AsTransaction(), nil } func (suite *KeeperTestSuite) createContractMsgTx(nonce uint64, signer ethtypes.Signer, gasPrice *big.Int) (*evmtypes.MsgEthereumTx, error) { diff --git a/x/evm/keeper/statedb.go b/x/evm/keeper/statedb.go index 07153061ff..cb04e6f376 100644 --- a/x/evm/keeper/statedb.go +++ b/x/evm/keeper/statedb.go @@ -1,43 +1,16 @@ package keeper import ( - "fmt" - "math/big" - storetypes "cosmossdk.io/store/types" + "fmt" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - - sdkmath "cosmossdk.io/math" - - errorsmod "cosmossdk.io/errors" "cosmossdk.io/store/prefix" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" ) -var _ statedb.Keeper = &Keeper{} - -// ---------------------------------------------------------------------------- -// StateDB Keeper implementation -// ---------------------------------------------------------------------------- - -// GetAccount returns nil if account is not exist -func (k *Keeper) GetAccount(ctx sdk.Context, addr common.Address) *statedb.Account { - acct := k.GetAccountWithoutBalance(ctx, addr) - if acct == nil { - return nil - } - - acct.Balance = k.GetBalance(ctx, addr) - return acct -} - -// GetState loads contract state from database, implements `statedb.Keeper` interface. +// GetState loads contract state func (k *Keeper) GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash { store := prefix.NewStore(ctx.KVStore(k.storeKey), evmtypes.AddressStoragePrefix(addr)) @@ -49,10 +22,21 @@ func (k *Keeper) GetState(ctx sdk.Context, addr common.Address, key common.Hash) return common.BytesToHash(value) } -// 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), evmtypes.KeyPrefixCode) - return store.Get(codeHash.Bytes()) +// 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), evmtypes.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(), + ) } // ForEachStorage iterate contract storage, callback return false to break early @@ -76,89 +60,10 @@ func (k *Keeper) ForEachStorage(ctx sdk.Context, addr common.Address, cb func(ke } } -// 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, sdkmath.NewIntFromBigInt(delta))) - if err := k.bankKeeper.MintCoins(ctx, evmtypes.ModuleName, coins); err != nil { - return err - } - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, evmtypes.ModuleName, cosmosAddr, coins); err != nil { - return err - } - case -1: - // burn - coins := sdk.NewCoins(sdk.NewCoin(params.EvmDenom, sdkmath.NewIntFromBigInt(new(big.Int).Neg(delta)))) - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, cosmosAddr, evmtypes.ModuleName, coins); err != nil { - return err - } - if err := k.bankKeeper.BurnCoins(ctx, evmtypes.ModuleName, coins); err != nil { - return err - } - default: - // not changed - } - return 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) - } - - if err := acct.SetSequence(account.Nonce); err != nil { - return err - } - - codeHash := common.BytesToHash(account.CodeHash) - - if _, isBaseAccount := acct.(*authtypes.BaseAccount); isBaseAccount { - k.SetCodeHash(ctx, addr, codeHash) - } - - k.accountKeeper.SetAccount(ctx, acct) - - if err := k.SetBalance(ctx, addr, account.Balance); err != nil { - return err - } - - k.Logger(ctx).Debug( - "account updated", - "ethereum-address", addr.Hex(), - "nonce", account.Nonce, - "codeHash", codeHash.Hex(), - "balance", account.Balance, - ) - return 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), evmtypes.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(), - ) +// GetCode loads contract code +func (k *Keeper) GetCode(ctx sdk.Context, codeHash common.Hash) []byte { + store := prefix.NewStore(ctx.KVStore(k.storeKey), evmtypes.KeyPrefixCode) + return store.Get(codeHash.Bytes()) } // SetCode set contract code, delete if code is empty. @@ -179,53 +84,6 @@ func (k *Keeper) SetCode(ctx sdk.Context, codeHash, code []byte) { ) } -// 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 - } - - // NOTE: only Ethereum accounts (contracts) can be selfdestructed - if isNotProhibitedAccount, reason := isNotProhibitedAccountType(acct); !isNotProhibitedAccount { - return errorsmod.Wrapf(evmtypes.ErrInvalidAccount, "type %T, address %s, reason: %s", acct, addr, reason) - } - - // clear code-hash - codeHash := k.GetCodeHash(ctx, addr.Bytes()) - if evmtypes.IsEmptyCodeHash(codeHash) { - return errorsmod.Wrapf(evmtypes.ErrInvalidAccount, "type %T, address %s, not smart contract", acct, addr) - } - k.DeleteCodeHash(ctx, addr.Bytes()) - - // clear balance - if err := k.SetBalance(ctx, addr, new(big.Int)); err != nil { - return err - } - - // clear storage - k.ForEachStorage(ctx, addr, func(key, _ 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 -} - // GetCodeHash returns the code hash for the corresponding account address. func (k *Keeper) GetCodeHash(ctx sdk.Context, addr []byte) common.Hash { store := prefix.NewStore(ctx.KVStore(k.storeKey), evmtypes.KeyPrefixCodeHash) @@ -233,7 +91,9 @@ func (k *Keeper) GetCodeHash(ctx sdk.Context, addr []byte) common.Hash { var codeHash common.Hash if len(bz) == 0 { - codeHash = common.BytesToHash(evmtypes.EmptyCodeHash) + if k.accountKeeper.HasAccount(ctx, addr) { + codeHash = common.BytesToHash(evmtypes.EmptyCodeHash) + } } else { codeHash = common.BytesToHash(bz) } @@ -275,24 +135,3 @@ func (k Keeper) IterateContracts(ctx sdk.Context, callback func(addr common.Addr } } } - -// isNotProhibitedAccountType returns false if the given account is module account or vesting account -func isNotProhibitedAccountType(accI sdk.AccountI) (notProhibited bool, explain string) { - if moduleAccount, isModuleAccount := accI.(sdk.ModuleAccountI); isModuleAccount { - explain = fmt.Sprintf("%s is module account of %s", moduleAccount.GetAddress().String(), moduleAccount.GetName()) - return - } - - if _, isVestingAccount := accI.(*vestingtypes.BaseVestingAccount); isVestingAccount { - explain = fmt.Sprintf("%s is vesting account", accI.GetAddress().String()) - return - } - - if _, isVestingAccount := accI.(banktypes.VestingAccount); isVestingAccount { - explain = fmt.Sprintf("%s is vesting account", accI.GetAddress().String()) - return - } - - notProhibited = true - return -} diff --git a/x/evm/keeper/statedb_test.go b/x/evm/keeper/statedb_test.go index b843a2d2c5..93f0276d06 100644 --- a/x/evm/keeper/statedb_test.go +++ b/x/evm/keeper/statedb_test.go @@ -2,14 +2,12 @@ package keeper_test import ( "fmt" + "github.com/EscanBE/evermint/v12/x/evm/vm" "math/big" - "github.com/EscanBE/evermint/v12/constants" - "cosmossdk.io/store/prefix" "github.com/EscanBE/evermint/v12/crypto/ethsecp256k1" utiltx "github.com/EscanBE/evermint/v12/testutil/tx" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" @@ -17,7 +15,7 @@ import ( authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" ) @@ -25,27 +23,27 @@ func (suite *KeeperTestSuite) TestCreateAccount() { testCases := []struct { name string addr common.Address - malleate func(vm.StateDB, common.Address) - callback func(vm.StateDB, common.Address) + malleate func(corevm.StateDB, common.Address) + callback func(corevm.StateDB, common.Address) }{ { name: "reset account (keep balance)", addr: suite.address, - malleate: func(vmdb vm.StateDB, addr common.Address) { + malleate: func(vmdb corevm.StateDB, addr common.Address) { vmdb.AddBalance(addr, big.NewInt(100)) suite.Require().NotZero(vmdb.GetBalance(addr).Int64()) }, - callback: func(vmdb vm.StateDB, addr common.Address) { + callback: func(vmdb corevm.StateDB, addr common.Address) { suite.Require().Equal(vmdb.GetBalance(addr).Int64(), int64(100)) }, }, { name: "create account", addr: utiltx.GenerateAddress(), - malleate: func(vmdb vm.StateDB, addr common.Address) { + malleate: func(vmdb corevm.StateDB, addr common.Address) { suite.Require().False(vmdb.Exist(addr)) }, - callback: func(vmdb vm.StateDB, addr common.Address) { + callback: func(vmdb corevm.StateDB, addr common.Address) { suite.Require().True(vmdb.Exist(addr)) }, }, @@ -63,9 +61,10 @@ func (suite *KeeperTestSuite) TestCreateAccount() { func (suite *KeeperTestSuite) TestAddBalance() { testCases := []struct { - name string - amount *big.Int - isNoOp bool + name string + amount *big.Int + isNoOp bool + wantPanic bool }{ { name: "positive amount", @@ -78,9 +77,9 @@ func (suite *KeeperTestSuite) TestAddBalance() { isNoOp: true, }, { - name: "negative amount", - amount: big.NewInt(-1), // seems to be consistent with go-ethereum's implementation - isNoOp: false, + name: "negative amount", + amount: big.NewInt(-1), + wantPanic: true, }, } @@ -88,6 +87,14 @@ func (suite *KeeperTestSuite) TestAddBalance() { suite.Run(tc.name, func() { vmdb := suite.StateDB() prev := vmdb.GetBalance(suite.address) + + if tc.wantPanic { + suite.Require().Panics(func() { + vmdb.AddBalance(suite.address, tc.amount) + }) + return + } + vmdb.AddBalance(suite.address, tc.amount) post := vmdb.GetBalance(suite.address) @@ -102,21 +109,22 @@ func (suite *KeeperTestSuite) TestAddBalance() { func (suite *KeeperTestSuite) TestSubBalance() { testCases := []struct { - name string - amount *big.Int - malleate func(vm.StateDB) - isNoOp bool + name string + amount *big.Int + malleate func(corevm.StateDB) + isNoOp bool + wantPanic bool }{ { - name: "positive amount, below zero", - amount: big.NewInt(100), - malleate: func(vm.StateDB) {}, - isNoOp: false, + name: "positive amount, below zero", + amount: big.NewInt(100), + malleate: func(corevm.StateDB) {}, + wantPanic: true, }, { name: "positive amount, above zero", amount: big.NewInt(50), - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { vmdb.AddBalance(suite.address, big.NewInt(100)) }, isNoOp: false, @@ -124,14 +132,14 @@ func (suite *KeeperTestSuite) TestSubBalance() { { name: "zero amount", amount: big.NewInt(0), - malleate: func(vm.StateDB) {}, + malleate: func(corevm.StateDB) {}, isNoOp: true, }, { - name: "negative amount", - amount: big.NewInt(-1), - malleate: func(vm.StateDB) {}, - isNoOp: false, + name: "negative amount", + amount: big.NewInt(-1), + malleate: func(corevm.StateDB) {}, + wantPanic: true, }, } @@ -141,6 +149,14 @@ func (suite *KeeperTestSuite) TestSubBalance() { tc.malleate(vmdb) prev := vmdb.GetBalance(suite.address) + + if tc.wantPanic { + suite.Require().Panics(func() { + vmdb.SubBalance(suite.address, tc.amount) + }) + return + } + vmdb.SubBalance(suite.address, tc.amount) post := vmdb.GetBalance(suite.address) @@ -158,19 +174,19 @@ func (suite *KeeperTestSuite) TestGetNonce() { name string address common.Address expectedNonce uint64 - malleate func(vm.StateDB) + malleate func(corevm.StateDB) }{ { name: "account not found", address: utiltx.GenerateAddress(), expectedNonce: 0, - malleate: func(vm.StateDB) {}, + malleate: func(corevm.StateDB) {}, }, { name: "existing account", address: suite.address, expectedNonce: 1, - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { vmdb.SetNonce(suite.address, 1) }, }, @@ -227,25 +243,25 @@ func (suite *KeeperTestSuite) TestGetCodeHash() { name string address common.Address expHash common.Hash - malleate func(vm.StateDB) + malleate func(corevm.StateDB) }{ { name: "account not found", address: utiltx.GenerateAddress(), expHash: common.Hash{}, - malleate: func(vm.StateDB) {}, + malleate: func(corevm.StateDB) {}, }, { name: "account with EmptyCodeHash", address: addr, expHash: common.BytesToHash(evmtypes.EmptyCodeHash), - malleate: func(vm.StateDB) {}, + malleate: func(corevm.StateDB) {}, }, { name: "account with non-empty code hash", address: suite.address, expHash: crypto.Keccak256Hash([]byte("codeHash")), - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { vmdb.SetCode(suite.address, []byte("codeHash")) }, }, @@ -348,13 +364,13 @@ func (suite *KeeperTestSuite) TestKeeperSetCode() { func (suite *KeeperTestSuite) TestRefund() { testCases := []struct { name string - malleate func(vm.StateDB) + malleate func(corevm.StateDB) expRefund uint64 expPanic bool }{ { name: "pass - add and subtract refund", - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { vmdb.AddRefund(11) }, expRefund: 1, @@ -362,7 +378,7 @@ func (suite *KeeperTestSuite) TestRefund() { }, { name: "fail - subtract amount > current refund", - malleate: func(vm.StateDB) { + malleate: func(corevm.StateDB) { }, expRefund: 0, expPanic: true, @@ -418,7 +434,7 @@ func (suite *KeeperTestSuite) TestCommittedState() { vmdb := suite.StateDB() vmdb.SetState(suite.address, key, value1) - err := vmdb.Commit() + err := vmdb.CommitMultiStore(false) suite.Require().NoError(err) vmdb = suite.StateDB() @@ -427,7 +443,7 @@ func (suite *KeeperTestSuite) TestCommittedState() { suite.Require().Equal(value2, tmp) tmp = vmdb.GetCommittedState(suite.address, key) suite.Require().Equal(value1, tmp) - err = vmdb.Commit() + err = vmdb.CommitMultiStore(false) suite.Require().NoError(err) vmdb = suite.StateDB() @@ -446,7 +462,7 @@ func (suite *KeeperTestSuite) TestSuicide() { 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()) + suite.Require().NoError(db.CommitMultiStore(false)) db = suite.StateDB() // Generate 2nd address @@ -469,7 +485,7 @@ func (suite *KeeperTestSuite) TestSuicide() { suite.Require().Equal(true, db.HasSuicided(suite.address)) // Commit state - suite.Require().NoError(db.Commit()) + suite.Require().NoError(db.CommitMultiStore(false)) db = suite.StateDB() // Check code is deleted @@ -494,19 +510,19 @@ func (suite *KeeperTestSuite) TestExist() { testCases := []struct { name string address common.Address - malleate func(vm.StateDB) + malleate func(corevm.StateDB) exists bool }{ { name: "success, account exists", address: suite.address, - malleate: func(vm.StateDB) {}, + malleate: func(corevm.StateDB) {}, exists: true, }, { name: "success, has suicided", address: suite.address, - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { vmdb.Suicide(suite.address) }, exists: true, @@ -514,7 +530,7 @@ func (suite *KeeperTestSuite) TestExist() { { name: "success, account doesn't exist", address: utiltx.GenerateAddress(), - malleate: func(vm.StateDB) {}, + malleate: func(corevm.StateDB) {}, exists: false, }, } @@ -530,32 +546,77 @@ func (suite *KeeperTestSuite) TestExist() { } func (suite *KeeperTestSuite) TestEmpty() { + randomAddr1 := utiltx.GenerateAddress() + randomAddr2 := utiltx.GenerateAddress() + randomAddr3 := utiltx.GenerateAddress() testCases := []struct { name string address common.Address - malleate func(vm.StateDB) + malleate func(corevm.StateDB) empty bool }{ { name: "empty, account exists", address: suite.address, - malleate: func(vm.StateDB) {}, + malleate: func(corevm.StateDB) {}, empty: true, }, { name: "not empty, positive balance", address: suite.address, - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { vmdb.AddBalance(suite.address, big.NewInt(100)) }, empty: false, }, { name: "empty, account doesn't exist", - address: utiltx.GenerateAddress(), - malleate: func(vm.StateDB) {}, + address: randomAddr1, + malleate: func(corevm.StateDB) {}, empty: true, }, + { + name: "not empty, account = none, nonce = none, balance != 0, code = none", + address: randomAddr2, + malleate: func(vmdb corevm.StateDB) { + vmdb.AddBalance(randomAddr2, big.NewInt(1)) + + // delete account + ctx := vmdb.(vm.CStateDB).ForTest_GetCurrentContext() + acc := suite.app.AccountKeeper.GetAccount(ctx, randomAddr2.Bytes()) + suite.Require().NotNil(acc) // created by bank transfer + suite.app.AccountKeeper.RemoveAccount(ctx, acc) + suite.Require().Nil(suite.app.AccountKeeper.GetAccount(ctx, randomAddr2.Bytes())) + }, + empty: false, + }, + { + name: "not empty, account = none, nonce = none, balance = 0, code = exists", + address: randomAddr3, + malleate: func(vmdb corevm.StateDB) { + vmdb.SetCode(randomAddr3, []byte("code")) + + // delete account + ctx := vmdb.(vm.CStateDB).ForTest_GetCurrentContext() + acc := suite.app.AccountKeeper.GetAccount(ctx, randomAddr3.Bytes()) + suite.Require().NotNil(acc) // created by set code + suite.app.AccountKeeper.RemoveAccount(ctx, acc) + suite.Require().Nil(suite.app.AccountKeeper.GetAccount(ctx, randomAddr3.Bytes())) + }, + empty: false, + }, + { + name: "not empty, account = exists, nonce > 0, balance = 0, code = none", + address: suite.address, + malleate: func(vmdb corevm.StateDB) { + suite.Require().Equal(uint64(0), vmdb.GetNonce(suite.address)) + + vmdb.SetNonce(suite.address, 1) + + suite.Require().Equal(uint64(1), vmdb.GetNonce(suite.address)) + }, + empty: false, + }, } for _, tc := range testCases { @@ -576,13 +637,13 @@ func (suite *KeeperTestSuite) TestSnapshot() { testCases := []struct { name string - malleate func(vm.StateDB) + malleate func(corevm.StateDB) }{ { name: "simple revert", - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { revision := vmdb.Snapshot() - suite.Require().Zero(revision) + suite.Require().Equal(0, revision) vmdb.SetState(suite.address, key, value1) suite.Require().Equal(value1, vmdb.GetState(suite.address, key)) @@ -595,9 +656,9 @@ func (suite *KeeperTestSuite) TestSnapshot() { }, { name: "nested snapshot/revert", - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { revision1 := vmdb.Snapshot() - suite.Require().Zero(revision1) + suite.Require().Equal(0, revision1) vmdb.SetState(suite.address, key, value1) @@ -615,7 +676,7 @@ func (suite *KeeperTestSuite) TestSnapshot() { }, { name: "jump revert", - malleate: func(vmdb vm.StateDB) { + malleate: func(vmdb corevm.StateDB) { revision1 := vmdb.Snapshot() vmdb.SetState(suite.address, key, value1) vmdb.Snapshot() @@ -654,152 +715,6 @@ func (suite *KeeperTestSuite) CreateTestTx(msg *evmtypes.MsgEthereumTx, priv cry return txBuilder.GetTx() } -func (suite *KeeperTestSuite) TestAddLog() { - addr, privKey := utiltx.NewAddrKey() - ethTx1Params := &evmtypes.EvmTxArgs{ - From: addr, - ChainID: big.NewInt(constants.TestnetEIP155ChainId), - Nonce: 0, - To: &suite.address, - Amount: big.NewInt(1), - GasLimit: 100000, - GasPrice: big.NewInt(1), - Input: []byte("test"), - } - msg1 := evmtypes.NewTx(ethTx1Params) - tx1 := suite.CreateTestTx(msg1, privKey) - msg1, _ = tx1.GetMsgs()[0].(*evmtypes.MsgEthereumTx) - txHash1 := msg1.AsTransaction().Hash() - - ethTx2Params := &evmtypes.EvmTxArgs{ - From: addr, - ChainID: big.NewInt(constants.TestnetEIP155ChainId), - Nonce: 2, - To: &suite.address, - Amount: big.NewInt(1), - GasLimit: 100000, - GasPrice: big.NewInt(1), - Input: []byte("test"), - } - msg2 := evmtypes.NewTx(ethTx2Params) - tx2 := suite.CreateTestTx(msg2, privKey) - msg2, _ = tx2.GetMsgs()[0].(*evmtypes.MsgEthereumTx) - txHash2 := msg2.AsTransaction().Hash() - - ethTx3Params := &evmtypes.EvmTxArgs{ - From: addr, - ChainID: big.NewInt(constants.TestnetEIP155ChainId), - Nonce: 0, - To: &suite.address, - Amount: big.NewInt(1), - GasLimit: 100000, - GasFeeCap: big.NewInt(1), - GasTipCap: big.NewInt(1), - Input: []byte("test"), - } - msg3 := evmtypes.NewTx(ethTx3Params) - tx3 := suite.CreateTestTx(msg3, privKey) - msg3, _ = tx3.GetMsgs()[0].(*evmtypes.MsgEthereumTx) - txHash3 := msg3.AsTransaction().Hash() - - ethTx4Params := &evmtypes.EvmTxArgs{ - From: addr, - ChainID: big.NewInt(constants.TestnetEIP155ChainId), - Nonce: 1, - To: &suite.address, - Amount: big.NewInt(1), - GasLimit: 100000, - GasFeeCap: big.NewInt(1), - GasTipCap: big.NewInt(1), - Input: []byte("test"), - } - msg4 := evmtypes.NewTx(ethTx4Params) - tx4 := suite.CreateTestTx(msg4, privKey) - msg4, _ = tx4.GetMsgs()[0].(*evmtypes.MsgEthereumTx) - txHash4 := msg4.AsTransaction().Hash() - - testCases := []struct { - name string - hash common.Hash - log, expLog *ethtypes.Log // pre and post populating log fields - malleate func(vm.StateDB) - }{ - { - name: "legacy tx - tx hash from message", - hash: txHash1, - log: ðtypes.Log{ - Address: addr, - Topics: make([]common.Hash, 0), - }, - expLog: ðtypes.Log{ - Address: addr, - TxHash: txHash1, - Topics: make([]common.Hash, 0), - }, - malleate: func(vm.StateDB) {}, - }, - { - name: "legacy tx - tx hash from message", - hash: txHash2, - log: ðtypes.Log{ - Address: addr, - Topics: make([]common.Hash, 0), - }, - expLog: ðtypes.Log{ - Address: addr, - TxHash: txHash2, - Topics: make([]common.Hash, 0), - }, - malleate: func(vm.StateDB) {}, - }, - { - name: "Dynamic Fee - tx hash from message", - hash: txHash3, - log: ðtypes.Log{ - Address: addr, - Topics: make([]common.Hash, 0), - }, - expLog: ðtypes.Log{ - Address: addr, - TxHash: txHash3, - Topics: make([]common.Hash, 0), - }, - malleate: func(vm.StateDB) {}, - }, - { - name: "Dynamic Fee - tx hash from message", - hash: txHash4, - log: ðtypes.Log{ - Address: addr, - Topics: make([]common.Hash, 0), - }, - expLog: ðtypes.Log{ - Address: addr, - TxHash: txHash4, - Topics: make([]common.Hash, 0), - }, - malleate: func(vm.StateDB) {}, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupTest() - vmdb := statedb.New(suite.ctx, suite.app.EvmKeeper, statedb.NewTxConfig( - common.BytesToHash(suite.ctx.HeaderHash()), - tc.hash, - 0, 0, - )) - tc.malleate(vmdb) - - vmdb.AddLog(tc.log) - logs := vmdb.Logs() - suite.Require().Equal(1, len(logs)) - suite.Require().Equal(tc.expLog, logs[0]) - }) - } -} - func (suite *KeeperTestSuite) TestPrepareAccessList() { dest := utiltx.GenerateAddress() precompiles := []common.Address{utiltx.GenerateAddress(), utiltx.GenerateAddress()} @@ -960,101 +875,3 @@ func (suite *KeeperTestSuite) _TestForEachStorage() { } } */ - -func (suite *KeeperTestSuite) TestSetBalance() { - amount := big.NewInt(-10) - - testCases := []struct { - name string - addr common.Address - malleate func() - expErr bool - }{ - { - name: "fail - address without funds - invalid amount", - addr: suite.address, - malleate: func() {}, - expErr: true, - }, - { - name: "pass - mint to address", - addr: suite.address, - malleate: func() { - amount = big.NewInt(100) - }, - expErr: false, - }, - { - name: "pass - burn from address", - addr: suite.address, - malleate: func() { - amount = big.NewInt(60) - }, - expErr: false, - }, - { - name: "fail - address with funds - invalid amount", - addr: suite.address, - malleate: func() { - amount = big.NewInt(-10) - }, - expErr: true, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupTest() - tc.malleate() - err := suite.app.EvmKeeper.SetBalance(suite.ctx, tc.addr, amount) - if tc.expErr { - suite.Require().Error(err) - } else { - balance := suite.app.EvmKeeper.GetBalance(suite.ctx, tc.addr) - suite.Require().NoError(err) - suite.Require().Equal(amount, balance) - } - }) - } -} - -func (suite *KeeperTestSuite) TestDeleteAccount() { - supply := big.NewInt(100) - contractAddr := suite.DeployTestContract(suite.T(), suite.address, supply) - - testCases := []struct { - name string - addr common.Address - expErr bool - }{ - { - name: "fail - remove address is prohibited, contract only", - addr: suite.address, - expErr: true, - }, - { - name: "pass - remove non-existence address", - addr: common.HexToAddress("void"), - expErr: false, - }, - { - name: "pass - remove deployed contract", - addr: contractAddr, - expErr: false, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupTest() - err := suite.app.EvmKeeper.DeleteAccount(suite.ctx, tc.addr) - if tc.expErr { - suite.Require().Error(err) - } else { - suite.Require().NoError(err) - balance := suite.app.EvmKeeper.GetBalance(suite.ctx, tc.addr) - suite.Require().Equal(new(big.Int), balance) - } - }) - } -} diff --git a/x/evm/keeper/utils_test.go b/x/evm/keeper/utils_test.go index 43a6c679fd..bc04fa6607 100644 --- a/x/evm/keeper/utils_test.go +++ b/x/evm/keeper/utils_test.go @@ -6,10 +6,10 @@ import ( "time" evertypes "github.com/EscanBE/evermint/v12/types" + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" "github.com/EscanBE/evermint/v12/server/config" "github.com/EscanBE/evermint/v12/testutil" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" @@ -36,8 +36,8 @@ func (suite *KeeperTestSuite) Commit() { suite.queryClient = evmtypes.NewQueryClient(queryHelper) } -func (suite *KeeperTestSuite) StateDB() *statedb.StateDB { - return statedb.New(suite.ctx, suite.app.EvmKeeper, statedb.NewEmptyTxConfig(common.BytesToHash(suite.ctx.HeaderHash()))) +func (suite *KeeperTestSuite) StateDB() evmvm.CStateDB { + return evmvm.NewStateDB(suite.ctx, suite.app.EvmKeeper, suite.app.AccountKeeper, suite.app.BankKeeper) } // DeployTestContract deploy a test erc20 contract and returns the contract address diff --git a/x/evm/types/errors.go b/x/evm/types/errors.go index da8ee9403a..e900c58308 100644 --- a/x/evm/types/errors.go +++ b/x/evm/types/errors.go @@ -27,6 +27,7 @@ const ( codeErrGasOverflow codeErrInvalidAccount codeErrInvalidGasLimit + codeErrEngineFailure ) var ( @@ -74,6 +75,9 @@ var ( // ErrInvalidGasLimit returns an error if gas limit value is invalid ErrInvalidGasLimit = errorsmod.Register(ModuleName, codeErrInvalidGasLimit, "invalid gas limit") + + // ErrEngineFailure returns an error if the EVM execution engine fails + ErrEngineFailure = errorsmod.Register(ModuleName, codeErrEngineFailure, "EVM execution engine failure") ) // NewExecErrorWithReason unpacks the revert return bytes and returns a wrapped error diff --git a/x/evm/types/params.go b/x/evm/types/params.go index 1be38427c5..dac0d2b59e 100644 --- a/x/evm/types/params.go +++ b/x/evm/types/params.go @@ -7,7 +7,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" ) var ( @@ -100,8 +100,8 @@ func validateEIPs(i interface{}) error { } for _, eip := range eips { - if !vm.ValidEip(int(eip)) { - return fmt.Errorf("EIP %d is not activateable, valid EIPS are: %s", eip, vm.ActivateableEips()) + if !corevm.ValidEip(int(eip)) { + return fmt.Errorf("EIP %d is not activateable, valid EIPS are: %s", eip, corevm.ActivateableEips()) } } diff --git a/x/evm/types/tracer.go b/x/evm/types/tracer.go index 055757c7f8..19e79d4136 100644 --- a/x/evm/types/tracer.go +++ b/x/evm/types/tracer.go @@ -9,7 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" ethparams "github.com/ethereum/go-ethereum/params" ) @@ -22,7 +22,7 @@ const ( // NewTracer creates a new Logger tracer to collect execution traces from an // EVM transaction. -func NewTracer(tracer string, msg core.Message, cfg *ethparams.ChainConfig, height int64) vm.EVMLogger { +func NewTracer(tracer string, msg core.Message, cfg *ethparams.ChainConfig, height int64) corevm.EVMLogger { // TODO: enable additional log configuration logCfg := &logger.Config{ Debug: true, @@ -31,7 +31,7 @@ func NewTracer(tracer string, msg core.Message, cfg *ethparams.ChainConfig, heig switch tracer { case TracerAccessList: const mergeNetsplit = true - preCompiles := vm.ActivePrecompiles(cfg.Rules(big.NewInt(height), mergeNetsplit)) + preCompiles := corevm.ActivePrecompiles(cfg.Rules(big.NewInt(height), mergeNetsplit)) return logger.NewAccessListTracer(msg.AccessList(), msg.From(), *msg.To(), preCompiles) case TracerJSON: return logger.NewJSONLogger(logCfg, os.Stderr) @@ -50,7 +50,7 @@ type TxTraceResult struct { Error string `json:"error,omitempty"` // Trace failure produced by the tracer } -var _ vm.EVMLogger = &NoOpTracer{} +var _ corevm.EVMLogger = &NoOpTracer{} // NoOpTracer is an empty implementation of vm.Tracer interface type NoOpTracer struct{} @@ -63,7 +63,7 @@ func NewNoOpTracer() *NoOpTracer { // CaptureStart implements vm.Tracer interface // //nolint:revive // allow unused parameters to indicate expected signature -func (dt NoOpTracer) CaptureStart(env *vm.EVM, +func (dt NoOpTracer) CaptureStart(env *corevm.EVM, from common.Address, to common.Address, create bool, @@ -75,13 +75,13 @@ func (dt NoOpTracer) CaptureStart(env *vm.EVM, // CaptureState implements vm.Tracer interface // //nolint:revive // allow unused parameters to indicate expected signature -func (dt NoOpTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { +func (dt NoOpTracer) CaptureState(pc uint64, op corevm.OpCode, gas, cost uint64, scope *corevm.ScopeContext, rData []byte, depth int, err error) { } // CaptureFault implements vm.Tracer interface // //nolint:revive // allow unused parameters to indicate expected signature -func (dt NoOpTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { +func (dt NoOpTracer) CaptureFault(pc uint64, op corevm.OpCode, gas, cost uint64, scope *corevm.ScopeContext, depth int, err error) { } // CaptureEnd implements vm.Tracer interface @@ -92,7 +92,7 @@ func (dt NoOpTracer) CaptureEnd(output []byte, gasUsed uint64, tm time.Duration, // CaptureEnter implements vm.Tracer interface // //nolint:revive // allow unused parameters to indicate expected signature -func (dt NoOpTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +func (dt NoOpTracer) CaptureEnter(typ corevm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { } // CaptureExit implements vm.Tracer interface diff --git a/x/evm/types/tx.go b/x/evm/types/tx.go index 8efa383148..7630b3abb2 100644 --- a/x/evm/types/tx.go +++ b/x/evm/types/tx.go @@ -6,7 +6,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" ) // EvmTxArgs encapsulates all possible params to create all EVM txs types. @@ -42,7 +42,7 @@ func (m *MsgEthereumTxResponse) Return() []byte { // Revert returns the concrete revert reason if the execution is aborted by `REVERT` // opcode. Note the reason can be nil if no data supplied with revert opcode. func (m *MsgEthereumTxResponse) Revert() []byte { - if m.VmError != vm.ErrExecutionReverted.Error() { + if m.VmError != corevm.ErrExecutionReverted.Error() { return nil } return common.CopyBytes(m.Ret) diff --git a/x/evm/types/utils.go b/x/evm/types/utils.go index 7dd5463e77..f2c876dd99 100644 --- a/x/evm/types/utils.go +++ b/x/evm/types/utils.go @@ -76,5 +76,5 @@ func BinSearch(lo, hi uint64, executable func(uint64) (bool, *MsgEthereumTxRespo // IsEmptyCodeHash returns true if the given code hash is the empty code hash func IsEmptyCodeHash(codeHash common.Hash) bool { - return bytes.Equal(codeHash.Bytes(), EmptyCodeHash) + return codeHash == (common.Hash{}) || bytes.Equal(codeHash.Bytes(), EmptyCodeHash) } From 8875a841f9159f7288ac9cbea04858a508b8401d Mon Sep 17 00:00:00 2001 From: VictorTrustyDev Date: Sun, 22 Sep 2024 02:43:39 +0700 Subject: [PATCH 3/6] refactor and integrate Context-based-StateDB to AnteHandler --- app/antedl/ante.go | 4 ++-- app/antedl/evmlane/993e_exec_without_error.go | 18 ++++++++++-------- .../evmlane/993e_exec_without_error_it_test.go | 4 +++- app/modules.go | 2 +- rpc/backend/call_tx.go | 4 ++-- testutil/tx/signer.go | 6 ++++++ 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/antedl/ante.go b/app/antedl/ante.go index e500491d86..be17d8ef1d 100644 --- a/app/antedl/ante.go +++ b/app/antedl/ante.go @@ -39,8 +39,8 @@ func NewAnteHandler(options HandlerOptions) sdk.AnteHandler { // EVM-only lane evmlane.NewEvmLaneSetupExecutionDecorator(*options.EvmKeeper), - evmlane.NewEvmLaneEmitEventDecorator(*options.EvmKeeper), // must be the last effective Ante - evmlane.NewEvmLaneExecWithoutErrorDecorator(*options.AccountKeeper, *options.EvmKeeper), // simulation ante + evmlane.NewEvmLaneEmitEventDecorator(*options.EvmKeeper), // must be the last effective Ante + evmlane.NewEvmLaneExecWithoutErrorDecorator(*options.AccountKeeper, options.BankKeeper, *options.EvmKeeper), // simulation ante // Cosmos-only lane cosmoslane.NewCosmosLaneRejectEthereumMsgsDecorator(), diff --git a/app/antedl/evmlane/993e_exec_without_error.go b/app/antedl/evmlane/993e_exec_without_error.go index 0bea3c05cd..1da1544aff 100644 --- a/app/antedl/evmlane/993e_exec_without_error.go +++ b/app/antedl/evmlane/993e_exec_without_error.go @@ -3,11 +3,13 @@ package evmlane import ( "errors" + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + errorsmod "cosmossdk.io/errors" dlanteutils "github.com/EscanBE/evermint/v12/app/antedl/utils" evmkeeper "github.com/EscanBE/evermint/v12/x/evm/keeper" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -15,11 +17,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" ) type ELExecWithoutErrorDecorator struct { ak authkeeper.AccountKeeper + bk bankkeeper.Keeper ek evmkeeper.Keeper } @@ -27,9 +30,10 @@ type ELExecWithoutErrorDecorator struct { // This decorator only executes in (re)check-tx and simulation mode. // - If the input transaction is a Cosmos transaction, it calls next ante handler. // - If the input transaction is an Ethereum transaction, it runs simulate the state transition to ensure tx can be executed. -func NewEvmLaneExecWithoutErrorDecorator(ak authkeeper.AccountKeeper, ek evmkeeper.Keeper) ELExecWithoutErrorDecorator { +func NewEvmLaneExecWithoutErrorDecorator(ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, ek evmkeeper.Keeper) ELExecWithoutErrorDecorator { return ELExecWithoutErrorDecorator{ ak: ak, + bk: bk, ek: ek, } } @@ -72,13 +76,11 @@ func (ed ELExecWithoutErrorDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim ed.ek.SetFlagSenderNonceIncreasedByAnteHandle(simulationCtx, false) } - var evm *vm.EVM + var evm *corevm.EVM { // initialize EVM - txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(simulationCtx.HeaderHash())) - txConfig = txConfig.WithTxTypeFromMessage(ethCoreMsg) - stateDB := statedb.New(simulationCtx, &ed.ek, txConfig) + stateDB := evmvm.NewStateDB(simulationCtx, &ed.ek, ed.ak, ed.bk) evmParams := ed.ek.GetParams(simulationCtx) - evmCfg := &statedb.EVMConfig{ + evmCfg := &evmvm.EVMConfig{ Params: evmParams, ChainConfig: evmParams.ChainConfig.EthereumConfig(ed.ek.ChainID()), CoinBase: common.Address{}, diff --git a/app/antedl/evmlane/993e_exec_without_error_it_test.go b/app/antedl/evmlane/993e_exec_without_error_it_test.go index ed1add1e6a..d8b71926df 100644 --- a/app/antedl/evmlane/993e_exec_without_error_it_test.go +++ b/app/antedl/evmlane/993e_exec_without_error_it_test.go @@ -150,7 +150,9 @@ func (s *ELTestSuite) Test_ELExecWithoutErrorDecorator() { cachedCtx, _ := s.Ctx().CacheContext() tt.decoratorSpec.WithDecorator( - evmlane.NewEvmLaneExecWithoutErrorDecorator(*s.App().AccountKeeper(), *s.App().EvmKeeper()), + evmlane.NewEvmLaneExecWithoutErrorDecorator( + *s.App().AccountKeeper(), s.App().BankKeeper(), *s.App().EvmKeeper(), + ), ) if tt.reCheckTx { diff --git a/app/modules.go b/app/modules.go index 9f2f6925df..c132ad09e0 100644 --- a/app/modules.go +++ b/app/modules.go @@ -68,7 +68,7 @@ var maccPerms = map[string][]string{ minttypes.ModuleName: {authtypes.Minter}, ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, icatypes.ModuleName: nil, - evmtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, // used for secure addition and subtraction of balance using module account + evmtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, // used for secure addition and subtraction of balance erc20types.ModuleName: {authtypes.Minter, authtypes.Burner}, vauthtypes.ModuleName: {authtypes.Burner}, } diff --git a/rpc/backend/call_tx.go b/rpc/backend/call_tx.go index 72a702c5a3..7d17c007c4 100644 --- a/rpc/backend/call_tx.go +++ b/rpc/backend/call_tx.go @@ -16,7 +16,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" "github.com/pkg/errors" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -381,7 +381,7 @@ func (b *Backend) DoCall( } if res.Failed() { - if res.VmError != vm.ErrExecutionReverted.Error() { + if res.VmError != corevm.ErrExecutionReverted.Error() { return nil, status.Error(codes.Internal, res.VmError) } return nil, evmtypes.NewExecErrorWithReason(res.Ret) diff --git a/testutil/tx/signer.go b/testutil/tx/signer.go index 2687fad632..1dd0a41da6 100644 --- a/testutil/tx/signer.go +++ b/testutil/tx/signer.go @@ -41,6 +41,12 @@ func GenerateAddress() common.Address { return addr } +// GenerateHash generates an Ethereum hash. +func GenerateHash() common.Hash { + _, pk := NewAddrKey() + return common.BytesToHash(pk.Bytes()) +} + var _ keyring.Signer = &Signer{} // Signer defines a type that is used on testing for signing MsgEthereumTx From dd306c0d6c7fa65030add49e037fc5d834dcef0c Mon Sep 17 00:00:00 2001 From: VictorTrustyDev Date: Sun, 22 Sep 2024 02:44:02 +0700 Subject: [PATCH 4/6] refactor and integrate Context-based-StateDB to `x/erc20` --- x/erc20/keeper/mock_test.go | 12 +- x/erc20/keeper/msg_server.go | 8 +- x/erc20/keeper/msg_server_test.go | 700 ++---------------------------- x/erc20/keeper/utils_test.go | 6 +- x/erc20/types/interfaces.go | 7 +- 5 files changed, 37 insertions(+), 696 deletions(-) diff --git a/x/erc20/keeper/mock_test.go b/x/erc20/keeper/mock_test.go index f7f2037802..2dbeba3c60 100644 --- a/x/erc20/keeper/mock_test.go +++ b/x/erc20/keeper/mock_test.go @@ -7,13 +7,12 @@ import ( bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" erc20types "github.com/EscanBE/evermint/v12/x/erc20/types" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" "github.com/stretchr/testify/mock" ) @@ -29,12 +28,9 @@ func (m *MockEVMKeeper) GetParams(_ sdk.Context) evmtypes.Params { return args.Get(0).(evmtypes.Params) } -func (m *MockEVMKeeper) GetAccountWithoutBalance(_ sdk.Context, _ common.Address) *statedb.Account { +func (m *MockEVMKeeper) IsEmptyAccount(_ sdk.Context, _ common.Address) bool { args := m.Called(mock.Anything, mock.Anything) - if args.Get(0) == nil { - return nil - } - return args.Get(0).(*statedb.Account) + return args.Get(0).(bool) } func (m *MockEVMKeeper) EstimateGas(_ context.Context, _ *evmtypes.EthCallRequest) (*evmtypes.EstimateGasResponse, error) { @@ -45,7 +41,7 @@ func (m *MockEVMKeeper) EstimateGas(_ context.Context, _ *evmtypes.EthCallReques return args.Get(0).(*evmtypes.EstimateGasResponse), args.Error(1) } -func (m *MockEVMKeeper) ApplyMessage(_ sdk.Context, _ core.Message, _ vm.EVMLogger, _ bool) (*evmtypes.MsgEthereumTxResponse, error) { +func (m *MockEVMKeeper) ApplyMessage(_ sdk.Context, _ core.Message, _ corevm.EVMLogger, _ bool) (*evmtypes.MsgEthereumTxResponse, error) { args := m.Called(mock.Anything, mock.Anything, mock.Anything, mock.Anything) if args.Get(0) == nil { diff --git a/x/erc20/keeper/msg_server.go b/x/erc20/keeper/msg_server.go index b72ef5d16f..dd5ed5a08d 100644 --- a/x/erc20/keeper/msg_server.go +++ b/x/erc20/keeper/msg_server.go @@ -37,9 +37,7 @@ func (k Keeper) ConvertCoin( // Remove token pair if contract is suicided erc20 := common.HexToAddress(pair.Erc20Address) - acc := k.evmKeeper.GetAccountWithoutBalance(ctx, erc20) - - if acc == nil || !acc.IsContract() { + if k.evmKeeper.IsEmptyAccount(ctx, erc20) { k.DeleteTokenPair(ctx, pair) k.Logger(ctx).Debug( "deleting selfdestructed token pair from state", @@ -79,9 +77,7 @@ func (k Keeper) ConvertERC20( // Remove token pair if contract is suicided erc20 := common.HexToAddress(pair.Erc20Address) - acc := k.evmKeeper.GetAccountWithoutBalance(ctx, erc20) - - if acc == nil || !acc.IsContract() { + if k.evmKeeper.IsEmptyAccount(ctx, erc20) { k.DeleteTokenPair(ctx, pair) k.Logger(ctx).Debug( "deleting selfdestructed token pair from state", diff --git a/x/erc20/keeper/msg_server_test.go b/x/erc20/keeper/msg_server_test.go index f0db56e1c2..3616639a16 100644 --- a/x/erc20/keeper/msg_server_test.go +++ b/x/erc20/keeper/msg_server_test.go @@ -1,7 +1,6 @@ package keeper_test import ( - "fmt" "math/big" sdkmath "cosmossdk.io/math" @@ -10,14 +9,9 @@ import ( govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/mock" - "github.com/ethereum/go-ethereum/common" - erc20keeper "github.com/EscanBE/evermint/v12/x/erc20/keeper" erc20types "github.com/EscanBE/evermint/v12/x/erc20/types" - "github.com/EscanBE/evermint/v12/x/evm/statedb" - evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" ) func (suite *KeeperTestSuite) TestConvertCoinNativeCoin() { @@ -56,7 +50,7 @@ func (suite *KeeperTestSuite) TestConvertCoinNativeCoin() { stateDB := suite.StateDB() ok := stateDB.Suicide(erc20) suite.Require().True(ok) - suite.Require().NoError(stateDB.Commit()) + suite.Require().NoError(stateDB.CommitMultiStore(false)) }, extra: func() {}, expPass: true, @@ -97,80 +91,6 @@ func (suite *KeeperTestSuite) TestConvertCoinNativeCoin() { expPass: false, selfdestructed: false, }, - { - name: "fail - force evm fail", - mint: 100, - burn: 10, - malleate: func(common.Address) {}, - extra: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - selfdestructed: false, - }, - { - name: "fail - force evm balance error", - mint: 100, - burn: 10, - malleate: func(common.Address) {}, - extra: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - // first balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - // convert coin - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil).Once() - // second balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, fmt.Errorf("third")).Once() - // Extra call on test - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - selfdestructed: false, - }, - { - name: "fail - force balance error", - mint: 100, - burn: 10, - malleate: func(common.Address) {}, - extra: func() { - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Times(4) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - selfdestructed: false, - }, } for _, tc := range testCases { suite.Run(tc.name, func() { @@ -208,21 +128,19 @@ func (suite *KeeperTestSuite) TestConvertCoinNativeCoin() { if tc.expPass { suite.Require().NoError(err) - acc := suite.app.EvmKeeper.GetAccountWithoutBalance(suite.ctx, erc20) + isEmptyAcc := suite.app.EvmKeeper.IsEmptyAccount(suite.ctx, erc20) if tc.selfdestructed { - suite.Require().Nil(acc, "expected contract to be destroyed") - } else { - suite.Require().NotNil(acc) - } + suite.Require().True(isEmptyAcc, "expected contract to be destroyed") - if tc.selfdestructed || !acc.IsContract() { id := suite.app.Erc20Keeper.GetTokenPairID(suite.ctx, erc20.String()) _, found := suite.app.Erc20Keeper.GetTokenPair(suite.ctx, id) suite.Require().False(found) } else { + suite.Require().False(isEmptyAcc) + suite.Require().Equal(&erc20types.MsgConvertCoinResponse{}, res) - suite.Require().Equal(cosmosBalance.Amount.Int64(), sdkmath.NewInt(tc.mint-tc.burn).Int64()) - suite.Require().Equal(balance.(*big.Int).Int64(), big.NewInt(tc.burn).Int64()) + suite.Require().Equal(sdkmath.NewInt(tc.mint-tc.burn).Int64(), cosmosBalance.Amount.Int64()) + suite.Require().Equal(big.NewInt(tc.burn).Int64(), balance.(*big.Int).Int64()) } } else { suite.Require().Error(err) @@ -284,123 +202,6 @@ func (suite *KeeperTestSuite) TestConvertERC20NativeCoin() { }, expPass: false, }, - { - name: "fail - force evm fail", - mint: 100, - burn: 10, - reconvert: 5, - malleate: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - }, - { - name: "fail - force fail second balance", - mint: 100, - burn: 10, - reconvert: 5, - malleate: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - // first balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - // convert coin - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil).Once() - // second balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, fmt.Errorf("third")).Once() - // Extra call on test - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - }, - { - name: "fail - force fail second balance", - mint: 100, - burn: 10, - reconvert: 5, - malleate: func() { - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - // first balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - // convert coin - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil).Once() - // second balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - // Extra call on test - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - }, - { - name: "fail - force fail unescrow", - mint: 100, - burn: 10, - reconvert: 5, - malleate: func() { - mockBankKeeper := &MockBankKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - mockBankKeeper, suite.app.EvmKeeper, - ) - - mockBankKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(nil) - mockBankKeeper.On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("failed to unescrow")) - mockBankKeeper.On("BlockedAddr", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(false) - mockBankKeeper.On("GetBalance", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(sdk.Coin{Denom: "coin", Amount: sdkmath.OneInt()}) - }, - expPass: false, - }, - { - name: "fail - force fail balance after transfer", - mint: 100, - burn: 10, - reconvert: 5, - malleate: func() { - mockBankKeeper := &MockBankKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - mockBankKeeper, suite.app.EvmKeeper, - ) - - mockBankKeeper.On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - mockBankKeeper.On("BlockedAddr", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(false) - mockBankKeeper.On("GetBalance", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(sdk.Coin{Denom: "acoin", Amount: sdkmath.OneInt()}) - }, - expPass: false, - }, } for _, tc := range testCases { //nolint:dupl suite.Run(tc.name, func() { @@ -515,7 +316,7 @@ func (suite *KeeperTestSuite) TestConvertERC20NativeERC20() { stateDB := suite.StateDB() ok := stateDB.Suicide(erc20) suite.Require().True(ok) - suite.Require().NoError(stateDB.Commit()) + suite.Require().NoError(stateDB.CommitMultiStore(false)) }, extra: func() {}, contractType: contractMinterBurner, @@ -590,171 +391,6 @@ func (suite *KeeperTestSuite) TestConvertERC20NativeERC20() { expPass: false, selfdestructed: false, }, - { - name: "fail - force evm fail", - mint: 100, - transfer: 10, - malleate: func(common.Address) {}, - extra: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - contractType: contractMinterBurner, - expPass: false, - selfdestructed: false, - }, - { - name: "fail - force get balance fail", - mint: 100, - transfer: 10, - malleate: func(common.Address) {}, - extra: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - balance[31] = uint8(1) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced balance error")) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - contractType: contractMinterBurner, - expPass: false, - selfdestructed: false, - }, - { - name: "fail - force transfer unpack fail", - mint: 100, - transfer: 10, - malleate: func(common.Address) {}, - extra: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - contractType: contractMinterBurner, - expPass: false, - selfdestructed: false, - }, - { - name: "fail - force invalid transfer fail", - mint: 100, - transfer: 10, - malleate: func(common.Address) {}, - extra: func() { - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - contractType: contractMinterBurner, - expPass: false, - selfdestructed: false, - }, - { - name: "fail - force mint fail", - mint: 100, - transfer: 10, - malleate: func(common.Address) {}, - extra: func() { - mockBankKeeper := &MockBankKeeper{} - - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - mockBankKeeper, suite.app.EvmKeeper, - ) - - mockBankKeeper.On("MintCoins", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("failed to mint")) - mockBankKeeper.On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("failed to unescrow")) - mockBankKeeper.On("BlockedAddr", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(false) - mockBankKeeper.On("GetBalance", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(sdk.Coin{Denom: "coin", Amount: sdkmath.OneInt()}) - }, - contractType: contractMinterBurner, - expPass: false, - selfdestructed: false, - }, - { - name: "fail - force send minted fail", - mint: 100, - transfer: 10, - malleate: func(common.Address) {}, - extra: func() { - mockBankKeeper := &MockBankKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - mockBankKeeper, suite.app.EvmKeeper, - ) - - mockBankKeeper.On("MintCoins", mock.Anything, mock.Anything, mock.Anything).Return(nil) - mockBankKeeper.On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("failed to unescrow")) - mockBankKeeper.On("BlockedAddr", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(false) - mockBankKeeper.On("GetBalance", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(sdk.Coin{Denom: "coin", Amount: sdkmath.OneInt()}) - }, - contractType: contractMinterBurner, - expPass: false, - selfdestructed: false, - }, - { - name: "fail - force bank balance fail", - mint: 100, - transfer: 10, - malleate: func(common.Address) {}, - extra: func() { - mockBankKeeper := &MockBankKeeper{} - - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - mockBankKeeper, suite.app.EvmKeeper, - ) - - mockBankKeeper.On("MintCoins", mock.Anything, mock.Anything, mock.Anything).Return(nil) - mockBankKeeper.On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - mockBankKeeper.On("BlockedAddr", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(false) - mockBankKeeper.On("GetBalance", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(sdk.Coin{Denom: coinName, Amount: sdkmath.NewInt(int64(10))}) - }, - contractType: contractMinterBurner, - expPass: false, - selfdestructed: false, - }, } for _, tc := range testCases { suite.Run(tc.name, func() { @@ -792,21 +428,19 @@ func (suite *KeeperTestSuite) TestConvertERC20NativeERC20() { if tc.expPass { suite.Require().NoError(err, tc.name) - acc := suite.app.EvmKeeper.GetAccountWithoutBalance(suite.ctx, contractAddr) + isEmptyAcc := suite.app.EvmKeeper.IsEmptyAccount(suite.ctx, contractAddr) if tc.selfdestructed { - suite.Require().Nil(acc, "expected contract to be destroyed") - } else { - suite.Require().NotNil(acc) - } + suite.Require().True(isEmptyAcc, "expected contract to be destroyed") - if tc.selfdestructed || !acc.IsContract() { id := suite.app.Erc20Keeper.GetTokenPairID(suite.ctx, contractAddr.String()) _, found := suite.app.Erc20Keeper.GetTokenPair(suite.ctx, id) suite.Require().False(found) } else { + suite.Require().False(isEmptyAcc) + suite.Require().Equal(&erc20types.MsgConvertERC20Response{}, res) - suite.Require().Equal(cosmosBalance.Amount, sdkmath.NewInt(tc.transfer)) - suite.Require().Equal(balance.(*big.Int).Int64(), big.NewInt(tc.mint-tc.transfer).Int64()) + suite.Require().Equal(sdkmath.NewInt(tc.transfer), cosmosBalance.Amount) + suite.Require().Equal(big.NewInt(tc.mint-tc.transfer).Int64(), balance.(*big.Int).Int64()) } } else { suite.Require().Error(err, tc.name) @@ -885,98 +519,6 @@ func (suite *KeeperTestSuite) TestConvertCoinNativeERC20() { contractType: contractMinterBurner, expPass: false, }, - { - name: "fail - force evm fail", - mint: 100, - convert: 10, - malleate: func(common.Address) {}, - extra: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - contractType: contractMinterBurner, - expPass: false, - }, - { - name: "fail - force invalid transfer", - mint: 100, - convert: 10, - malleate: func(common.Address) {}, - extra: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - contractType: contractMinterBurner, - expPass: false, - }, - { - name: "fail - force fail second balance", - mint: 100, - convert: 10, - malleate: func(common.Address) {}, - extra: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - balance[31] = uint8(1) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("fail second balance")) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - contractType: contractMinterBurner, - expPass: false, - }, - { - name: "fail - force fail transfer", - mint: 100, - convert: 10, - malleate: func(common.Address) {}, - extra: func() { - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - contractType: contractMinterBurner, - expPass: false, - }, } for _, tc := range testCases { suite.Run(tc.name, func() { @@ -1132,7 +674,7 @@ func (suite *KeeperTestSuite) TestConvertCoinNativeIBCVoucher() { stateDB := suite.StateDB() ok := stateDB.Suicide(erc20) suite.Require().True(ok) - suite.Require().NoError(stateDB.Commit()) + suite.Require().NoError(stateDB.CommitMultiStore(false)) }, extra: func() {}, expPass: true, @@ -1172,80 +714,6 @@ func (suite *KeeperTestSuite) TestConvertCoinNativeIBCVoucher() { expPass: false, selfdestructed: false, }, - { - name: "fail - force evm fail", - mint: 100, - burn: 10, - malleate: func(common.Address) {}, - extra: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - selfdestructed: false, - }, - { - name: "fail - force evm balance error", - mint: 100, - burn: 10, - malleate: func(common.Address) {}, - extra: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - // first balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - // convert coin - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil).Once() - // second balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, fmt.Errorf("third")).Once() - // Extra call on test - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - selfdestructed: false, - }, - { - name: "fail - force balance error", - mint: 100, - burn: 10, - malleate: func(common.Address) {}, - extra: func() { - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Times(4) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - selfdestructed: false, - }, } for _, tc := range testCases { suite.Run(tc.name, func() { @@ -1265,8 +733,10 @@ func (suite *KeeperTestSuite) TestConvertCoinNativeIBCVoucher() { sender, ) - suite.app.BankKeeper.MintCoins(suite.ctx, erc20types.ModuleName, coins) //nolint:errcheck - suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, erc20types.ModuleName, sender, coins) + err := suite.app.BankKeeper.MintCoins(suite.ctx, erc20types.ModuleName, coins) + suite.Require().NoError(err) + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, erc20types.ModuleName, sender, coins) + suite.Require().NoError(err) tc.extra() res, err := suite.app.Erc20Keeper.ConvertCoin(suite.ctx, msg) @@ -1278,21 +748,19 @@ func (suite *KeeperTestSuite) TestConvertCoinNativeIBCVoucher() { if tc.expPass { suite.Require().NoError(err, tc.name) - acc := suite.app.EvmKeeper.GetAccountWithoutBalance(suite.ctx, erc20) + isEmptyAcc := suite.app.EvmKeeper.IsEmptyAccount(suite.ctx, erc20) if tc.selfdestructed { - suite.Require().Nil(acc, "expected contract to be destroyed") - } else { - suite.Require().NotNil(acc) - } + suite.Require().True(isEmptyAcc, "expected contract to be destroyed") - if tc.selfdestructed || !acc.IsContract() { id := suite.app.Erc20Keeper.GetTokenPairID(suite.ctx, erc20.String()) _, found := suite.app.Erc20Keeper.GetTokenPair(suite.ctx, id) suite.Require().False(found) } else { + suite.Require().False(isEmptyAcc) + suite.Require().Equal(expRes, res) - suite.Require().Equal(cosmosBalance.Amount.Int64(), sdkmath.NewInt(tc.mint-tc.burn).Int64()) - suite.Require().Equal(balance.(*big.Int).Int64(), big.NewInt(tc.burn).Int64()) + suite.Require().Equal(sdkmath.NewInt(tc.mint-tc.burn).Int64(), cosmosBalance.Amount.Int64()) + suite.Require().Equal(big.NewInt(tc.burn).Int64(), balance.(*big.Int).Int64()) } } else { suite.Require().Error(err, tc.name) @@ -1354,124 +822,6 @@ func (suite *KeeperTestSuite) TestConvertERC20NativeIBCVoucher() { }, expPass: false, }, - { - name: "fail - force evm fail", - mint: 100, - burn: 10, - reconvert: 5, - malleate: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - }, - { - name: "fail - force fail second balance", - mint: 100, - burn: 10, - reconvert: 5, - malleate: func() { //nolint:dupl - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - // first balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - // convert coin - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil).Once() - // second balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, fmt.Errorf("third")).Once() - // Extra call on test - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - }, - { - name: "fail - force fail second balance", - mint: 100, - burn: 10, - reconvert: 5, - malleate: func() { - mockEVMKeeper := &MockEVMKeeper{} - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - suite.app.BankKeeper, mockEVMKeeper, - ) - - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: common.Big1} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGas", mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - // first balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - // convert coin - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil).Once() - // second balance of - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - // Extra call on test - mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - }, - expPass: false, - }, - { - name: "fail - force fail unescrow", - mint: 100, - burn: 10, - reconvert: 5, - malleate: func() { - mockBankKeeper := &MockBankKeeper{} - - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - mockBankKeeper, suite.app.EvmKeeper, - ) - - mockBankKeeper.On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("failed to unescrow")) - mockBankKeeper.On("BlockedAddr", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(false) - mockBankKeeper.On("GetBalance", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(sdk.Coin{Denom: "coin", Amount: sdkmath.OneInt()}) - }, - expPass: false, - }, - { - name: "fail - force fail balance after transfer", - mint: 100, - burn: 10, - reconvert: 5, - malleate: func() { - mockBankKeeper := &MockBankKeeper{} - - suite.app.Erc20Keeper = erc20keeper.NewKeeper( - suite.app.GetKey("erc20"), suite.app.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), suite.app.AccountKeeper, - mockBankKeeper, suite.app.EvmKeeper, - ) - - mockBankKeeper.On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - mockBankKeeper.On("BlockedAddr", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(false) - mockBankKeeper.On("GetBalance", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(sdk.Coin{Denom: ibcBase, Amount: sdkmath.OneInt()}) - }, - expPass: false, - }, } for _, tc := range testCases { //nolint:dupl suite.Run(tc.name, func() { diff --git a/x/erc20/keeper/utils_test.go b/x/erc20/keeper/utils_test.go index d153ae2d27..b659c34790 100644 --- a/x/erc20/keeper/utils_test.go +++ b/x/erc20/keeper/utils_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "bytes" "encoding/json" + evmvm "github.com/EscanBE/evermint/v12/x/evm/vm" "math/big" "strconv" "time" @@ -21,7 +22,6 @@ import ( utiltx "github.com/EscanBE/evermint/v12/testutil/tx" teststypes "github.com/EscanBE/evermint/v12/types/tests" erc20types "github.com/EscanBE/evermint/v12/x/erc20/types" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" feemarkettypes "github.com/EscanBE/evermint/v12/x/feemarket/types" "github.com/cosmos/cosmos-sdk/baseapp" @@ -242,8 +242,8 @@ func (suite *KeeperTestSuite) SetupIBCTest() { var timeoutHeight = clienttypes.NewHeight(1000, 1000) -func (suite *KeeperTestSuite) StateDB() *statedb.StateDB { - return statedb.New(suite.ctx, suite.app.EvmKeeper, statedb.NewEmptyTxConfig(common.BytesToHash(suite.ctx.HeaderHash()))) +func (suite *KeeperTestSuite) StateDB() evmvm.CStateDB { + return evmvm.NewStateDB(suite.ctx, suite.app.EvmKeeper, suite.app.AccountKeeper, suite.app.BankKeeper) } func (suite *KeeperTestSuite) MintFeeCollector(coins sdk.Coins) { diff --git a/x/erc20/types/interfaces.go b/x/erc20/types/interfaces.go index ac44afe710..96772425f6 100644 --- a/x/erc20/types/interfaces.go +++ b/x/erc20/types/interfaces.go @@ -8,9 +8,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/vm" + corevm "github.com/ethereum/go-ethereum/core/vm" - "github.com/EscanBE/evermint/v12/x/evm/statedb" evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" ) @@ -24,9 +23,9 @@ type AccountKeeper interface { // EVMKeeper defines the expected EVM keeper interface used on erc20 type EVMKeeper interface { GetParams(ctx sdk.Context) evmtypes.Params - GetAccountWithoutBalance(ctx sdk.Context, addr common.Address) *statedb.Account + IsEmptyAccount(ctx sdk.Context, addr common.Address) bool EstimateGas(c context.Context, req *evmtypes.EthCallRequest) (*evmtypes.EstimateGasResponse, error) - ApplyMessage(ctx sdk.Context, msg core.Message, tracer vm.EVMLogger, commit bool) (*evmtypes.MsgEthereumTxResponse, error) + ApplyMessage(ctx sdk.Context, msg core.Message, tracer corevm.EVMLogger, commit bool) (*evmtypes.MsgEthereumTxResponse, error) SetFlagEnableNoBaseFee(ctx sdk.Context, enable bool) IsNoBaseFeeEnabled(ctx sdk.Context) bool } From ce647f77a5c5be462ea40a1fd4432745e8ea3403 Mon Sep 17 00:00:00 2001 From: VictorTrustyDev Date: Sun, 22 Sep 2024 02:44:19 +0700 Subject: [PATCH 5/6] remove legacy StateDB --- x/evm/statedb/access_list.go | 118 ------- x/evm/statedb/config.go | 65 ---- x/evm/statedb/interfaces.go | 33 -- x/evm/statedb/journal.go | 241 -------------- x/evm/statedb/mock_test.go | 113 ------- x/evm/statedb/state_object.go | 234 -------------- x/evm/statedb/statedb.go | 486 ---------------------------- x/evm/statedb/statedb_test.go | 586 ---------------------------------- 8 files changed, 1876 deletions(-) delete mode 100644 x/evm/statedb/access_list.go delete mode 100644 x/evm/statedb/config.go delete mode 100644 x/evm/statedb/interfaces.go delete mode 100644 x/evm/statedb/journal.go delete mode 100644 x/evm/statedb/mock_test.go delete mode 100644 x/evm/statedb/state_object.go delete mode 100644 x/evm/statedb/statedb.go delete mode 100644 x/evm/statedb/statedb_test.go diff --git a/x/evm/statedb/access_list.go b/x/evm/statedb/access_list.go deleted file mode 100644 index 4513a91641..0000000000 --- a/x/evm/statedb/access_list.go +++ /dev/null @@ -1,118 +0,0 @@ -// 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), - } -} - -// 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 deleted file mode 100644 index 17e8317a9a..0000000000 --- a/x/evm/statedb/config.go +++ /dev/null @@ -1,65 +0,0 @@ -package statedb - -import ( - "math/big" - - core "github.com/ethereum/go-ethereum/core" - ethtypes "github.com/ethereum/go-ethereum/core/types" - - evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" - "github.com/ethereum/go-ethereum/common" - ethparams "github.com/ethereum/go-ethereum/params" -) - -// 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 - TxType int16 // transaction type, optionally provided -} - -// NewTxConfig returns a TxConfig -func NewTxConfig(bhash, thash common.Hash, txIndex, logIndex uint) TxConfig { - return TxConfig{ - BlockHash: bhash, - TxHash: thash, - TxIndex: txIndex, - LogIndex: logIndex, - TxType: -1, - } -} - -func (m TxConfig) WithTxTypeFromMessage(msg core.Message) TxConfig { - if msg.GasTipCap() != nil || msg.GasFeeCap() != nil { - m.TxType = ethtypes.DynamicFeeTxType - } else if msg.AccessList() != nil { - m.TxType = ethtypes.AccessListTxType - } else { - m.TxType = ethtypes.LegacyTxType - } - return m -} - -// 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, - TxType: -1, - } -} - -// EVMConfig encapsulates common parameters needed to create an EVM to execute a message -// It's mainly to reduce the number of method parameters -type EVMConfig struct { - Params evmtypes.Params - ChainConfig *ethparams.ChainConfig - CoinBase common.Address - BaseFee *big.Int - NoBaseFee bool -} diff --git a/x/evm/statedb/interfaces.go b/x/evm/statedb/interfaces.go deleted file mode 100644 index ae016a2c86..0000000000 --- a/x/evm/statedb/interfaces.go +++ /dev/null @@ -1,33 +0,0 @@ -package statedb - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/vm" -) - -// ExtStateDB defines an extension to the interface provided by the go-ethereum -// codebase to support additional state transition functionalities. In particular -// it supports appending a new entry to the state journal through -// AppendJournalEntry so that the state can be reverted after running -// stateful precompiled contracts. -type ExtStateDB interface { - vm.StateDB - AppendJournalEntry(JournalEntry) -} - -// Keeper provide underlying storage of StateDB -type Keeper interface { - // Read methods - GetAccount(ctx sdk.Context, addr common.Address) *Account - 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 deleted file mode 100644 index 14bb7b1df3..0000000000 --- a/x/evm/statedb/journal.go +++ /dev/null @@ -1,241 +0,0 @@ -// 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, 0, len(j.dirties)) - for k := range j.dirties { - keys = append(keys, k) - } - 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 deleted file mode 100644 index 620d4aabf5..0000000000 --- a/x/evm/statedb/mock_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package statedb_test - -import ( - "errors" - "math/big" - - evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" - - "github.com/EscanBE/evermint/v12/x/evm/statedb" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" -) - -var ( - _ statedb.Keeper = &MockKeeper{} - errAddress common.Address = common.BigToAddress(big.NewInt(100)) -) - -type MockAcount struct { - account statedb.Account - states statedb.Storage -} - -type MockKeeper struct { - accounts map[common.Address]MockAcount - codes map[common.Hash][]byte -} - -func NewMockKeeper() *MockKeeper { - return &MockKeeper{ - accounts: make(map[common.Address]MockAcount), - codes: make(map[common.Hash][]byte), - } -} - -func (k MockKeeper) GetAccount(_ sdk.Context, addr common.Address) *statedb.Account { - acct, ok := k.accounts[addr] - if !ok { - return nil - } - return &acct.account -} - -func (k MockKeeper) GetState(_ sdk.Context, addr common.Address, key common.Hash) common.Hash { - return k.accounts[addr].states[key] -} - -func (k MockKeeper) GetCode(_ sdk.Context, codeHash common.Hash) []byte { - return k.codes[codeHash] -} - -func (k MockKeeper) ForEachStorage(_ sdk.Context, addr common.Address, cb func(key, value common.Hash) bool) { - if acct, ok := k.accounts[addr]; ok { - for k, v := range acct.states { - if !cb(k, v) { - return - } - } - } -} - -func (k MockKeeper) SetAccount(_ sdk.Context, addr common.Address, account statedb.Account) error { - if addr == errAddress { - return errors.New("mock db error") - } - acct, exists := k.accounts[addr] - if exists { - // update - acct.account = account - k.accounts[addr] = acct - } else { - k.accounts[addr] = MockAcount{account: account, states: make(statedb.Storage)} - } - return nil -} - -func (k MockKeeper) SetState(_ sdk.Context, addr common.Address, key common.Hash, value []byte) { - if acct, ok := k.accounts[addr]; ok { - if len(value) == 0 { - delete(acct.states, key) - } else { - acct.states[key] = common.BytesToHash(value) - } - } -} - -func (k MockKeeper) SetCode(_ sdk.Context, codeHash []byte, code []byte) { - k.codes[common.BytesToHash(codeHash)] = code -} - -func (k MockKeeper) DeleteAccount(_ sdk.Context, addr common.Address) error { - if addr == errAddress { - return errors.New("mock db error") - } - old := k.accounts[addr] - delete(k.accounts, addr) - if !evmtypes.IsEmptyCodeHash(common.BytesToHash(old.account.CodeHash)) { - delete(k.codes, common.BytesToHash(old.account.CodeHash)) - } - return nil -} - -func (k MockKeeper) Clone() *MockKeeper { - accounts := make(map[common.Address]MockAcount, len(k.accounts)) - for k, v := range k.accounts { - accounts[k] = v - } - codes := make(map[common.Hash][]byte, len(k.codes)) - for k, v := range k.codes { - codes[k] = v - } - return &MockKeeper{accounts, codes} -} diff --git a/x/evm/statedb/state_object.go b/x/evm/statedb/state_object.go deleted file mode 100644 index 6ffaef33f3..0000000000 --- a/x/evm/statedb/state_object.go +++ /dev/null @@ -1,234 +0,0 @@ -package statedb - -import ( - "bytes" - "math/big" - "sort" - - evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" - - "github.com/ethereum/go-ethereum/common" -) - -// 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: evmtypes.EmptyCodeHash, - } -} - -// IsContract returns if the account contains contract code. -func (acct Account) IsContract() bool { - return !evmtypes.IsEmptyCodeHash(common.BytesToHash(acct.CodeHash)) -} - -// 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, 0, len(s)) - for k := range s { - keys = append(keys, k) - } - 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 = evmtypes.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 && !s.account.IsContract() -} - -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 -} - -// -// 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 !s.account.IsContract() { - 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 deleted file mode 100644 index 372cc6357b..0000000000 --- a/x/evm/statedb/statedb.go +++ /dev/null @@ -1,486 +0,0 @@ -package statedb - -import ( - "fmt" - "math/big" - "sort" - - "github.com/EscanBE/evermint/v12/utils" - - errorsmod "cosmossdk.io/errors" - 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/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{} - -// preventCommit is a flag to prevent committing state changes to the underlying storage. -// This is used for testing purposes to simulate cases where Commit() is failed. -var preventCommit bool - -// 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 - - // 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 -} - -// GetContext returns the transaction Context. -func (s *StateDB) GetContext() sdk.Context { - return s.ctx -} - -// 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 -} - -// 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(_ common.Hash, _ []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 := s.keeper.GetAccount(s.ctx, addr) - 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 preventCommit { - return fmt.Errorf("failed to commit state changes") - } - 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 errorsmod.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 errorsmod.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 -} - -// ToggleStateDBPreventCommit toggles the flag to prevent committing state changes to the underlying storage. -// This is used for testing purposes to simulate cases where Commit() is failed. -func (s *StateDB) ToggleStateDBPreventCommit(prevent bool) { - if !utils.IsTestnet(s.ctx.ChainID()) { - panic("can only be called during testing") - } - preventCommit = prevent -} diff --git a/x/evm/statedb/statedb_test.go b/x/evm/statedb/statedb_test.go deleted file mode 100644 index 0ec2f92e05..0000000000 --- a/x/evm/statedb/statedb_test.go +++ /dev/null @@ -1,586 +0,0 @@ -package statedb_test - -import ( - "math/big" - "testing" - - evmtypes "github.com/EscanBE/evermint/v12/x/evm/types" - - "github.com/EscanBE/evermint/v12/x/evm/statedb" - 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/core/vm" - "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/suite" -) - -var ( - address common.Address = common.BigToAddress(big.NewInt(101)) - address2 common.Address = common.BigToAddress(big.NewInt(102)) - address3 common.Address = common.BigToAddress(big.NewInt(103)) - blockHash common.Hash = common.BigToHash(big.NewInt(9999)) - emptyTxConfig statedb.TxConfig = statedb.NewEmptyTxConfig(blockHash) -) - -type StateDBTestSuite struct { - suite.Suite -} - -func (suite *StateDBTestSuite) TestAccount() { - key1 := common.BigToHash(big.NewInt(1)) - value1 := common.BigToHash(big.NewInt(2)) - key2 := common.BigToHash(big.NewInt(3)) - value2 := common.BigToHash(big.NewInt(4)) - testCases := []struct { - name string - malleate func(*statedb.StateDB) - }{ - {"non-exist account", func(db *statedb.StateDB) { - suite.Require().Equal(false, db.Exist(address)) - suite.Require().Equal(true, db.Empty(address)) - suite.Require().Equal(big.NewInt(0), db.GetBalance(address)) - suite.Require().Equal([]byte(nil), db.GetCode(address)) - suite.Require().Equal(common.Hash{}, db.GetCodeHash(address)) - suite.Require().Equal(uint64(0), db.GetNonce(address)) - }}, - {"empty account", func(db *statedb.StateDB) { - db.CreateAccount(address) - suite.Require().NoError(db.Commit()) - - keeper := db.Keeper().(*MockKeeper) - acct := keeper.accounts[address] - suite.Require().Equal(statedb.NewEmptyAccount(), &acct.account) - suite.Require().Empty(acct.states) - suite.Require().False(acct.account.IsContract()) - - db = statedb.New(sdk.Context{}, keeper, emptyTxConfig) - suite.Require().Equal(true, db.Exist(address)) - suite.Require().Equal(true, db.Empty(address)) - suite.Require().Equal(big.NewInt(0), db.GetBalance(address)) - suite.Require().Equal([]byte(nil), db.GetCode(address)) - suite.Require().True(evmtypes.IsEmptyCodeHash(db.GetCodeHash(address))) - suite.Require().Equal(uint64(0), db.GetNonce(address)) - }}, - {"suicide", func(db *statedb.StateDB) { - // non-exist account. - suite.Require().False(db.Suicide(address)) - suite.Require().False(db.HasSuicided(address)) - - // create a contract account - db.CreateAccount(address) - db.SetCode(address, []byte("hello world")) - db.AddBalance(address, big.NewInt(100)) - db.SetState(address, key1, value1) - db.SetState(address, key2, value2) - suite.Require().NoError(db.Commit()) - - // suicide - db = statedb.New(sdk.Context{}, db.Keeper(), emptyTxConfig) - suite.Require().False(db.HasSuicided(address)) - suite.Require().True(db.Suicide(address)) - - // check dirty state - suite.Require().True(db.HasSuicided(address)) - // balance is cleared - suite.Require().Equal(big.NewInt(0), db.GetBalance(address)) - // but code and state are still accessible in dirty state - suite.Require().Equal(value1, db.GetState(address, key1)) - suite.Require().Equal([]byte("hello world"), db.GetCode(address)) - - suite.Require().NoError(db.Commit()) - - // not accessible from StateDB anymore - db = statedb.New(sdk.Context{}, db.Keeper(), emptyTxConfig) - suite.Require().False(db.Exist(address)) - - // and cleared in keeper too - keeper := db.Keeper().(*MockKeeper) - suite.Require().Empty(keeper.accounts) - suite.Require().Empty(keeper.codes) - }}, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - keeper := NewMockKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) - tc.malleate(db) - }) - } -} - -func (suite *StateDBTestSuite) TestAccountOverride() { - keeper := NewMockKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) - // test balance carry over when overwritten - amount := big.NewInt(1) - - // init an EOA account, account overridden only happens on EOA account. - db.AddBalance(address, amount) - db.SetNonce(address, 1) - - // override - db.CreateAccount(address) - - // check balance is not lost - suite.Require().Equal(amount, db.GetBalance(address)) - // but nonce is reset - suite.Require().Equal(uint64(0), db.GetNonce(address)) -} - -func (suite *StateDBTestSuite) TestDBError() { - testCases := []struct { - name string - malleate func(vm.StateDB) - }{ - {"set account", func(db vm.StateDB) { - db.SetNonce(errAddress, 1) - }}, - {"delete account", func(db vm.StateDB) { - db.SetNonce(errAddress, 1) - suite.Require().True(db.Suicide(errAddress)) - }}, - } - for _, tc := range testCases { - db := statedb.New(sdk.Context{}, NewMockKeeper(), emptyTxConfig) - tc.malleate(db) - suite.Require().Error(db.Commit()) - } -} - -func (suite *StateDBTestSuite) TestBalance() { - // NOTE: no need to test overflow/underflow, that is guaranteed by evm implementation. - testCases := []struct { - name string - malleate func(*statedb.StateDB) - expBalance *big.Int - }{ - {"add balance", func(db *statedb.StateDB) { - db.AddBalance(address, big.NewInt(10)) - }, big.NewInt(10)}, - {"sub balance", func(db *statedb.StateDB) { - db.AddBalance(address, big.NewInt(10)) - // get dirty balance - suite.Require().Equal(big.NewInt(10), db.GetBalance(address)) - db.SubBalance(address, big.NewInt(2)) - }, big.NewInt(8)}, - {"add zero balance", func(db *statedb.StateDB) { - db.AddBalance(address, big.NewInt(0)) - }, big.NewInt(0)}, - {"sub zero balance", func(db *statedb.StateDB) { - db.SubBalance(address, big.NewInt(0)) - }, big.NewInt(0)}, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - keeper := NewMockKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) - tc.malleate(db) - - // check dirty state - suite.Require().Equal(tc.expBalance, db.GetBalance(address)) - suite.Require().NoError(db.Commit()) - // check committed balance too - suite.Require().Equal(tc.expBalance, keeper.accounts[address].account.Balance) - }) - } -} - -func (suite *StateDBTestSuite) TestState() { - key1 := common.BigToHash(big.NewInt(1)) - value1 := common.BigToHash(big.NewInt(1)) - testCases := []struct { - name string - malleate func(*statedb.StateDB) - expStates statedb.Storage - }{ - {"empty state", func(db *statedb.StateDB) { - }, nil}, - {"set empty value", func(db *statedb.StateDB) { - db.SetState(address, key1, common.Hash{}) - }, statedb.Storage{}}, - {"noop state change", func(db *statedb.StateDB) { - db.SetState(address, key1, value1) - db.SetState(address, key1, common.Hash{}) - }, statedb.Storage{}}, - {"set state", func(db *statedb.StateDB) { - // check empty initial state - suite.Require().Equal(common.Hash{}, db.GetState(address, key1)) - suite.Require().Equal(common.Hash{}, db.GetCommittedState(address, key1)) - - // set state - db.SetState(address, key1, value1) - // query dirty state - suite.Require().Equal(value1, db.GetState(address, key1)) - // check committed state is still not exist - suite.Require().Equal(common.Hash{}, db.GetCommittedState(address, key1)) - - // set same value again, should be noop - db.SetState(address, key1, value1) - suite.Require().Equal(value1, db.GetState(address, key1)) - }, statedb.Storage{ - key1: value1, - }}, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - keeper := NewMockKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) - tc.malleate(db) - suite.Require().NoError(db.Commit()) - - // check committed states in keeper - suite.Require().Equal(tc.expStates, keeper.accounts[address].states) - - // check ForEachStorage - db = statedb.New(sdk.Context{}, keeper, emptyTxConfig) - collected := CollectContractStorage(db) - if len(tc.expStates) > 0 { - suite.Require().Equal(tc.expStates, collected) - } else { - suite.Require().Empty(collected) - } - }) - } -} - -func (suite *StateDBTestSuite) TestCode() { - code := []byte("hello world") - codeHash := crypto.Keccak256Hash(code) - - testCases := []struct { - name string - malleate func(vm.StateDB) - expCode []byte - expCodeHash common.Hash - }{ - {"non-exist account", func(vm.StateDB) {}, nil, common.Hash{}}, - {"empty account", func(db vm.StateDB) { - db.CreateAccount(address) - }, nil, common.BytesToHash(evmtypes.EmptyCodeHash)}, - {"set code", func(db vm.StateDB) { - db.SetCode(address, code) - }, code, codeHash}, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - keeper := NewMockKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) - tc.malleate(db) - - // check dirty state - suite.Require().Equal(tc.expCode, db.GetCode(address)) - suite.Require().Equal(len(tc.expCode), db.GetCodeSize(address)) - suite.Require().Equal(tc.expCodeHash, db.GetCodeHash(address)) - - suite.Require().NoError(db.Commit()) - - // check again - db = statedb.New(sdk.Context{}, keeper, emptyTxConfig) - suite.Require().Equal(tc.expCode, db.GetCode(address)) - suite.Require().Equal(len(tc.expCode), db.GetCodeSize(address)) - suite.Require().Equal(tc.expCodeHash, db.GetCodeHash(address)) - }) - } -} - -func (suite *StateDBTestSuite) TestRevertSnapshot() { - v1 := common.BigToHash(big.NewInt(1)) - v2 := common.BigToHash(big.NewInt(2)) - v3 := common.BigToHash(big.NewInt(3)) - testCases := []struct { - name string - malleate func(vm.StateDB) - }{ - {"set state", func(db vm.StateDB) { - db.SetState(address, v1, v3) - }}, - {"set nonce", func(db vm.StateDB) { - db.SetNonce(address, 10) - }}, - {"change balance", func(db vm.StateDB) { - db.AddBalance(address, big.NewInt(10)) - db.SubBalance(address, big.NewInt(5)) - }}, - {"override account", func(db vm.StateDB) { - db.CreateAccount(address) - }}, - {"set code", func(db vm.StateDB) { - db.SetCode(address, []byte("hello world")) - }}, - {"suicide", func(db vm.StateDB) { - db.SetState(address, v1, v2) - db.SetCode(address, []byte("hello world")) - suite.Require().True(db.Suicide(address)) - }}, - {"add log", func(db vm.StateDB) { - db.AddLog(ðtypes.Log{ - Address: address, - }) - }}, - {"add refund", func(db vm.StateDB) { - db.AddRefund(10) - db.SubRefund(5) - }}, - {"access list", func(db vm.StateDB) { - db.AddAddressToAccessList(address) - db.AddSlotToAccessList(address, v1) - }}, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - ctx := sdk.Context{} - keeper := NewMockKeeper() - - { - // do some arbitrary changes to the storage - db := statedb.New(ctx, keeper, emptyTxConfig) - db.SetNonce(address, 1) - db.AddBalance(address, big.NewInt(100)) - db.SetCode(address, []byte("hello world")) - db.SetState(address, v1, v2) - db.SetNonce(address2, 1) - suite.Require().NoError(db.Commit()) - } - - originalKeeper := keeper.Clone() - - // run test - db := statedb.New(ctx, keeper, emptyTxConfig) - rev := db.Snapshot() - tc.malleate(db) - db.RevertToSnapshot(rev) - - // check empty states after revert - suite.Require().Zero(db.GetRefund()) - suite.Require().Empty(db.Logs()) - - suite.Require().NoError(db.Commit()) - - // check keeper should stay the same - suite.Require().Equal(originalKeeper, keeper) - }) - } -} - -func (suite *StateDBTestSuite) TestNestedSnapshot() { - key := common.BigToHash(big.NewInt(1)) - value1 := common.BigToHash(big.NewInt(1)) - value2 := common.BigToHash(big.NewInt(2)) - - db := statedb.New(sdk.Context{}, NewMockKeeper(), emptyTxConfig) - - rev1 := db.Snapshot() - db.SetState(address, key, value1) - - rev2 := db.Snapshot() - db.SetState(address, key, value2) - suite.Require().Equal(value2, db.GetState(address, key)) - - db.RevertToSnapshot(rev2) - suite.Require().Equal(value1, db.GetState(address, key)) - - db.RevertToSnapshot(rev1) - suite.Require().Equal(common.Hash{}, db.GetState(address, key)) -} - -func (suite *StateDBTestSuite) TestInvalidSnapshotId() { - db := statedb.New(sdk.Context{}, NewMockKeeper(), emptyTxConfig) - suite.Require().Panics(func() { - db.RevertToSnapshot(1) - }) -} - -func (suite *StateDBTestSuite) TestAccessList() { - value1 := common.BigToHash(big.NewInt(1)) - value2 := common.BigToHash(big.NewInt(2)) - - testCases := []struct { - name string - malleate func(vm.StateDB) - }{ - {"add address", func(db vm.StateDB) { - suite.Require().False(db.AddressInAccessList(address)) - db.AddAddressToAccessList(address) - suite.Require().True(db.AddressInAccessList(address)) - - addrPresent, slotPresent := db.SlotInAccessList(address, value1) - suite.Require().True(addrPresent) - suite.Require().False(slotPresent) - - // add again, should be no-op - db.AddAddressToAccessList(address) - suite.Require().True(db.AddressInAccessList(address)) - }}, - {"add slot", func(db vm.StateDB) { - addrPresent, slotPresent := db.SlotInAccessList(address, value1) - suite.Require().False(addrPresent) - suite.Require().False(slotPresent) - db.AddSlotToAccessList(address, value1) - addrPresent, slotPresent = db.SlotInAccessList(address, value1) - suite.Require().True(addrPresent) - suite.Require().True(slotPresent) - - // add another slot - db.AddSlotToAccessList(address, value2) - addrPresent, slotPresent = db.SlotInAccessList(address, value2) - suite.Require().True(addrPresent) - suite.Require().True(slotPresent) - - // add again, should be noop - db.AddSlotToAccessList(address, value2) - addrPresent, slotPresent = db.SlotInAccessList(address, value2) - suite.Require().True(addrPresent) - suite.Require().True(slotPresent) - }}, - {"prepare access list", func(db vm.StateDB) { - al := ethtypes.AccessList{{ - Address: address3, - StorageKeys: []common.Hash{value1}, - }} - db.PrepareAccessList(address, &address2, vm.PrecompiledAddressesBerlin, al) - - // check sender and dst - suite.Require().True(db.AddressInAccessList(address)) - suite.Require().True(db.AddressInAccessList(address2)) - // check precompiles - suite.Require().True(db.AddressInAccessList(common.BytesToAddress([]byte{1}))) - // check AccessList - suite.Require().True(db.AddressInAccessList(address3)) - addrPresent, slotPresent := db.SlotInAccessList(address3, value1) - suite.Require().True(addrPresent) - suite.Require().True(slotPresent) - addrPresent, slotPresent = db.SlotInAccessList(address3, value2) - suite.Require().True(addrPresent) - suite.Require().False(slotPresent) - }}, - } - - for _, tc := range testCases { - db := statedb.New(sdk.Context{}, NewMockKeeper(), emptyTxConfig) - tc.malleate(db) - } -} - -func (suite *StateDBTestSuite) TestLog() { - txHash := common.BytesToHash([]byte("tx")) - // use a non-default tx config - txConfig := statedb.NewTxConfig( - blockHash, - txHash, - 1, 1, - ) - db := statedb.New(sdk.Context{}, NewMockKeeper(), txConfig) - data := []byte("hello world") - db.AddLog(ðtypes.Log{ - Address: address, - Topics: []common.Hash{}, - Data: data, - BlockNumber: 1, - }) - suite.Require().Equal(1, len(db.Logs())) - expecedLog := ðtypes.Log{ - Address: address, - Topics: []common.Hash{}, - Data: data, - BlockNumber: 1, - BlockHash: blockHash, - TxHash: txHash, - TxIndex: 1, - Index: 1, - } - suite.Require().Equal(expecedLog, db.Logs()[0]) - - db.AddLog(ðtypes.Log{ - Address: address, - Topics: []common.Hash{}, - Data: data, - BlockNumber: 1, - }) - suite.Require().Equal(2, len(db.Logs())) - expecedLog.Index++ - suite.Require().Equal(expecedLog, db.Logs()[1]) -} - -func (suite *StateDBTestSuite) TestRefund() { - testCases := []struct { - name string - malleate func(vm.StateDB) - expRefund uint64 - expPanic bool - }{ - {"add refund", func(db vm.StateDB) { - db.AddRefund(uint64(10)) - }, 10, false}, - {"sub refund", func(db vm.StateDB) { - db.AddRefund(uint64(10)) - db.SubRefund(uint64(5)) - }, 5, false}, - {"negative refund counter", func(db vm.StateDB) { - db.AddRefund(uint64(5)) - db.SubRefund(uint64(10)) - }, 0, true}, - } - for _, tc := range testCases { - db := statedb.New(sdk.Context{}, NewMockKeeper(), emptyTxConfig) - if !tc.expPanic { - tc.malleate(db) - suite.Require().Equal(tc.expRefund, db.GetRefund()) - } else { - suite.Require().Panics(func() { - tc.malleate(db) - }) - } - } -} - -func (suite *StateDBTestSuite) TestIterateStorage() { - key1 := common.BigToHash(big.NewInt(1)) - value1 := common.BigToHash(big.NewInt(2)) - key2 := common.BigToHash(big.NewInt(3)) - value2 := common.BigToHash(big.NewInt(4)) - - keeper := NewMockKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) - db.SetState(address, key1, value1) - db.SetState(address, key2, value2) - - // ForEachStorage only iterate committed state - suite.Require().Empty(CollectContractStorage(db)) - - suite.Require().NoError(db.Commit()) - - storage := CollectContractStorage(db) - suite.Require().Equal(2, len(storage)) - suite.Require().Equal(keeper.accounts[address].states, storage) - - // break early iteration - storage = make(statedb.Storage) - err := db.ForEachStorage(address, func(k, v common.Hash) bool { - storage[k] = v - // return false to break early - return false - }) - suite.Require().NoError(err) - suite.Require().Equal(1, len(storage)) -} - -func CollectContractStorage(db vm.StateDB) statedb.Storage { - storage := make(statedb.Storage) - err := db.ForEachStorage(address, func(k, v common.Hash) bool { - storage[k] = v - return true - }) - if err != nil { - return nil - } - - return storage -} - -func TestStateDBTestSuite(t *testing.T) { - suite.Run(t, &StateDBTestSuite{}) -} From 26b75fd3d507dc6781297e090ba9377ff4f3d0eb Mon Sep 17 00:00:00 2001 From: VictorTrustyDev Date: Sun, 22 Sep 2024 03:46:21 +0700 Subject: [PATCH 6/6] update CHANGELOG.md --- CHANGELOG.md | 1 + README.md | 49 ++++++++++++++++++------------------------------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c1484f89..83a22cca25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - (ante) [#164](https://github.com/EscanBE/evermint/pull/164) Introduce Dual-Lane AnteHandler - (ante) [#165](https://github.com/EscanBE/evermint/pull/165) Simulate EVM txs before accept mempool +- (evm) [#167](https://github.com/EscanBE/evermint/pull/167) Introduce Context-based StateDB ### Improvement diff --git a/README.md b/README.md index 97307c1152..d6782cf092 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ parent: +Requires [Go 1.22+](https://golang.org/dl/) + ### Create your own fully customized EVM-enabled blockchain network in just 2 steps [> Quick rename](https://github.com/EscanBE/evermint/blob/main/RENAME_CHAIN.md) @@ -24,38 +26,16 @@ parent: ### About Evermint -Evermint is a fork of open source Evmos v12.1.6, maintained by Escan team with bug fixes, customization and enable developers to fork and transform to their chain, fully customized, in just 2 steps. - -_Important Note: Evermint was born for development and research purpose so maintainers do not support migration for new upgrade/breaking changes._ - -### About Evmos - -Evmos is a scalable, high-throughput Proof-of-Stake blockchain -that is fully compatible and interoperable with Ethereum. -It's built using the [Cosmos SDK](https://github.com/cosmos/cosmos-sdk/) -which runs on top of the [CometBFT](https://github.com/cometbft/cometbft) consensus engine. - -### Different of Evermint & Evmos - -- Evermint is for research and development purpose. -- Evermint is fork of open source Evmos v12.1.6 plus bug fixes. -- Evermint is [currently removing some modules](https://github.com/EscanBE/evermint/issues/41) from Evmos codebase and only keep `x/evm`, `x/erc20`, `x/feemarket`. The goal is to make it more simple for research and only focus on the skeleton of Evmos. -- After [upgraded to Cosmos-SDK v0.50](https://github.com/EscanBE/evermint/pull/148), Evermint is now has many breaking change to the Legacy Evmos v12.1.6. If you want to use the open-source libraries previously developed for Evmos v12.1.6, you might need to use [Evermint version which uses Cosmos-SDK v0.47 and below](https://github.com/EscanBE/evermint/tree/v12.3.0-cosmos47). +Evermint originally a fork of open source Evmos v12.1.6 plus lots of magic. -## Documentation +Many important pieces of code was replaced by Evermint, such as: +- Replaced legacy AnteHandler with [Dual-Lane AnteHandler](https://github.com/EscanBE/evermint/pull/164). +- Replaced legacy StateDB with [Context-based StateDB](https://github.com/EscanBE/evermint/pull/167). +- Used go-ethereum code for [state-transition](https://github.com/EscanBE/evermint/pull/156). +- Some project structure was replaced during upgrade to Cosmos-SDK v0.50, CometBFT v0.38, ibc-go v8.5. +- Data structure, code logic of `MsgEthereumTx` and some proto msgs were replaced. -Evermint does not maintain its own documentation site, user can refer to Evmos documentation hosted at [evmos/docs](https://github.com/evmos/docs) and can be found at [docs.evmos.org](https://docs.evmos.org). -Head over there and check it out. - -**Note**: Requires [Go 1.22+](https://golang.org/dl/) - -## Quick Start - -To learn how the Evmos works from a high-level perspective, -go to the [Protocol Overview](https://docs.evmos.org/protocol) section from the documentation. -You can also check the instructions to [Run a Node](https://docs.evmos.org/protocol/evmos-cli#run-an-evmos-node). - -### Additional feature provided by Evermint: +#### Some other features provided by Evermint: 1. Command convert between 0x address and bech32 address, or any custom bech32 HRP ```bash evmd convert-address evm1sv9m0g7ycejwr3s369km58h5qe7xj77hxrsmsz evmos @@ -66,4 +46,11 @@ evmd convert-address evm1sv9m0g7ycejwr3s369km58h5qe7xj77hxrsmsz evmos 4. [`snapshots` command](https://github.com/EscanBE/evermint/pull/12) 5. [`inspect` command](https://github.com/EscanBE/evermint/pull/14) 6. [Flag `--allow-insecure-unlock`](https://github.com/EscanBE/evermint/pull/142) -7. Dependencies updated: `Cosmos-SDK v0.50.9`, `CometBFT v0.38.12`, `ibc-go v8.5.0`, `go-ethereum v1.10.26` \ No newline at end of file + +### About Evmos +_v12.1.6_ + +Evmos is a scalable, high-throughput Proof-of-Stake blockchain +that is fully compatible and interoperable with Ethereum. +It's built using the [Cosmos SDK](https://github.com/cosmos/cosmos-sdk/) +which runs on top of the [Tendermint](https://github.com/tendermint/tendermint) consensus engine.