evm: balance and nonce invariants (#661)

* evm: balance and nonce invariants

* nonce invariant

* changelog

* use iterator on export
This commit is contained in:
Federico Kunze 2020-12-15 15:43:06 -03:00 committed by GitHub
parent 6001baed80
commit a1386eec09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 263 additions and 80 deletions

View File

@ -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.

View File

@ -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,
)

View File

@ -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)

View File

@ -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(),
},
},
},

100
x/evm/keeper/invariants.go Normal file
View File

@ -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
}
}

View File

@ -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)
}
})
}
}

View File

@ -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),

View File

@ -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,
}

View File

@ -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 {

View File

@ -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)

View File

@ -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")
}

View File

@ -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})},