diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6aea04..fadc2f9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,13 +37,19 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Unreleased +### API Breaking + +* (evm) [\#661](https://github.com/cosmos/ethermint/pull/661) `Balance` field has been removed from the evm module's `GenesisState`. + ### Improvements +* (evm) [\#661](https://github.com/cosmos/ethermint/pull/661) Add invariant check for account balance and account nonce. * (deps) [\#654](https://github.com/cosmos/ethermint/pull/654) Bump go-ethereum version to [v1.9.25](https://github.com/ethereum/go-ethereum/releases/tag/v1.9.25) * (evm) [\#627](https://github.com/cosmos/ethermint/issues/627) Add extra EIPs parameter to apply custom EVM jump tables. ### Bug Fixes +* (evm) [\#661](https://github.com/cosmos/ethermint/pull/661) Set nonce to the EVM account on genesis initialization. * (evm) [\#621](https://github.com/cosmos/ethermint/issues/621) EVM `GenesisAccount` fields now share the same format as the auth module `Account`. * (evm) [\#618](https://github.com/cosmos/ethermint/issues/618) Add missing EVM `Context` `GetHash` field that retrieves a the header hash from a given block height. * (app) [\#617](https://github.com/cosmos/ethermint/issues/617) Fix genesis export functionality. diff --git a/app/ethermint.go b/app/ethermint.go index 65d71a04..4b793e38 100644 --- a/app/ethermint.go +++ b/app/ethermint.go @@ -280,7 +280,7 @@ func NewEthermintApp( app.mm.SetOrderInitGenesis( auth.ModuleName, distr.ModuleName, staking.ModuleName, bank.ModuleName, slashing.ModuleName, gov.ModuleName, mint.ModuleName, supply.ModuleName, - crisis.ModuleName, genutil.ModuleName, evidence.ModuleName, evm.ModuleName, + evm.ModuleName, crisis.ModuleName, genutil.ModuleName, evidence.ModuleName, faucet.ModuleName, ) diff --git a/x/evm/genesis.go b/x/evm/genesis.go index 1b21fa03..31d0b22c 100644 --- a/x/evm/genesis.go +++ b/x/evm/genesis.go @@ -4,6 +4,7 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" ethcmn "github.com/ethereum/go-ethereum/common" @@ -20,7 +21,6 @@ func InitGenesis(ctx sdk.Context, k Keeper, accountKeeper types.AccountKeeper, d for _, account := range data.Accounts { address := ethcmn.HexToAddress(account.Address) accAddress := sdk.AccAddress(address.Bytes()) - // check that the EVM balance the matches the account balance acc := accountKeeper.GetAccount(ctx, accAddress) if acc == nil { @@ -37,17 +37,11 @@ func InitGenesis(ctx sdk.Context, k Keeper, accountKeeper types.AccountKeeper, d } evmBalance := acc.GetCoins().AmountOf(evmDenom) - if !evmBalance.Equal(account.Balance) { - panic( - fmt.Errorf( - "balance mismatch for account %s, expected %s%s, got %s%s", - account.Address, evmBalance, evmDenom, account.Balance, evmDenom, - ), - ) - } - k.SetBalance(ctx, address, account.Balance.BigInt()) + k.SetNonce(ctx, address, acc.GetSequence()) + k.SetBalance(ctx, address, evmBalance.BigInt()) k.SetCode(ctx, address, account.Code) + for _, storage := range account.Storage { k.SetState(ctx, address, storage.Key, storage.Value) } @@ -83,12 +77,11 @@ func InitGenesis(ctx sdk.Context, k Keeper, accountKeeper types.AccountKeeper, d func ExportGenesis(ctx sdk.Context, k Keeper, ak types.AccountKeeper) GenesisState { // nolint: prealloc var ethGenAccounts []types.GenesisAccount - accounts := ak.GetAllAccounts(ctx) - - for _, account := range accounts { + ak.IterateAccounts(ctx, func(account authexported.Account) bool { ethAccount, ok := account.(*ethermint.EthAccount) if !ok { - continue + // ignore non EthAccounts + return false } addr := ethAccount.EthAddress() @@ -98,18 +91,15 @@ func ExportGenesis(ctx sdk.Context, k Keeper, ak types.AccountKeeper) GenesisSta panic(err) } - balanceInt := k.GetBalance(ctx, addr) - balance := sdk.NewIntFromBigInt(balanceInt) - genAccount := types.GenesisAccount{ Address: addr.String(), - Balance: balance, Code: k.GetCode(ctx, addr), Storage: storage, } ethGenAccounts = append(ethGenAccounts, genAccount) - } + return false + }) config, _ := k.GetChainConfig(ctx) diff --git a/x/evm/genesis_test.go b/x/evm/genesis_test.go index 37b39310..5cdd6f98 100644 --- a/x/evm/genesis_test.go +++ b/x/evm/genesis_test.go @@ -54,7 +54,6 @@ func (suite *EvmTestSuite) TestInitGenesis() { Accounts: []types.GenesisAccount{ { Address: address.String(), - Balance: sdk.OneInt(), Storage: types.Storage{ {Key: common.BytesToHash([]byte("key")), Value: common.BytesToHash([]byte("value"))}, }, @@ -87,25 +86,6 @@ func (suite *EvmTestSuite) TestInitGenesis() { Accounts: []types.GenesisAccount{ { Address: address.String(), - Balance: sdk.OneInt(), - }, - }, - }, - true, - }, - { - "balance mismatch", - func() { - acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, address.Bytes()) - suite.Require().NotNil(acc) - suite.app.AccountKeeper.SetAccount(suite.ctx, acc) - }, - types.GenesisState{ - Params: types.DefaultParams(), - Accounts: []types.GenesisAccount{ - { - Address: address.String(), - Balance: sdk.OneInt(), }, }, }, diff --git a/x/evm/keeper/invariants.go b/x/evm/keeper/invariants.go new file mode 100644 index 00000000..ef0ab16a --- /dev/null +++ b/x/evm/keeper/invariants.go @@ -0,0 +1,100 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + + ethermint "github.com/cosmos/ethermint/types" + "github.com/cosmos/ethermint/x/evm/types" +) + +const ( + balanceInvariant = "balance" + nonceInvariant = "nonce" +) + +// RegisterInvariants registers the evm module invariants +func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { + ir.RegisterRoute(types.ModuleName, balanceInvariant, k.BalanceInvariant()) + ir.RegisterRoute(types.ModuleName, nonceInvariant, k.NonceInvariant()) +} + +// BalanceInvariant checks that all auth module's EthAccounts in the application have the same balance +// as the EVM one. +func (k Keeper) BalanceInvariant() sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + var ( + msg string + count int + ) + + k.accountKeeper.IterateAccounts(ctx, func(account authexported.Account) bool { + ethAccount, ok := account.(*ethermint.EthAccount) + if !ok { + // ignore non EthAccounts + return false + } + + evmDenom := k.GetParams(ctx).EvmDenom + accountBalance := ethAccount.GetCoins().AmountOf(evmDenom) + evmBalance := k.GetBalance(ctx, ethAccount.EthAddress()) + + if evmBalance.Cmp(accountBalance.BigInt()) != 0 { + count++ + msg += fmt.Sprintf( + "\tbalance mismatch for address %s: account balance %s, evm balance %s\n", + account.GetAddress(), accountBalance.String(), evmBalance.String(), + ) + } + + return false + }) + + broken := count != 0 + + return sdk.FormatInvariant( + types.ModuleName, balanceInvariant, + fmt.Sprintf("account balances mismatches found %d\n%s", count, msg), + ), broken + } +} + +// NonceInvariant checks that all auth module's EthAccounts in the application have the same nonce +// sequence as the EVM. +func (k Keeper) NonceInvariant() sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + var ( + msg string + count int + ) + + k.accountKeeper.IterateAccounts(ctx, func(account authexported.Account) bool { + ethAccount, ok := account.(*ethermint.EthAccount) + if !ok { + // ignore non EthAccounts + return false + } + + evmNonce := k.GetNonce(ctx, ethAccount.EthAddress()) + + if evmNonce != ethAccount.Sequence { + count++ + msg += fmt.Sprintf( + "\nonce mismatch for address %s: account nonce %d, evm nonce %d\n", + account.GetAddress(), ethAccount.Sequence, evmNonce, + ) + } + + return false + }) + + broken := count != 0 + + return sdk.FormatInvariant( + types.ModuleName, nonceInvariant, + fmt.Sprintf("account nonces mismatches found %d\n%s", count, msg), + ), broken + } +} diff --git a/x/evm/keeper/invariants_test.go b/x/evm/keeper/invariants_test.go new file mode 100644 index 00000000..6c07e082 --- /dev/null +++ b/x/evm/keeper/invariants_test.go @@ -0,0 +1,139 @@ +package keeper_test + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/cosmos/ethermint/crypto/ethsecp256k1" + ethermint "github.com/cosmos/ethermint/types" + + ethcmn "github.com/ethereum/go-ethereum/common" +) + +func (suite *KeeperTestSuite) TestBalanceInvariant() { + privkey, err := ethsecp256k1.GenerateKey() + suite.Require().NoError(err) + + address := ethcmn.HexToAddress(privkey.PubKey().Address().String()) + + testCases := []struct { + name string + malleate func() + expBroken bool + }{ + { + "balance mismatch", + func() { + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, address.Bytes()) + suite.Require().NotNil(acc) + err := acc.SetCoins(sdk.NewCoins(ethermint.NewPhotonCoinInt64(1))) + suite.Require().NoError(err) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + + suite.app.EvmKeeper.SetBalance(suite.ctx, address, big.NewInt(1000)) + }, + true, + }, + { + "balance ok", + func() { + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, address.Bytes()) + suite.Require().NotNil(acc) + err := acc.SetCoins(sdk.NewCoins(ethermint.NewPhotonCoinInt64(1))) + suite.Require().NoError(err) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + + suite.app.EvmKeeper.SetBalance(suite.ctx, address, big.NewInt(1)) + }, + false, + }, + { + "invalid account type", + func() { + acc := authtypes.NewBaseAccountWithAddress(address.Bytes()) + suite.app.AccountKeeper.SetAccount(suite.ctx, &acc) + }, + false, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() // reset values + + tc.malleate() + + _, broken := suite.app.EvmKeeper.BalanceInvariant()(suite.ctx) + if tc.expBroken { + suite.Require().True(broken) + } else { + suite.Require().False(broken) + } + }) + } +} + +func (suite *KeeperTestSuite) TestNonceInvariant() { + privkey, err := ethsecp256k1.GenerateKey() + suite.Require().NoError(err) + + address := ethcmn.HexToAddress(privkey.PubKey().Address().String()) + + testCases := []struct { + name string + malleate func() + expBroken bool + }{ + { + "nonce mismatch", + func() { + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, address.Bytes()) + suite.Require().NotNil(acc) + err := acc.SetSequence(1) + suite.Require().NoError(err) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + + suite.app.EvmKeeper.SetNonce(suite.ctx, address, 100) + }, + true, + }, + { + "nonce ok", + func() { + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, address.Bytes()) + suite.Require().NotNil(acc) + err := acc.SetSequence(1) + suite.Require().NoError(err) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + + suite.app.EvmKeeper.SetNonce(suite.ctx, address, 1) + }, + false, + }, + { + "invalid account type", + func() { + acc := authtypes.NewBaseAccountWithAddress(address.Bytes()) + suite.app.AccountKeeper.SetAccount(suite.ctx, &acc) + }, + false, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() // reset values + + tc.malleate() + + _, broken := suite.app.EvmKeeper.NonceInvariant()(suite.ctx) + if tc.expBroken { + suite.Require().True(broken) + } else { + suite.Require().False(broken) + } + }) + } +} diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 8b42d96d..4a29fd4d 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -30,6 +30,8 @@ type Keeper struct { // - storing block height -> bloom filter map. Needed for the Web3 API. // - storing block hash -> block height map. Needed for the Web3 API. storeKey sdk.StoreKey + // Account Keeper for fetching accounts + accountKeeper types.AccountKeeper // Ethermint concrete implementation on the EVM StateDB interface CommitStateDB *types.CommitStateDB // Transaction counter in a block. Used on StateSB's Prepare function. @@ -52,6 +54,7 @@ func NewKeeper( return Keeper{ cdc: cdc, storeKey: storeKey, + accountKeeper: ak, CommitStateDB: types.NewCommitStateDB(sdk.Context{}, storeKey, paramSpace, ak), TxCount: 0, Bloom: big.NewInt(0), diff --git a/x/evm/keeper/querier.go b/x/evm/keeper/querier.go index 783d9828..ca749e09 100644 --- a/x/evm/keeper/querier.go +++ b/x/evm/keeper/querier.go @@ -197,12 +197,8 @@ func queryExportAccount(ctx sdk.Context, path []string, keeper Keeper) ([]byte, return nil, err } - balanceInt := keeper.GetBalance(ctx, addr) - balance := sdk.NewIntFromBigInt(balanceInt) - res := types.GenesisAccount{ Address: hexAddress, - Balance: balance, Code: keeper.GetCode(ctx, addr), Storage: storage, } diff --git a/x/evm/module.go b/x/evm/module.go index 1bef9494..34b88294 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -88,7 +88,9 @@ func (AppModule) Name() string { } // RegisterInvariants interface for registering invariants -func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {} +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { + keeper.RegisterInvariants(ir, am.keeper) +} // Route specifies path for transactions func (am AppModule) Route() string { diff --git a/x/evm/types/expected_keepers.go b/x/evm/types/expected_keepers.go index 8e0afc26..a398fc15 100644 --- a/x/evm/types/expected_keepers.go +++ b/x/evm/types/expected_keepers.go @@ -9,6 +9,7 @@ import ( type AccountKeeper interface { NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) authexported.Account GetAllAccounts(ctx sdk.Context) (accounts []authexported.Account) + IterateAccounts(ctx sdk.Context, cb func(account authexported.Account) bool) GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account SetAccount(ctx sdk.Context, account authexported.Account) RemoveAccount(ctx sdk.Context, account authexported.Account) diff --git a/x/evm/types/genesis.go b/x/evm/types/genesis.go index 8d944301..1871103b 100644 --- a/x/evm/types/genesis.go +++ b/x/evm/types/genesis.go @@ -4,8 +4,6 @@ import ( "errors" "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" - ethcmn "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ) @@ -22,9 +20,9 @@ type ( // GenesisAccount defines an account to be initialized in the genesis state. // Its main difference between with Geth's GenesisAccount is that it uses a custom // storage type and that it doesn't contain the private key field. + // NOTE: balance is omitted as it is imported from the auth account balance. GenesisAccount struct { Address string `json:"address"` - Balance sdk.Int `json:"balance"` Code hexutil.Bytes `json:"code,omitempty"` Storage Storage `json:"storage,omitempty"` } @@ -35,12 +33,6 @@ func (ga GenesisAccount) Validate() error { if ga.Address == (ethcmn.Address{}.String()) { return fmt.Errorf("address cannot be the zero address %s", ga.Address) } - if ga.Balance.IsNil() { - return errors.New("balance cannot be nil") - } - if ga.Balance.IsNegative() { - return errors.New("balance cannot be negative") - } if ga.Code != nil && len(ga.Code) == 0 { return errors.New("code bytes cannot be empty") } diff --git a/x/evm/types/genesis_test.go b/x/evm/types/genesis_test.go index 1358a67f..1413ae07 100644 --- a/x/evm/types/genesis_test.go +++ b/x/evm/types/genesis_test.go @@ -9,8 +9,6 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" ethcrypto "github.com/ethereum/go-ethereum/crypto" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/ethermint/crypto/ethsecp256k1" ) @@ -26,7 +24,6 @@ func TestValidateGenesisAccount(t *testing.T) { "valid genesis account", GenesisAccount{ Address: address.String(), - Balance: sdk.NewInt(1), Code: []byte{1, 2, 3}, Storage: Storage{ NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})), @@ -38,23 +35,6 @@ func TestValidateGenesisAccount(t *testing.T) { "empty account address bytes", GenesisAccount{ Address: ethcmn.Address{}.String(), - Balance: sdk.NewInt(1), - }, - false, - }, - { - "nil account balance", - GenesisAccount{ - Address: address.String(), - Balance: sdk.Int{}, - }, - false, - }, - { - "nil account balance", - GenesisAccount{ - Address: address.String(), - Balance: sdk.NewInt(-1), }, false, }, @@ -62,7 +42,6 @@ func TestValidateGenesisAccount(t *testing.T) { "empty code bytes", GenesisAccount{ Address: address.String(), - Balance: sdk.NewInt(1), Code: []byte{}, }, false, @@ -101,7 +80,6 @@ func TestValidateGenesis(t *testing.T) { Accounts: []GenesisAccount{ { Address: address.String(), - Balance: sdk.NewInt(1), Code: []byte{1, 2, 3}, Storage: Storage{ {Key: ethcmn.BytesToHash([]byte{1, 2, 3})}, @@ -153,7 +131,6 @@ func TestValidateGenesis(t *testing.T) { Accounts: []GenesisAccount{ { Address: address.String(), - Balance: sdk.NewInt(1), Code: []byte{1, 2, 3}, Storage: Storage{ NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})), @@ -161,7 +138,6 @@ func TestValidateGenesis(t *testing.T) { }, { Address: address.String(), - Balance: sdk.NewInt(1), Code: []byte{1, 2, 3}, Storage: Storage{ NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})), @@ -177,7 +153,6 @@ func TestValidateGenesis(t *testing.T) { Accounts: []GenesisAccount{ { Address: address.String(), - Balance: sdk.NewInt(1), Code: []byte{1, 2, 3}, Storage: Storage{ {Key: ethcmn.BytesToHash([]byte{1, 2, 3})}, @@ -227,7 +202,6 @@ func TestValidateGenesis(t *testing.T) { Accounts: []GenesisAccount{ { Address: address.String(), - Balance: sdk.NewInt(1), Code: []byte{1, 2, 3}, Storage: Storage{ {Key: ethcmn.BytesToHash([]byte{1, 2, 3})},