diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index 246d03a0f..745824f00 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -14,6 +14,7 @@ import ( "github.com/tendermint/tendermint/libs/log" sdk "github.com/cosmos/cosmos-sdk/types" + banksim "github.com/cosmos/cosmos-sdk/x/bank/simulation" "github.com/cosmos/cosmos-sdk/x/mock/simulation" stake "github.com/cosmos/cosmos-sdk/x/stake" stakesim "github.com/cosmos/cosmos-sdk/x/stake/simulation" @@ -85,6 +86,7 @@ func TestFullGaiaSimulation(t *testing.T) { simulation.SimulateFromSeed( t, app.BaseApp, appStateFn, seed, []simulation.TestAndRunTx{ + banksim.TestAndRunSingleInputMsgSend(app.accountMapper), stakesim.SimulateMsgCreateValidator(app.accountMapper, app.stakeKeeper), stakesim.SimulateMsgEditValidator(app.stakeKeeper), stakesim.SimulateMsgDelegate(app.accountMapper, app.stakeKeeper), @@ -95,6 +97,7 @@ func TestFullGaiaSimulation(t *testing.T) { }, []simulation.RandSetup{}, []simulation.Invariant{ + banksim.NonnegativeBalanceInvariant(app.accountMapper), stakesim.AllInvariants(app.coinKeeper, app.stakeKeeper, app.accountMapper), }, NumKeys, diff --git a/x/bank/app_test.go b/x/bank/app_test.go index 74a421bd7..8c82cd475 100644 --- a/x/bank/app_test.go +++ b/x/bank/app_test.go @@ -83,21 +83,6 @@ func getMockApp(t *testing.T) *mock.App { return mapp } -func TestBankWithRandomMessages(t *testing.T) { - mapp := getMockApp(t) - setup := func(r *rand.Rand, keys []crypto.PrivKey) { - return - } - - mapp.RandomizedTesting( - t, - []mock.TestAndRunTx{TestAndRunSingleInputMsgSend}, - []mock.RandSetup{setup}, - []mock.Invariant{ModuleInvariants}, - 100, 30, 30, - ) -} - func TestMsgSendWithAccounts(t *testing.T) { mapp := getMockApp(t) diff --git a/x/bank/simulation/invariants.go b/x/bank/simulation/invariants.go new file mode 100644 index 000000000..847288e1f --- /dev/null +++ b/x/bank/simulation/invariants.go @@ -0,0 +1,50 @@ +package simulation + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + abci "github.com/tendermint/tendermint/abci/types" +) + +// NonnegativeBalanceInvariant checks that all accounts in the application have non-negative balances +func NonnegativeBalanceInvariant(mapper auth.AccountMapper) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + ctx := app.NewContext(false, abci.Header{}) + accts := mock.GetAllAccounts(mapper, ctx) + for _, acc := range accts { + coins := acc.GetCoins() + require.True(t, coins.IsNotNegative(), + fmt.Sprintf("%s has a negative denomination of %s\n%s", + acc.GetAddress().String(), + coins.String(), + log), + ) + } + } +} + +// TotalCoinsInvariant checks that the sum of the coins across all accounts +// is what is expected +func TotalCoinsInvariant(mapper auth.AccountMapper, totalSupplyFn func() sdk.Coins) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + ctx := app.NewContext(false, abci.Header{}) + totalCoins := sdk.Coins{} + + chkAccount := func(acc auth.Account) bool { + coins := acc.GetCoins() + totalCoins = totalCoins.Plus(coins) + return false + } + + mapper.IterateAccounts(ctx, chkAccount) + require.Equal(t, totalSupplyFn(), totalCoins, log) + } +} diff --git a/x/bank/simulation/msgs.go b/x/bank/simulation/msgs.go new file mode 100644 index 000000000..553d000db --- /dev/null +++ b/x/bank/simulation/msgs.go @@ -0,0 +1,117 @@ +package simulation + +import ( + "errors" + "fmt" + "math/big" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + "github.com/tendermint/tendermint/crypto" +) + +// TestAndRunSingleInputMsgSend tests and runs a single msg send, with one input and one output, where both +// accounts already exist. +func TestAndRunSingleInputMsgSend(mapper auth.AccountMapper) simulation.TestAndRunTx { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) { + fromKey := keys[r.Intn(len(keys))] + fromAddr := sdk.AccAddress(fromKey.PubKey().Address()) + toKey := keys[r.Intn(len(keys))] + // Disallow sending money to yourself + for { + if !fromKey.Equals(toKey) { + break + } + toKey = keys[r.Intn(len(keys))] + } + toAddr := sdk.AccAddress(toKey.PubKey().Address()) + initFromCoins := mapper.GetAccount(ctx, fromAddr).GetCoins() + + denomIndex := r.Intn(len(initFromCoins)) + amt, goErr := randPositiveInt(r, initFromCoins[denomIndex].Amount) + if goErr != nil { + return "skipping bank send due to account having no coins of denomination " + initFromCoins[denomIndex].Denom, nil + } + + action = fmt.Sprintf("%s is sending %s %s to %s", + fromAddr.String(), + amt.String(), + initFromCoins[denomIndex].Denom, + toAddr.String(), + ) + log = fmt.Sprintf("%s\n%s", log, action) + + coins := sdk.Coins{{initFromCoins[denomIndex].Denom, amt}} + var msg = bank.MsgSend{ + Inputs: []bank.Input{bank.NewInput(fromAddr, coins)}, + Outputs: []bank.Output{bank.NewOutput(toAddr, coins)}, + } + sendAndVerifyMsgSend(t, app, mapper, msg, ctx, log, []crypto.PrivKey{fromKey}) + event("bank/sendAndVerifyMsgSend/ok") + + return action, nil + } +} + +// Sends and verifies the transition of a msg send. This fails if there are repeated inputs or outputs +func sendAndVerifyMsgSend(t *testing.T, app *baseapp.BaseApp, mapper auth.AccountMapper, msg bank.MsgSend, ctx sdk.Context, log string, privkeys []crypto.PrivKey) { + initialInputAddrCoins := make([]sdk.Coins, len(msg.Inputs)) + initialOutputAddrCoins := make([]sdk.Coins, len(msg.Outputs)) + AccountNumbers := make([]int64, len(msg.Inputs)) + SequenceNumbers := make([]int64, len(msg.Inputs)) + + for i := 0; i < len(msg.Inputs); i++ { + acc := mapper.GetAccount(ctx, msg.Inputs[i].Address) + AccountNumbers[i] = acc.GetAccountNumber() + SequenceNumbers[i] = acc.GetSequence() + initialInputAddrCoins[i] = acc.GetCoins() + } + for i := 0; i < len(msg.Outputs); i++ { + acc := mapper.GetAccount(ctx, msg.Outputs[i].Address) + initialOutputAddrCoins[i] = acc.GetCoins() + } + tx := mock.GenTx([]sdk.Msg{msg}, + AccountNumbers, + SequenceNumbers, + privkeys...) + res := app.Deliver(tx) + if !res.IsOK() { + // TODO: Do this in a more 'canonical' way + fmt.Println(res) + fmt.Println(log) + t.FailNow() + } + + for i := 0; i < len(msg.Inputs); i++ { + terminalInputCoins := mapper.GetAccount(ctx, msg.Inputs[i].Address).GetCoins() + require.Equal(t, + initialInputAddrCoins[i].Minus(msg.Inputs[i].Coins), + terminalInputCoins, + fmt.Sprintf("Input #%d had an incorrect amount of coins\n%s", i, log), + ) + } + for i := 0; i < len(msg.Outputs); i++ { + terminalOutputCoins := mapper.GetAccount(ctx, msg.Outputs[i].Address).GetCoins() + require.Equal(t, + initialOutputAddrCoins[i].Plus(msg.Outputs[i].Coins), + terminalOutputCoins, + fmt.Sprintf("Output #%d had an incorrect amount of coins\n%s", i, log), + ) + } +} + +func randPositiveInt(r *rand.Rand, max sdk.Int) (sdk.Int, error) { + if !max.GT(sdk.OneInt()) { + return sdk.Int{}, errors.New("max too small") + } + max = max.Sub(sdk.OneInt()) + return sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.BigInt())).Add(sdk.OneInt()), nil +} diff --git a/x/bank/simulation/sim_test.go b/x/bank/simulation/sim_test.go new file mode 100644 index 000000000..5d76dd058 --- /dev/null +++ b/x/bank/simulation/sim_test.go @@ -0,0 +1,44 @@ +package simulation + +import ( + "encoding/json" + "math/rand" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" +) + +func TestBankWithRandomMessages(t *testing.T) { + mapp := mock.NewApp() + + bank.RegisterWire(mapp.Cdc) + mapper := mapp.AccountMapper + coinKeeper := bank.NewKeeper(mapper) + mapp.Router().AddRoute("bank", bank.NewHandler(coinKeeper)) + + err := mapp.CompleteSetup([]*sdk.KVStoreKey{}) + if err != nil { + panic(err) + } + + appStateFn := func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage { + mock.RandomSetGenesis(r, mapp, accs, []string{"stake"}) + return json.RawMessage("{}") + } + + simulation.Simulate( + t, mapp.BaseApp, appStateFn, + []simulation.TestAndRunTx{ + TestAndRunSingleInputMsgSend(mapper), + }, + []simulation.RandSetup{}, + []simulation.Invariant{ + NonnegativeBalanceInvariant(mapper), + TotalCoinsInvariant(mapper, func() sdk.Coins { return mapp.TotalCoinsSupply }), + }, + 100, 30, 30, + ) +} diff --git a/x/bank/test_helpers.go b/x/bank/test_helpers.go deleted file mode 100644 index 1dad0ba26..000000000 --- a/x/bank/test_helpers.go +++ /dev/null @@ -1,150 +0,0 @@ -package bank - -import ( - "errors" - "fmt" - "math/big" - "math/rand" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/cosmos/cosmos-sdk/x/mock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/crypto" -) - -// ModuleInvariants runs all invariants of the bank module. -// Currently runs non-negative balance invariant and TotalCoinsInvariant -func ModuleInvariants(t *testing.T, app *mock.App, log string) { - NonnegativeBalanceInvariant(t, app, log) - TotalCoinsInvariant(t, app, log) -} - -// NonnegativeBalanceInvariant checks that all accounts in the application have non-negative balances -func NonnegativeBalanceInvariant(t *testing.T, app *mock.App, log string) { - ctx := app.NewContext(false, abci.Header{}) - accts := mock.GetAllAccounts(app.AccountMapper, ctx) - for _, acc := range accts { - coins := acc.GetCoins() - assert.True(t, coins.IsNotNegative(), - fmt.Sprintf("%s has a negative denomination of %s\n%s", - acc.GetAddress().String(), - coins.String(), - log), - ) - } -} - -// TotalCoinsInvariant checks that the sum of the coins across all accounts -// is what is expected -func TotalCoinsInvariant(t *testing.T, app *mock.App, log string) { - ctx := app.BaseApp.NewContext(false, abci.Header{}) - totalCoins := sdk.Coins{} - - chkAccount := func(acc auth.Account) bool { - coins := acc.GetCoins() - totalCoins = totalCoins.Plus(coins) - return false - } - - app.AccountMapper.IterateAccounts(ctx, chkAccount) - require.Equal(t, app.TotalCoinsSupply, totalCoins, log) -} - -// TestAndRunSingleInputMsgSend tests and runs a single msg send, with one input and one output, where both -// accounts already exist. -func TestAndRunSingleInputMsgSend(t *testing.T, r *rand.Rand, app *mock.App, ctx sdk.Context, keys []crypto.PrivKey, log string) (action string, err sdk.Error) { - fromKey := keys[r.Intn(len(keys))] - fromAddr := sdk.AccAddress(fromKey.PubKey().Address()) - toKey := keys[r.Intn(len(keys))] - // Disallow sending money to yourself - for { - if !fromKey.Equals(toKey) { - break - } - toKey = keys[r.Intn(len(keys))] - } - toAddr := sdk.AccAddress(toKey.PubKey().Address()) - initFromCoins := app.AccountMapper.GetAccount(ctx, fromAddr).GetCoins() - - denomIndex := r.Intn(len(initFromCoins)) - amt, goErr := randPositiveInt(r, initFromCoins[denomIndex].Amount) - if goErr != nil { - return "skipping bank send due to account having no coins of denomination " + initFromCoins[denomIndex].Denom, nil - } - - action = fmt.Sprintf("%s is sending %s %s to %s", - fromAddr.String(), - amt.String(), - initFromCoins[denomIndex].Denom, - toAddr.String(), - ) - log = fmt.Sprintf("%s\n%s", log, action) - - coins := sdk.Coins{{initFromCoins[denomIndex].Denom, amt}} - var msg = MsgSend{ - Inputs: []Input{NewInput(fromAddr, coins)}, - Outputs: []Output{NewOutput(toAddr, coins)}, - } - sendAndVerifyMsgSend(t, app, msg, ctx, log, []crypto.PrivKey{fromKey}) - - return action, nil -} - -// Sends and verifies the transition of a msg send. This fails if there are repeated inputs or outputs -func sendAndVerifyMsgSend(t *testing.T, app *mock.App, msg MsgSend, ctx sdk.Context, log string, privkeys []crypto.PrivKey) { - initialInputAddrCoins := make([]sdk.Coins, len(msg.Inputs)) - initialOutputAddrCoins := make([]sdk.Coins, len(msg.Outputs)) - AccountNumbers := make([]int64, len(msg.Inputs)) - SequenceNumbers := make([]int64, len(msg.Inputs)) - - for i := 0; i < len(msg.Inputs); i++ { - acc := app.AccountMapper.GetAccount(ctx, msg.Inputs[i].Address) - AccountNumbers[i] = acc.GetAccountNumber() - SequenceNumbers[i] = acc.GetSequence() - initialInputAddrCoins[i] = acc.GetCoins() - } - for i := 0; i < len(msg.Outputs); i++ { - acc := app.AccountMapper.GetAccount(ctx, msg.Outputs[i].Address) - initialOutputAddrCoins[i] = acc.GetCoins() - } - tx := mock.GenTx([]sdk.Msg{msg}, - AccountNumbers, - SequenceNumbers, - privkeys...) - res := app.Deliver(tx) - if !res.IsOK() { - // TODO: Do this in a more 'canonical' way - fmt.Println(res) - fmt.Println(log) - t.FailNow() - } - - for i := 0; i < len(msg.Inputs); i++ { - terminalInputCoins := app.AccountMapper.GetAccount(ctx, msg.Inputs[i].Address).GetCoins() - require.Equal(t, - initialInputAddrCoins[i].Minus(msg.Inputs[i].Coins), - terminalInputCoins, - fmt.Sprintf("Input #%d had an incorrect amount of coins\n%s", i, log), - ) - } - for i := 0; i < len(msg.Outputs); i++ { - terminalOutputCoins := app.AccountMapper.GetAccount(ctx, msg.Outputs[i].Address).GetCoins() - require.Equal(t, - initialOutputAddrCoins[i].Plus(msg.Outputs[i].Coins), - terminalOutputCoins, - fmt.Sprintf("Output #%d had an incorrect amount of coins\n%s", i, log), - ) - } -} - -func randPositiveInt(r *rand.Rand, max sdk.Int) (sdk.Int, error) { - if !max.GT(sdk.OneInt()) { - return sdk.Int{}, errors.New("max too small") - } - max = max.Sub(sdk.OneInt()) - return sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.BigInt())).Add(sdk.OneInt()), nil -}