diff --git a/.circleci/config.yml b/.circleci/config.yml index 7a77d8d217ef..bb4e957b18c0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,6 +85,23 @@ jobs: export PATH="$GOBIN:$PATH" make test_cli + test_sim: + <<: *defaults + parallelism: 1 + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-pkg-cache + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: Test simulation + command: | + export PATH="$GOBIN:$PATH" + export GAIA_SIMULATION_SEED=1531897442166404087 + make test_sim + test_cover: <<: *defaults parallelism: 4 @@ -144,6 +161,9 @@ workflows: - test_cli: requires: - setup_dependencies + - test_sim: + requires: + - setup_dependencies - test_cover: requires: - setup_dependencies diff --git a/Makefile b/Makefile index 4c01f68e40a7..6459f08e8974 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ PACKAGES=$(shell go list ./... | grep -v '/vendor/') -PACKAGES_NOCLITEST=$(shell go list ./... | grep -v '/vendor/' | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test) +PACKAGES_NOCLITEST=$(shell go list ./... | grep -v '/vendor/' | grep -v '/simulation' | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test) +PACKAGES_SIMTEST=$(shell go list ./... | grep -v '/vendor/' | grep '/simulation') COMMIT_HASH := $(shell git rev-parse --short HEAD) BUILD_TAGS = netgo ledger BUILD_FLAGS = -tags "${BUILD_TAGS}" -ldflags "-X github.com/cosmos/cosmos-sdk/version.GitCommit=${COMMIT_HASH}" @@ -127,6 +128,16 @@ test_unit: test_race: @go test -race $(PACKAGES_NOCLITEST) +test_sim: + @echo "Running individual module simulations." + @go test $(PACKAGES_SIMTEST) -v + @echo "Running full Gaia simulation. This may take several minutes." + @echo "Pass the flag 'SimulationSeed' to run with a constant seed." + @echo "Pass the flag 'SimulationNumKeys' to run with the specified number of keys." + @echo "Pass the flag 'SimulationNumBlocks' to run with the specified number of blocks." + @echo "Pass the flag 'SimulationBlockSize' to run with the specified block size (operations per block)." + @go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationBlockSize=200 -v + test_cover: @bash tests/test_cover.sh @@ -212,4 +223,4 @@ remotenet-status: check_tools get_tools get_vendor_deps draw_deps test test_cli test_unit \ test_cover test_lint benchmark devdoc_init devdoc devdoc_save devdoc_update \ build-linux build-docker-gaiadnode localnet-start localnet-stop remotenet-start \ -remotenet-stop remotenet-status format check-ledger +remotenet-stop remotenet-status format check-ledger test_sim diff --git a/PENDING.md b/PENDING.md index 16ca3adc9326..6f9c3a13cf90 100644 --- a/PENDING.md +++ b/PENDING.md @@ -6,6 +6,10 @@ BREAKING CHANGES FEATURES * [lcd] Can now query governance proposals by ProposalStatus +* [x/mock/simulation] Randomized simulation framework + * Modules specify invariants and operations, preferably in an x/[module]/simulation package + * Modules can test random combinations of their own operations + * Applications can integrate operations and invariants from modules together for an integrated simulation * [baseapp] Initialize validator set on ResponseInitChain * Added support for cosmos-sdk-cli tool under cosmos-sdk/cmd * This allows SDK users to init a new project repository with a single command. diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go new file mode 100644 index 000000000000..f0bea1e177fc --- /dev/null +++ b/cmd/gaia/app/sim_test.go @@ -0,0 +1,100 @@ +package app + +import ( + "encoding/json" + "flag" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + dbm "github.com/tendermint/tendermint/libs/db" + "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" +) + +var ( + seed int64 + numKeys int + numBlocks int + blockSize int + enabled bool +) + +func init() { + flag.Int64Var(&seed, "SimulationSeed", 42, "Simulation random seed") + flag.IntVar(&numKeys, "SimulationNumKeys", 10, "Number of keys (accounts)") + flag.IntVar(&numBlocks, "SimulationNumBlocks", 100, "Number of blocks") + flag.IntVar(&blockSize, "SimulationBlockSize", 100, "Operations per block") + flag.BoolVar(&enabled, "SimulationEnabled", false, "Enable the simulation") +} + +func appStateFn(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage { + var genesisAccounts []GenesisAccount + + // Randomly generate some genesis accounts + for _, addr := range accs { + coins := sdk.Coins{sdk.Coin{"steak", sdk.NewInt(100)}} + genesisAccounts = append(genesisAccounts, GenesisAccount{ + Address: addr, + Coins: coins, + }) + } + + // Default genesis state + stakeGenesis := stake.DefaultGenesisState() + stakeGenesis.Pool.LooseTokens = sdk.NewRat(1000) + genesis := GenesisState{ + Accounts: genesisAccounts, + StakeData: stakeGenesis, + } + + // Marshal genesis + appState, err := MakeCodec().MarshalJSON(genesis) + if err != nil { + panic(err) + } + + return appState +} + +func TestFullGaiaSimulation(t *testing.T) { + if !enabled { + t.Skip("Skipping Gaia simulation") + } + + // Setup Gaia application + logger := log.NewNopLogger() + db := dbm.NewMemDB() + app := NewGaiaApp(logger, db, nil) + require.Equal(t, "GaiaApp", app.Name()) + + // Run randomized simulation + 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), + stakesim.SimulateMsgBeginUnbonding(app.accountMapper, app.stakeKeeper), + stakesim.SimulateMsgCompleteUnbonding(app.stakeKeeper), + stakesim.SimulateMsgBeginRedelegate(app.accountMapper, app.stakeKeeper), + stakesim.SimulateMsgCompleteRedelegate(app.stakeKeeper), + }, + []simulation.RandSetup{}, + []simulation.Invariant{ + banksim.NonnegativeBalanceInvariant(app.accountMapper), + stakesim.AllInvariants(app.coinKeeper, app.stakeKeeper, app.accountMapper), + }, + numKeys, + numBlocks, + blockSize, + ) + +} diff --git a/examples/democoin/mock/validator.go b/examples/democoin/mock/validator.go index 84d41d4880f2..c3d01b170353 100644 --- a/examples/democoin/mock/validator.go +++ b/examples/democoin/mock/validator.go @@ -28,6 +28,11 @@ func (v Validator) GetPubKey() crypto.PubKey { return nil } +// Implements sdk.Validator +func (v Validator) GetTokens() sdk.Rat { + return sdk.ZeroRat() +} + // Implements sdk.Validator func (v Validator) GetPower() sdk.Rat { return v.Power diff --git a/tools/Makefile b/tools/Makefile index 66ad10f6e05e..a11f2ec70d76 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -126,10 +126,10 @@ else @echo "Installing unparam" go get -v $(UNPARAM) endif -ifdef GOYCLO_CHECK - @echo "goyclo is already installed. Run 'make update_tools' to update." +ifdef GOCYCLO_CHECK + @echo "gocyclo is already installed. Run 'make update_tools' to update." else - @echo "Installing goyclo" + @echo "Installing gocyclo" go get -v $(GOCYCLO) endif diff --git a/types/coin.go b/types/coin.go index eba645932146..862614ca0753 100644 --- a/types/coin.go +++ b/types/coin.go @@ -15,9 +15,13 @@ type Coin struct { } func NewCoin(denom string, amount int64) Coin { + return NewIntCoin(denom, NewInt(amount)) +} + +func NewIntCoin(denom string, amount Int) Coin { return Coin{ Denom: denom, - Amount: NewInt(amount), + Amount: amount, } } diff --git a/types/stake.go b/types/stake.go index eb3f660820f3..e48577c0bf16 100644 --- a/types/stake.go +++ b/types/stake.go @@ -43,6 +43,7 @@ type Validator interface { GetOwner() AccAddress // owner AccAddress to receive/return validators coins GetPubKey() crypto.PubKey // validation pubkey GetPower() Rat // validation power + GetTokens() Rat // validation tokens GetDelegatorShares() Rat // Total out standing delegator shares GetBondHeight() int64 // height in which the validator became active } diff --git a/x/bank/app_test.go b/x/bank/app_test.go index 74a421bd7edf..2deb5de38f24 100644 --- a/x/bank/app_test.go +++ b/x/bank/app_test.go @@ -5,8 +5,6 @@ import ( "github.com/stretchr/testify/require" - "math/rand" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/mock" @@ -83,21 +81,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 000000000000..847288e1f16e --- /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 000000000000..3a72488757ed --- /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 := simulation.RandomKey(r, keys) + fromAddr := sdk.AccAddress(fromKey.PubKey().Address()) + toKey := simulation.RandomKey(r, keys) + // Disallow sending money to yourself + for { + if !fromKey.Equals(toKey) { + break + } + toKey = simulation.RandomKey(r, 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 000000000000..5d76dd0589a6 --- /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 1dad0ba267b4..000000000000 --- 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 -} diff --git a/x/mock/app.go b/x/mock/app.go index 27f3b9d46e72..175222fa8874 100644 --- a/x/mock/app.go +++ b/x/mock/app.go @@ -202,8 +202,7 @@ func RandomSetGenesis(r *rand.Rand, app *App, addrs []sdk.AccAddress, denoms []s (&baseAcc).SetCoins(coins) accts[i] = &baseAcc } - - SetGenesis(app, accts) + app.GenesisAccounts = accts } // GetAllAccounts returns all accounts in the accountMapper. diff --git a/x/mock/random_simulate_blocks.go b/x/mock/random_simulate_blocks.go deleted file mode 100644 index a37913065d1e..000000000000 --- a/x/mock/random_simulate_blocks.go +++ /dev/null @@ -1,95 +0,0 @@ -package mock - -import ( - "fmt" - "math/big" - "math/rand" - "testing" - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" -) - -// RandomizedTesting tests application by sending random messages. -func (app *App) RandomizedTesting( - t *testing.T, ops []TestAndRunTx, setups []RandSetup, - invariants []Invariant, numKeys int, numBlocks int, blockSize int, -) { - time := time.Now().UnixNano() - app.RandomizedTestingFromSeed(t, time, ops, setups, invariants, numKeys, numBlocks, blockSize) -} - -// RandomizedTestingFromSeed tests an application by running the provided -// operations, testing the provided invariants, but using the provided seed. -func (app *App) RandomizedTestingFromSeed( - t *testing.T, seed int64, ops []TestAndRunTx, setups []RandSetup, - invariants []Invariant, numKeys int, numBlocks int, blockSize int, -) { - log := fmt.Sprintf("Starting SingleModuleTest with randomness created with seed %d", int(seed)) - keys, addrs := GeneratePrivKeyAddressPairs(numKeys) - r := rand.New(rand.NewSource(seed)) - - for i := 0; i < len(setups); i++ { - setups[i](r, keys) - } - - RandomSetGenesis(r, app, addrs, []string{"foocoin"}) - header := abci.Header{Height: 0} - - for i := 0; i < numBlocks; i++ { - app.BeginBlock(abci.RequestBeginBlock{}) - - // Make sure invariants hold at beginning of block and when nothing was - // done. - app.assertAllInvariants(t, invariants, log) - - ctx := app.NewContext(false, header) - - // TODO: Add modes to simulate "no load", "medium load", and - // "high load" blocks. - for j := 0; j < blockSize; j++ { - logUpdate, err := ops[r.Intn(len(ops))](t, r, app, ctx, keys, log) - log += "\n" + logUpdate - - require.Nil(t, err, log) - app.assertAllInvariants(t, invariants, log) - } - - app.EndBlock(abci.RequestEndBlock{}) - header.Height++ - } -} - -func (app *App) assertAllInvariants(t *testing.T, tests []Invariant, log string) { - for i := 0; i < len(tests); i++ { - tests[i](t, app, log) - } -} - -// BigInterval is a representation of the interval [lo, hi), where -// lo and hi are both of type sdk.Int -type BigInterval struct { - lo sdk.Int - hi sdk.Int -} - -// RandFromBigInterval chooses an interval uniformly from the provided list of -// BigIntervals, and then chooses an element from an interval uniformly at random. -func RandFromBigInterval(r *rand.Rand, intervals []BigInterval) sdk.Int { - if len(intervals) == 0 { - return sdk.ZeroInt() - } - - interval := intervals[r.Intn(len(intervals))] - - lo := interval.lo - hi := interval.hi - - diff := hi.Sub(lo) - result := sdk.NewIntFromBigInt(new(big.Int).Rand(r, diff.BigInt())) - result = result.Add(lo) - - return result -} diff --git a/x/mock/simulation/random_simulate_blocks.go b/x/mock/simulation/random_simulate_blocks.go new file mode 100644 index 000000000000..5f507b89c1fd --- /dev/null +++ b/x/mock/simulation/random_simulate_blocks.go @@ -0,0 +1,81 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" +) + +// Simulate tests application by sending random messages. +func Simulate( + t *testing.T, app *baseapp.BaseApp, appStateFn func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage, ops []TestAndRunTx, setups []RandSetup, + invariants []Invariant, numKeys int, numBlocks int, blockSize int, +) { + time := time.Now().UnixNano() + SimulateFromSeed(t, app, appStateFn, time, ops, setups, invariants, numKeys, numBlocks, blockSize) +} + +// SimulateFromSeed tests an application by running the provided +// operations, testing the provided invariants, but using the provided seed. +func SimulateFromSeed( + t *testing.T, app *baseapp.BaseApp, appStateFn func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage, seed int64, ops []TestAndRunTx, setups []RandSetup, + invariants []Invariant, numKeys int, numBlocks int, blockSize int, +) { + log := fmt.Sprintf("Starting SimulateFromSeed with randomness created with seed %d", int(seed)) + keys, addrs := mock.GeneratePrivKeyAddressPairs(numKeys) + r := rand.New(rand.NewSource(seed)) + + // Setup event stats + events := make(map[string]uint) + event := func(what string) { + events[what]++ + } + + app.InitChain(abci.RequestInitChain{AppStateBytes: appStateFn(r, addrs)}) + for i := 0; i < len(setups); i++ { + setups[i](r, keys) + } + app.Commit() + + header := abci.Header{Height: 0} + + for i := 0; i < numBlocks; i++ { + app.BeginBlock(abci.RequestBeginBlock{}) + + // Make sure invariants hold at beginning of block and when nothing was + // done. + AssertAllInvariants(t, app, invariants, log) + + ctx := app.NewContext(false, header) + + // TODO: Add modes to simulate "no load", "medium load", and + // "high load" blocks. + for j := 0; j < blockSize; j++ { + logUpdate, err := ops[r.Intn(len(ops))](t, r, app, ctx, keys, log, event) + log += "\n" + logUpdate + + require.Nil(t, err, log) + AssertAllInvariants(t, app, invariants, log) + } + + app.EndBlock(abci.RequestEndBlock{}) + header.Height++ + } + + DisplayEvents(events) +} + +// AssertAllInvariants asserts a list of provided invariants against application state +func AssertAllInvariants(t *testing.T, app *baseapp.BaseApp, tests []Invariant, log string) { + for i := 0; i < len(tests); i++ { + tests[i](t, app, log) + } +} diff --git a/x/mock/types.go b/x/mock/simulation/types.go similarity index 76% rename from x/mock/types.go rename to x/mock/simulation/types.go index 50957e1c4669..6e1d9f198ffb 100644 --- a/x/mock/types.go +++ b/x/mock/simulation/types.go @@ -1,9 +1,10 @@ -package mock +package simulation import ( "math/rand" "testing" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/tendermint/crypto" ) @@ -13,8 +14,8 @@ type ( // transition was as expected. It returns a descriptive message "action" // about what this fuzzed tx actually did, for ease of debugging. TestAndRunTx func( - t *testing.T, r *rand.Rand, app *App, ctx sdk.Context, - privKeys []crypto.PrivKey, log string, + t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + privKeys []crypto.PrivKey, log string, event func(string), ) (action string, err sdk.Error) // RandSetup performs the random setup the mock module needs. @@ -23,14 +24,14 @@ type ( // An Invariant is a function which tests a particular invariant. // If the invariant has been broken, the function should halt the // test and output the log. - Invariant func(t *testing.T, app *App, log string) + Invariant func(t *testing.T, app *baseapp.BaseApp, log string) ) // PeriodicInvariant returns an Invariant function closure that asserts // a given invariant if the mock application's last block modulo the given // period is congruent to the given offset. func PeriodicInvariant(invariant Invariant, period int, offset int) Invariant { - return func(t *testing.T, app *App, log string) { + return func(t *testing.T, app *baseapp.BaseApp, log string) { if int(app.LastBlockHeight())%period == offset { invariant(t, app, log) } diff --git a/x/mock/simulation/util.go b/x/mock/simulation/util.go new file mode 100644 index 000000000000..14227a1aef70 --- /dev/null +++ b/x/mock/simulation/util.go @@ -0,0 +1,56 @@ +package simulation + +import ( + "fmt" + "math/rand" + + crypto "github.com/tendermint/tendermint/crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// shamelessly copied from https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang#31832326 +// TODO we should probably move this to tendermint/libs/common/random.go + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = r.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + return string(b) +} + +// Pretty-print events as a table +func DisplayEvents(events map[string]uint) { + // TODO + fmt.Printf("Events: %v\n", events) +} + +// Pick a random key from an array +func RandomKey(r *rand.Rand, keys []crypto.PrivKey) crypto.PrivKey { + return keys[r.Intn( + len(keys), + )] +} + +// Generate a random amount +func RandomAmount(r *rand.Rand, max sdk.Int) sdk.Int { + return sdk.NewInt(int64(r.Intn(int(max.Int64())))) +} diff --git a/x/mock/test_utils.go b/x/mock/test_utils.go index c3179849d725..c97f1c0c85b6 100644 --- a/x/mock/test_utils.go +++ b/x/mock/test_utils.go @@ -1,6 +1,8 @@ package mock import ( + "math/big" + "math/rand" "testing" "github.com/cosmos/cosmos-sdk/baseapp" @@ -10,6 +12,32 @@ import ( "github.com/tendermint/tendermint/crypto" ) +// BigInterval is a representation of the interval [lo, hi), where +// lo and hi are both of type sdk.Int +type BigInterval struct { + lo sdk.Int + hi sdk.Int +} + +// RandFromBigInterval chooses an interval uniformly from the provided list of +// BigIntervals, and then chooses an element from an interval uniformly at random. +func RandFromBigInterval(r *rand.Rand, intervals []BigInterval) sdk.Int { + if len(intervals) == 0 { + return sdk.ZeroInt() + } + + interval := intervals[r.Intn(len(intervals))] + + lo := interval.lo + hi := interval.hi + + diff := hi.Sub(lo) + result := sdk.NewIntFromBigInt(new(big.Int).Rand(r, diff.BigInt())) + result = result.Add(lo) + + return result +} + // CheckBalance checks the balance of an account. func CheckBalance(t *testing.T, app *App, addr sdk.AccAddress, exp sdk.Coins) { ctxCheck := app.BaseApp.NewContext(true, abci.Header{}) diff --git a/x/stake/keeper/delegation.go b/x/stake/keeper/delegation.go index e7168109a66b..23b58108f85c 100644 --- a/x/stake/keeper/delegation.go +++ b/x/stake/keeper/delegation.go @@ -110,6 +110,22 @@ func (k Keeper) GetUnbondingDelegationsFromValidator(ctx sdk.Context, valAddr sd return ubds } +// iterate through all of the unbonding delegations +func (k Keeper) IterateUnbondingDelegations(ctx sdk.Context, fn func(index int64, ubd types.UnbondingDelegation) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, UnbondingDelegationKey) + i := int64(0) + for ; iterator.Valid(); iterator.Next() { + ubd := types.MustUnmarshalUBD(k.cdc, iterator.Key(), iterator.Value()) + stop := fn(i, ubd) + if stop { + break + } + i++ + } + iterator.Close() +} + // set the unbonding delegation and associated index func (k Keeper) SetUnbondingDelegation(ctx sdk.Context, ubd types.UnbondingDelegation) { store := ctx.KVStore(k.storeKey) @@ -298,6 +314,12 @@ func (k Keeper) unbond(ctx sdk.Context, delegatorAddr, validatorAddr sdk.AccAddr // complete unbonding an unbonding record func (k Keeper) BeginUnbonding(ctx sdk.Context, delegatorAddr, validatorAddr sdk.AccAddress, sharesAmount sdk.Rat) sdk.Error { + // TODO quick fix, instead we should use an index, see https://github.com/cosmos/cosmos-sdk/issues/1402 + _, found := k.GetUnbondingDelegation(ctx, delegatorAddr, validatorAddr) + if found { + return types.ErrExistingUnbondingDelegation(k.Codespace()) + } + returnAmount, err := k.unbond(ctx, delegatorAddr, validatorAddr, sharesAmount) if err != nil { return err diff --git a/x/stake/simulation/invariants.go b/x/stake/simulation/invariants.go new file mode 100644 index 000000000000..e4869693cfa0 --- /dev/null +++ b/x/stake/simulation/invariants.go @@ -0,0 +1,81 @@ +package simulation + +import ( + "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/simulation" + "github.com/cosmos/cosmos-sdk/x/stake" + abci "github.com/tendermint/tendermint/abci/types" +) + +// AllInvariants runs all invariants of the stake module. +// Currently: total supply, positive power +func AllInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + SupplyInvariants(ck, k, am)(t, app, log) + PositivePowerInvariant(k)(t, app, log) + ValidatorSetInvariant(k)(t, app, log) + } +} + +// SupplyInvariants checks that the total supply reflects all held loose tokens, bonded tokens, and unbonding delegations +func SupplyInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + ctx := app.NewContext(false, abci.Header{}) + pool := k.GetPool(ctx) + + loose := sdk.ZeroInt() + bonded := sdk.ZeroRat() + am.IterateAccounts(ctx, func(acc auth.Account) bool { + loose = loose.Add(acc.GetCoins().AmountOf("steak")) + return false + }) + k.IterateUnbondingDelegations(ctx, func(_ int64, ubd stake.UnbondingDelegation) bool { + loose = loose.Add(ubd.Balance.Amount) + return false + }) + k.IterateValidators(ctx, func(_ int64, validator sdk.Validator) bool { + switch validator.GetStatus() { + case sdk.Bonded: + bonded = bonded.Add(validator.GetPower()) + case sdk.Unbonding: + case sdk.Unbonded: + loose = loose.Add(validator.GetTokens().RoundInt()) + } + return false + }) + + // Loose tokens should equal coin supply plus unbonding delegations plus tokens on unbonded validators + require.True(t, pool.LooseTokens.RoundInt64() == loose.Int64(), "expected loose tokens to equal total steak held by accounts - pool.LooseTokens: %v, sum of account tokens: %v\nlog: %s", + pool.LooseTokens.RoundInt64(), loose.Int64(), log) + + // Bonded tokens should equal sum of tokens with bonded validators + require.True(t, pool.BondedTokens.Equal(bonded), "expected bonded tokens to equal total steak held by bonded validators\nlog: %s", log) + + // TODO Inflation check on total supply + } +} + +// PositivePowerInvariant checks that all stored validators have > 0 power +func PositivePowerInvariant(k stake.Keeper) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + ctx := app.NewContext(false, abci.Header{}) + k.IterateValidatorsBonded(ctx, func(_ int64, validator sdk.Validator) bool { + require.True(t, validator.GetPower().GT(sdk.ZeroRat()), "validator with non-positive power stored") + return false + }) + } +} + +// ValidatorSetInvariant checks equivalence of Tendermint validator set and SDK validator set +func ValidatorSetInvariant(k stake.Keeper) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + // TODO + } +} diff --git a/x/stake/simulation/msgs.go b/x/stake/simulation/msgs.go new file mode 100644 index 000000000000..87324eed720e --- /dev/null +++ b/x/stake/simulation/msgs.go @@ -0,0 +1,253 @@ +package simulation + +import ( + "fmt" + "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/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + "github.com/cosmos/cosmos-sdk/x/stake" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" +) + +// SimulateMsgCreateValidator +func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) 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) { + denom := k.GetParams(ctx).BondDenom + description := stake.Description{ + Moniker: simulation.RandStringOfLength(r, 10), + } + key := simulation.RandomKey(r, keys) + pubkey := key.PubKey() + address := sdk.AccAddress(pubkey.Address()) + amount := m.GetAccount(ctx, address).GetCoins().AmountOf(denom) + if amount.GT(sdk.ZeroInt()) { + amount = simulation.RandomAmount(r, amount) + } + if amount.Equal(sdk.ZeroInt()) { + return "no-operation", nil + } + msg := stake.MsgCreateValidator{ + Description: description, + ValidatorAddr: address, + DelegatorAddr: address, + PubKey: pubkey, + Delegation: sdk.NewIntCoin(denom, amount), + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgCreateValidator/%v", result.IsOK())) + // require.True(t, result.IsOK(), "expected OK result but instead got %v", result) + action = fmt.Sprintf("TestMsgCreateValidator: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgEditValidator +func SimulateMsgEditValidator(k stake.Keeper) 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) { + description := stake.Description{ + Moniker: simulation.RandStringOfLength(r, 10), + Identity: simulation.RandStringOfLength(r, 10), + Website: simulation.RandStringOfLength(r, 10), + Details: simulation.RandStringOfLength(r, 10), + } + key := simulation.RandomKey(r, keys) + pubkey := key.PubKey() + address := sdk.AccAddress(pubkey.Address()) + msg := stake.MsgEditValidator{ + Description: description, + ValidatorAddr: address, + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgEditValidator/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgEditValidator: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgDelegate +func SimulateMsgDelegate(m auth.AccountMapper, k stake.Keeper) 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) { + denom := k.GetParams(ctx).BondDenom + validatorKey := simulation.RandomKey(r, keys) + validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address()) + delegatorKey := simulation.RandomKey(r, keys) + delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address()) + amount := m.GetAccount(ctx, delegatorAddress).GetCoins().AmountOf(denom) + if amount.GT(sdk.ZeroInt()) { + amount = simulation.RandomAmount(r, amount) + } + if amount.Equal(sdk.ZeroInt()) { + return "no-operation", nil + } + msg := stake.MsgDelegate{ + DelegatorAddr: delegatorAddress, + ValidatorAddr: validatorAddress, + Delegation: sdk.NewIntCoin(denom, amount), + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgDelegate/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgDelegate: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgBeginUnbonding +func SimulateMsgBeginUnbonding(m auth.AccountMapper, k stake.Keeper) 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) { + denom := k.GetParams(ctx).BondDenom + validatorKey := simulation.RandomKey(r, keys) + validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address()) + delegatorKey := simulation.RandomKey(r, keys) + delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address()) + amount := m.GetAccount(ctx, delegatorAddress).GetCoins().AmountOf(denom) + if amount.GT(sdk.ZeroInt()) { + amount = simulation.RandomAmount(r, amount) + } + if amount.Equal(sdk.ZeroInt()) { + return "no-operation", nil + } + msg := stake.MsgBeginUnbonding{ + DelegatorAddr: delegatorAddress, + ValidatorAddr: validatorAddress, + SharesAmount: sdk.NewRatFromInt(amount), + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgBeginUnbonding/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgBeginUnbonding: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgCompleteUnbonding +func SimulateMsgCompleteUnbonding(k stake.Keeper) 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) { + validatorKey := simulation.RandomKey(r, keys) + validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address()) + delegatorKey := simulation.RandomKey(r, keys) + delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address()) + msg := stake.MsgCompleteUnbonding{ + DelegatorAddr: delegatorAddress, + ValidatorAddr: validatorAddress, + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgCompleteUnbonding/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgCompleteUnbonding: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgBeginRedelegate +func SimulateMsgBeginRedelegate(m auth.AccountMapper, k stake.Keeper) 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) { + denom := k.GetParams(ctx).BondDenom + sourceValidatorKey := simulation.RandomKey(r, keys) + sourceValidatorAddress := sdk.AccAddress(sourceValidatorKey.PubKey().Address()) + destValidatorKey := simulation.RandomKey(r, keys) + destValidatorAddress := sdk.AccAddress(destValidatorKey.PubKey().Address()) + delegatorKey := simulation.RandomKey(r, keys) + delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address()) + // TODO + amount := m.GetAccount(ctx, delegatorAddress).GetCoins().AmountOf(denom) + if amount.GT(sdk.ZeroInt()) { + amount = simulation.RandomAmount(r, amount) + } + if amount.Equal(sdk.ZeroInt()) { + return "no-operation", nil + } + msg := stake.MsgBeginRedelegate{ + DelegatorAddr: delegatorAddress, + ValidatorSrcAddr: sourceValidatorAddress, + ValidatorDstAddr: destValidatorAddress, + SharesAmount: sdk.NewRatFromInt(amount), + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgBeginRedelegate/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgBeginRedelegate: %s", msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgCompleteRedelegate +func SimulateMsgCompleteRedelegate(k stake.Keeper) 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) { + validatorSrcKey := simulation.RandomKey(r, keys) + validatorSrcAddress := sdk.AccAddress(validatorSrcKey.PubKey().Address()) + validatorDstKey := simulation.RandomKey(r, keys) + validatorDstAddress := sdk.AccAddress(validatorDstKey.PubKey().Address()) + delegatorKey := simulation.RandomKey(r, keys) + delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address()) + msg := stake.MsgCompleteRedelegate{ + DelegatorAddr: delegatorAddress, + ValidatorSrcAddr: validatorSrcAddress, + ValidatorDstAddr: validatorDstAddress, + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgCompleteRedelegate/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgCompleteRedelegate: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// Setup +func Setup(mapp *mock.App, k stake.Keeper) simulation.RandSetup { + return func(r *rand.Rand, privKeys []crypto.PrivKey) { + ctx := mapp.NewContext(false, abci.Header{}) + stake.InitGenesis(ctx, k, stake.DefaultGenesisState()) + params := k.GetParams(ctx) + denom := params.BondDenom + loose := sdk.ZeroInt() + mapp.AccountMapper.IterateAccounts(ctx, func(acc auth.Account) bool { + balance := simulation.RandomAmount(r, sdk.NewInt(1000000)) + acc.SetCoins(acc.GetCoins().Plus(sdk.Coins{sdk.NewIntCoin(denom, balance)})) + mapp.AccountMapper.SetAccount(ctx, acc) + loose = loose.Add(balance) + return false + }) + pool := k.GetPool(ctx) + pool.LooseTokens = pool.LooseTokens.Add(sdk.NewRat(loose.Int64(), 1)) + k.SetPool(ctx, pool) + } +} diff --git a/x/stake/simulation/sim_test.go b/x/stake/simulation/sim_test.go new file mode 100644 index 000000000000..391ca1996e7d --- /dev/null +++ b/x/stake/simulation/sim_test.go @@ -0,0 +1,59 @@ +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" + "github.com/cosmos/cosmos-sdk/x/stake" + abci "github.com/tendermint/tendermint/abci/types" +) + +// TestStakeWithRandomMessages +func TestStakeWithRandomMessages(t *testing.T) { + mapp := mock.NewApp() + + bank.RegisterWire(mapp.Cdc) + mapper := mapp.AccountMapper + coinKeeper := bank.NewKeeper(mapper) + stakeKey := sdk.NewKVStoreKey("stake") + stakeKeeper := stake.NewKeeper(mapp.Cdc, stakeKey, coinKeeper, stake.DefaultCodespace) + mapp.Router().AddRoute("stake", stake.NewHandler(stakeKeeper)) + mapp.SetEndBlocker(func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + validatorUpdates := stake.EndBlocker(ctx, stakeKeeper) + return abci.ResponseEndBlock{ + ValidatorUpdates: validatorUpdates, + } + }) + + err := mapp.CompleteSetup([]*sdk.KVStoreKey{stakeKey}) + 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{ + SimulateMsgCreateValidator(mapper, stakeKeeper), + SimulateMsgEditValidator(stakeKeeper), + SimulateMsgDelegate(mapper, stakeKeeper), + SimulateMsgBeginUnbonding(mapper, stakeKeeper), + SimulateMsgCompleteUnbonding(stakeKeeper), + SimulateMsgBeginRedelegate(mapper, stakeKeeper), + SimulateMsgCompleteRedelegate(stakeKeeper), + }, []simulation.RandSetup{ + Setup(mapp, stakeKeeper), + }, []simulation.Invariant{ + AllInvariants(coinKeeper, stakeKeeper, mapp.AccountMapper), + }, 10, 100, 100, + ) +} diff --git a/x/stake/types/errors.go b/x/stake/types/errors.go index 237340b89d5b..2ef747bae63e 100644 --- a/x/stake/types/errors.go +++ b/x/stake/types/errors.go @@ -120,6 +120,10 @@ func ErrNoUnbondingDelegation(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidDelegation, "no unbonding delegation found") } +func ErrExistingUnbondingDelegation(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidDelegation, "existing unbonding delegation found") +} + func ErrNoRedelegation(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidDelegation, "no redelegation found") } diff --git a/x/stake/types/test_utils.go b/x/stake/types/test_utils.go index 104eae3d315f..1ab72119e856 100644 --- a/x/stake/types/test_utils.go +++ b/x/stake/types/test_utils.go @@ -1,12 +1,7 @@ package types import ( - "fmt" - "math/rand" - "testing" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto" ) @@ -21,171 +16,3 @@ var ( emptyAddr sdk.AccAddress emptyPubkey crypto.PubKey ) - -// Operation reflects any operation that transforms staking state. It takes in -// a RNG instance, pool, validator and returns an updated pool, updated -// validator, delta tokens, and descriptive message. -type Operation func(r *rand.Rand, pool Pool, c Validator) (Pool, Validator, sdk.Rat, string) - -// OpBondOrUnbond implements an operation that bonds or unbonds a validator -// depending on current status. -// nolint: unparam -// TODO split up into multiple operations -func OpBondOrUnbond(r *rand.Rand, pool Pool, validator Validator) (Pool, Validator, sdk.Rat, string) { - var ( - msg string - newStatus sdk.BondStatus - ) - - if validator.Status == sdk.Bonded { - msg = fmt.Sprintf("sdk.Unbonded previously bonded validator %#v", validator) - newStatus = sdk.Unbonded - - } else if validator.Status == sdk.Unbonded { - msg = fmt.Sprintf("sdk.Bonded previously bonded validator %#v", validator) - newStatus = sdk.Bonded - } - - validator, pool = validator.UpdateStatus(pool, newStatus) - return pool, validator, sdk.ZeroRat(), msg -} - -// OpAddTokens implements an operation that adds a random number of tokens to a -// validator. -func OpAddTokens(r *rand.Rand, pool Pool, validator Validator) (Pool, Validator, sdk.Rat, string) { - msg := fmt.Sprintf("validator %#v", validator) - - tokens := int64(r.Int31n(1000)) - validator, pool, _ = validator.AddTokensFromDel(pool, tokens) - msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg) - - // Tokens are removed so for accounting must be negative - return pool, validator, sdk.NewRat(-1 * tokens), msg -} - -// OpRemoveShares implements an operation that removes a random number of -// delegatorshares from a validator. -func OpRemoveShares(r *rand.Rand, pool Pool, validator Validator) (Pool, Validator, sdk.Rat, string) { - var shares sdk.Rat - for { - shares = sdk.NewRat(int64(r.Int31n(1000))) - if shares.LT(validator.DelegatorShares) { - break - } - } - - msg := fmt.Sprintf("Removed %v shares from validator %#v", shares, validator) - - validator, pool, tokens := validator.RemoveDelShares(pool, shares) - return pool, validator, tokens, msg -} - -// RandomOperation returns a random staking operation. -func RandomOperation(r *rand.Rand) Operation { - operations := []Operation{ - OpBondOrUnbond, - OpAddTokens, - OpRemoveShares, - } - r.Shuffle(len(operations), func(i, j int) { - operations[i], operations[j] = operations[j], operations[i] - }) - - return operations[0] -} - -// AssertInvariants ensures invariants that should always be true are true. -// nolint: unparam -func AssertInvariants(t *testing.T, msg string, - pOrig Pool, cOrig []Validator, pMod Pool, vMods []Validator) { - - // total tokens conserved - require.True(t, - pOrig.LooseTokens.Add(pOrig.BondedTokens).Equal( - pMod.LooseTokens.Add(pMod.BondedTokens)), - "Tokens not conserved - msg: %v\n, pOrig.BondedTokens: %v, pOrig.LooseTokens: %v, pMod.BondedTokens: %v, pMod.LooseTokens: %v", - msg, - pOrig.BondedTokens, pOrig.LooseTokens, - pMod.BondedTokens, pMod.LooseTokens) - - // Nonnegative bonded tokens - require.False(t, pMod.BondedTokens.LT(sdk.ZeroRat()), - "Negative bonded shares - msg: %v\npOrig: %v\npMod: %v\n", - msg, pOrig, pMod) - - // Nonnegative loose tokens - require.False(t, pMod.LooseTokens.LT(sdk.ZeroRat()), - "Negative unbonded shares - msg: %v\npOrig: %v\npMod: %v\n", - msg, pOrig, pMod) - - for _, vMod := range vMods { - // Nonnegative ex rate - require.False(t, vMod.DelegatorShareExRate().LT(sdk.ZeroRat()), - "Applying operation \"%s\" resulted in negative validator.DelegatorShareExRate(): %v (validator.Owner: %s)", - msg, - vMod.DelegatorShareExRate(), - vMod.Owner, - ) - - // Nonnegative poolShares - require.False(t, vMod.BondedTokens().LT(sdk.ZeroRat()), - "Applying operation \"%s\" resulted in negative validator.BondedTokens(): %#v", - msg, - vMod, - ) - - // Nonnegative delShares - require.False(t, vMod.DelegatorShares.LT(sdk.ZeroRat()), - "Applying operation \"%s\" resulted in negative validator.DelegatorShares: %#v", - msg, - vMod, - ) - } -} - -// TODO: refactor this random setup - -// randomValidator generates a random validator. -// nolint: unparam -func randomValidator(r *rand.Rand, i int) Validator { - - tokens := sdk.NewRat(int64(r.Int31n(10000))) - delShares := sdk.NewRat(int64(r.Int31n(10000))) - - // TODO add more options here - status := sdk.Bonded - if r.Float64() > float64(0.5) { - status = sdk.Unbonded - } - - validator := NewValidator(addr1, pk1, Description{}) - validator.Status = status - validator.Tokens = tokens - validator.DelegatorShares = delShares - - return validator -} - -// RandomSetup generates a random staking state. -func RandomSetup(r *rand.Rand, numValidators int) (Pool, []Validator) { - pool := InitialPool() - pool.LooseTokens = sdk.NewRat(100000) - - validators := make([]Validator, numValidators) - for i := 0; i < numValidators; i++ { - validator := randomValidator(r, i) - - switch validator.Status { - case sdk.Bonded: - pool.BondedTokens = pool.BondedTokens.Add(validator.Tokens) - case sdk.Unbonded, sdk.Unbonding: - pool.LooseTokens = pool.LooseTokens.Add(validator.Tokens) - default: - panic("improper use of RandomSetup") - } - - validators[i] = validator - } - - return pool, validators -} diff --git a/x/stake/types/validator.go b/x/stake/types/validator.go index f177c123d07e..9c70d69da9ef 100644 --- a/x/stake/types/validator.go +++ b/x/stake/types/validator.go @@ -434,5 +434,6 @@ func (v Validator) GetStatus() sdk.BondStatus { return v.Status } func (v Validator) GetOwner() sdk.AccAddress { return v.Owner } func (v Validator) GetPubKey() crypto.PubKey { return v.PubKey } func (v Validator) GetPower() sdk.Rat { return v.BondedTokens() } +func (v Validator) GetTokens() sdk.Rat { return v.Tokens } func (v Validator) GetDelegatorShares() sdk.Rat { return v.DelegatorShares } func (v Validator) GetBondHeight() int64 { return v.BondHeight } diff --git a/x/stake/types/validator_test.go b/x/stake/types/validator_test.go index 8d97cbce749e..5e025201585e 100644 --- a/x/stake/types/validator_test.go +++ b/x/stake/types/validator_test.go @@ -2,7 +2,6 @@ package types import ( "fmt" - "math/rand" "testing" sdk "github.com/cosmos/cosmos-sdk/types" @@ -234,67 +233,6 @@ func TestPossibleOverflow(t *testing.T) { msg, newValidator.DelegatorShareExRate()) } -// run random operations in a random order on a random single-validator state, assert invariants hold -func TestSingleValidatorIntegrationInvariants(t *testing.T) { - r := rand.New(rand.NewSource(41)) - - for i := 0; i < 10; i++ { - poolOrig, validatorsOrig := RandomSetup(r, 1) - require.Equal(t, 1, len(validatorsOrig)) - - // sanity check - AssertInvariants(t, "no operation", - poolOrig, validatorsOrig, - poolOrig, validatorsOrig) - - for j := 0; j < 5; j++ { - poolMod, validatorMod, _, msg := RandomOperation(r)(r, poolOrig, validatorsOrig[0]) - - validatorsMod := make([]Validator, len(validatorsOrig)) - copy(validatorsMod[:], validatorsOrig[:]) - require.Equal(t, 1, len(validatorsOrig), "j %v", j) - require.Equal(t, 1, len(validatorsMod), "j %v", j) - validatorsMod[0] = validatorMod - - AssertInvariants(t, msg, - poolOrig, validatorsOrig, - poolMod, validatorsMod) - - poolOrig = poolMod - validatorsOrig = validatorsMod - } - } -} - -// run random operations in a random order on a random multi-validator state, assert invariants hold -func TestMultiValidatorIntegrationInvariants(t *testing.T) { - r := rand.New(rand.NewSource(42)) - - for i := 0; i < 10; i++ { - poolOrig, validatorsOrig := RandomSetup(r, 100) - - AssertInvariants(t, "no operation", - poolOrig, validatorsOrig, - poolOrig, validatorsOrig) - - for j := 0; j < 5; j++ { - index := int(r.Int31n(int32(len(validatorsOrig)))) - poolMod, validatorMod, _, msg := RandomOperation(r)(r, poolOrig, validatorsOrig[index]) - validatorsMod := make([]Validator, len(validatorsOrig)) - copy(validatorsMod[:], validatorsOrig[:]) - validatorsMod[index] = validatorMod - - AssertInvariants(t, msg, - poolOrig, validatorsOrig, - poolMod, validatorsMod) - - poolOrig = poolMod - validatorsOrig = validatorsMod - - } - } -} - func TestHumanReadableString(t *testing.T) { validator := NewValidator(addr1, pk1, Description{})