diff --git a/.circleci/config.yml b/.circleci/config.yml index 7a77d8d21..bb4e957b1 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/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a85e301ae..c5534cb18 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,7 @@ v If a checkbox is n/a - please still include it but + a little note why * [ ] Updated all relevant documentation (`docs/`) * [ ] Updated all relevant code comments * [ ] Wrote tests -* [ ] Updated `CHANGELOG.md` +* [ ] Added entries in `PENDING.md` * [ ] Updated `cmd/gaia` and `examples/` ___________________________________ For Admin Use: diff --git a/Gopkg.lock b/Gopkg.lock index cf5c97909..050569195 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -646,6 +646,7 @@ "github.com/tendermint/tendermint/rpc/lib/client", "github.com/tendermint/tendermint/rpc/lib/server", "github.com/tendermint/tendermint/types", + "github.com/tendermint/tendermint/version", "github.com/zondax/ledger-goclient", "golang.org/x/crypto/blowfish", "golang.org/x/crypto/ripemd160", diff --git a/Makefile b/Makefile index 4c01f68e4..19a744d9f 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}" @@ -92,6 +93,9 @@ update_tools: update_dev_tools: cd tools && $(MAKE) update_dev_tools +get_tools: + cd tools && $(MAKE) get_tools + get_dev_tools: cd tools && $(MAKE) get_dev_tools @@ -127,6 +131,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 @@ -209,7 +223,7 @@ remotenet-status: # unless there is a reason not to. # https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html .PHONY: build build_cosmos-sdk-cli build_examples install install_examples install_cosmos-sdk-cli install_debug dist \ -check_tools get_tools get_vendor_deps draw_deps test test_cli test_unit \ +check_tools check_dev_tools get_tools get_dev_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 update_tools update_dev_tools diff --git a/PENDING.md b/PENDING.md index 6cfa09755..91bf4a8ed 100644 --- a/PENDING.md +++ b/PENDING.md @@ -2,6 +2,7 @@ BREAKING CHANGES * [baseapp] Msgs are no longer run on CheckTx, removed `ctx.IsCheckTx()` +* [x/gov] CLI flag changed from `proposalID` to `proposal-id` * [x/stake] Fixed the period check for the inflation calculation * [baseapp] NewBaseApp constructor now takes sdk.TxDecoder as argument instead of wire.Codec * [x/auth] Default TxDecoder can be found in `x/auth` rather than baseapp @@ -20,6 +21,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. @@ -30,6 +35,9 @@ IMPROVEMENTS * [tools] Remove `rm -rf vendor/` from `make get_vendor_deps` * [x/stake] Add revoked to human-readable validator * [x/auth] Recover ErrorOutOfGas panic in order to set sdk.Result attributes correctly +* [x/stake] Add revoked to human-readable validator +* [x/gov] Votes on a proposal can now be queried +* [x/bank] Unit tests are now table-driven BUG FIXES * \#1666 Add intra-tx counter to the genesis validators diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 2de92575a..b9efbc143 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -611,6 +611,17 @@ func TestProposalsQuery(t *testing.T) { // Test query voted and deposited by addr1 proposals = getProposalsFilterVoterDepositer(t, port, addr, addr) require.Equal(t, proposalID2, (proposals[0]).GetProposalID()) + + // Test query votes on Proposal 2 + votes := getVotes(t, port, proposalID2) + require.Len(t, votes, 1) + require.Equal(t, addr, votes[0].Voter) + + // Test query votes on Proposal 3 + votes = getVotes(t, port, proposalID3) + require.Len(t, votes, 2) + require.True(t, addr.String() == votes[0].Voter.String() || addr.String() == votes[1].Voter.String()) + require.True(t, addr2.String() == votes[0].Voter.String() || addr2.String() == votes[1].Voter.String()) } //_____________________________________________________________________________ @@ -875,6 +886,15 @@ func getVote(t *testing.T, port string, proposalID int64, voterAddr sdk.AccAddre return vote } +func getVotes(t *testing.T, port string, proposalID int64) []gov.Vote { + res, body := Request(t, port, "GET", fmt.Sprintf("/gov/proposals/%d/votes", proposalID), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + var votes []gov.Vote + err := cdc.UnmarshalJSON([]byte(body), &votes) + require.Nil(t, err) + return votes +} + func getProposalsAll(t *testing.T, port string) []gov.Proposal { res, body := Request(t, port, "GET", "/gov/proposals", nil) require.Equal(t, http.StatusOK, res.StatusCode, body) diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go new file mode 100644 index 000000000..f0bea1e17 --- /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/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index beac34097..e6ec2543f 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -188,35 +188,40 @@ func TestGaiaCLISubmitProposal(t *testing.T) { fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) require.Equal(t, int64(45), fooAcc.GetCoins().AmountOf("steak").Int64()) - proposal1 := executeGetProposal(t, fmt.Sprintf("gaiacli gov query-proposal --proposalID=1 --output=json %v", flags)) + proposal1 := executeGetProposal(t, fmt.Sprintf("gaiacli gov query-proposal --proposal-id=1 --output=json %v", flags)) require.Equal(t, int64(1), proposal1.GetProposalID()) require.Equal(t, gov.StatusDepositPeriod, proposal1.GetStatus()) depositStr := fmt.Sprintf("gaiacli gov deposit %v", flags) depositStr += fmt.Sprintf(" --from=%s", "foo") depositStr += fmt.Sprintf(" --deposit=%s", "10steak") - depositStr += fmt.Sprintf(" --proposalID=%s", "1") + depositStr += fmt.Sprintf(" --proposal-id=%s", "1") executeWrite(t, depositStr, pass) tests.WaitForNextNBlocksTM(2, port) fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) require.Equal(t, int64(35), fooAcc.GetCoins().AmountOf("steak").Int64()) - proposal1 = executeGetProposal(t, fmt.Sprintf("gaiacli gov query-proposal --proposalID=1 --output=json %v", flags)) + proposal1 = executeGetProposal(t, fmt.Sprintf("gaiacli gov query-proposal --proposal-id=1 --output=json %v", flags)) require.Equal(t, int64(1), proposal1.GetProposalID()) require.Equal(t, gov.StatusVotingPeriod, proposal1.GetStatus()) voteStr := fmt.Sprintf("gaiacli gov vote %v", flags) voteStr += fmt.Sprintf(" --from=%s", "foo") - voteStr += fmt.Sprintf(" --proposalID=%s", "1") + voteStr += fmt.Sprintf(" --proposal-id=%s", "1") voteStr += fmt.Sprintf(" --option=%s", "Yes") executeWrite(t, voteStr, pass) tests.WaitForNextNBlocksTM(2, port) - vote := executeGetVote(t, fmt.Sprintf("gaiacli gov query-vote --proposalID=1 --voter=%s --output=json %v", fooAddr, flags)) + vote := executeGetVote(t, fmt.Sprintf("gaiacli gov query-vote --proposal-id=1 --voter=%s --output=json %v", fooAddr, flags)) require.Equal(t, int64(1), vote.ProposalID) require.Equal(t, gov.OptionYes, vote.Option) + + votes := executeGetVotes(t, fmt.Sprintf("gaiacli gov query-votes --proposal-id=1 --output=json %v", flags)) + require.Len(t, votes, 1) + require.Equal(t, int64(1), votes[0].ProposalID) + require.Equal(t, gov.OptionYes, votes[0].Option) } //___________________________________________________________________________________ @@ -321,3 +326,12 @@ func executeGetVote(t *testing.T, cmdStr string) gov.Vote { require.NoError(t, err, "out %v\n, err %v", out, err) return vote } + +func executeGetVotes(t *testing.T, cmdStr string) []gov.Vote { + out := tests.ExecuteT(t, cmdStr) + var votes []gov.Vote + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &votes) + require.NoError(t, err, "out %v\n, err %v", out, err) + return votes +} diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index 4ab5d02b9..7c66cb9ef 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -112,6 +112,7 @@ func main() { client.GetCommands( govcmd.GetCmdQueryProposal("gov", cdc), govcmd.GetCmdQueryVote("gov", cdc), + govcmd.GetCmdQueryVotes("gov", cdc), )...) govCmd.AddCommand( client.PostCommands( diff --git a/examples/democoin/mock/validator.go b/examples/democoin/mock/validator.go index 84d41d488..c3d01b170 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 66ad10f6e..a11f2ec70 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 eba645932..862614ca0 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 eb3f66082..e48577c0b 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 74a421bd7..8b6968eb9 100644 --- a/x/bank/app_test.go +++ b/x/bank/app_test.go @@ -3,41 +3,50 @@ package bank import ( "testing" - "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" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" ) -// test bank module in a mock application +type ( + expectedBalance struct { + addr sdk.AccAddress + coins sdk.Coins + } + + appTestCase struct { + expPass bool + msgs []sdk.Msg + accNums []int64 + accSeqs []int64 + privKeys []crypto.PrivKey + expectedBalances []expectedBalance + } +) + var ( - priv1 = crypto.GenPrivKeyEd25519() - addr1 = sdk.AccAddress(priv1.PubKey().Address()) - priv2 = crypto.GenPrivKeyEd25519() - addr2 = sdk.AccAddress(priv2.PubKey().Address()) - addr3 = sdk.AccAddress(crypto.GenPrivKeyEd25519().PubKey().Address()) - priv4 = crypto.GenPrivKeyEd25519() - addr4 = sdk.AccAddress(priv4.PubKey().Address()) + priv1 = crypto.GenPrivKeyEd25519() + addr1 = sdk.AccAddress(priv1.PubKey().Address()) + priv2 = crypto.GenPrivKeyEd25519() + addr2 = sdk.AccAddress(priv2.PubKey().Address()) + addr3 = sdk.AccAddress(crypto.GenPrivKeyEd25519().PubKey().Address()) + priv4 = crypto.GenPrivKeyEd25519() + addr4 = sdk.AccAddress(priv4.PubKey().Address()) + coins = sdk.Coins{sdk.NewCoin("foocoin", 10)} halfCoins = sdk.Coins{sdk.NewCoin("foocoin", 5)} manyCoins = sdk.Coins{sdk.NewCoin("foocoin", 1), sdk.NewCoin("barcoin", 1)} - - freeFee = auth.StdFee{ // no fees for a buncha gas - sdk.Coins{sdk.NewCoin("foocoin", 0)}, - 100000, - } + freeFee = auth.NewStdFee(100000, sdk.Coins{sdk.NewCoin("foocoin", 0)}...) sendMsg1 = MsgSend{ Inputs: []Input{NewInput(addr1, coins)}, Outputs: []Output{NewOutput(addr2, coins)}, } - sendMsg2 = MsgSend{ Inputs: []Input{NewInput(addr1, coins)}, Outputs: []Output{ @@ -45,7 +54,6 @@ var ( NewOutput(addr3, halfCoins), }, } - sendMsg3 = MsgSend{ Inputs: []Input{ NewInput(addr1, coins), @@ -56,7 +64,6 @@ var ( NewOutput(addr3, coins), }, } - sendMsg4 = MsgSend{ Inputs: []Input{ NewInput(addr2, coins), @@ -65,7 +72,6 @@ var ( NewOutput(addr1, coins), }, } - sendMsg5 = MsgSend{ Inputs: []Input{ NewInput(addr1, manyCoins), @@ -83,56 +89,57 @@ 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) - - // Add an account at genesis acc := &auth.BaseAccount{ Address: addr1, Coins: sdk.Coins{sdk.NewCoin("foocoin", 67)}, } - accs := []auth.Account{acc} - // Construct genesis state - mock.SetGenesis(mapp, accs) + mock.SetGenesis(mapp, []auth.Account{acc}) - // A checkTx context (true) ctxCheck := mapp.BaseApp.NewContext(true, abci.Header{}) + res1 := mapp.AccountMapper.GetAccount(ctxCheck, addr1) require.NotNil(t, res1) require.Equal(t, acc, res1.(*auth.BaseAccount)) - // Run a CheckDeliver - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{sendMsg1}, []int64{0}, []int64{0}, true, priv1) + testCases := []appTestCase{ + { + msgs: []sdk.Msg{sendMsg1}, + accNums: []int64{0}, + accSeqs: []int64{0}, + expPass: true, + privKeys: []crypto.PrivKey{priv1}, + expectedBalances: []expectedBalance{ + expectedBalance{addr1, sdk.Coins{sdk.NewCoin("foocoin", 57)}}, + expectedBalance{addr2, sdk.Coins{sdk.NewCoin("foocoin", 10)}}, + }, + }, + { + msgs: []sdk.Msg{sendMsg1, sendMsg2}, + accNums: []int64{0}, + accSeqs: []int64{0}, + expPass: false, + privKeys: []crypto.PrivKey{priv1}, + }, + } - // Check balances - mock.CheckBalance(t, mapp, addr1, sdk.Coins{sdk.NewCoin("foocoin", 57)}) - mock.CheckBalance(t, mapp, addr2, sdk.Coins{sdk.NewCoin("foocoin", 10)}) + for _, tc := range testCases { + mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expPass, tc.privKeys...) - // Delivering again should cause replay error - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{sendMsg1, sendMsg2}, []int64{0}, []int64{0}, false, priv1) + for _, eb := range tc.expectedBalances { + mock.CheckBalance(t, mapp, eb.addr, eb.coins) + } + } - // bumping the txnonce number without resigning should be an auth error + // bumping the tx nonce number without resigning should be an auth error mapp.BeginBlock(abci.RequestBeginBlock{}) + tx := mock.GenTx([]sdk.Msg{sendMsg1}, []int64{0}, []int64{0}, priv1) tx.Signatures[0].Sequence = 1 - res := mapp.Deliver(tx) + res := mapp.Deliver(tx) require.Equal(t, sdk.ToABCICode(sdk.CodespaceRoot, sdk.CodeUnauthorized), res.Code, res.Log) // resigning the tx with the bumped sequence should work @@ -146,22 +153,35 @@ func TestMsgSendMultipleOut(t *testing.T) { Address: addr1, Coins: sdk.Coins{sdk.NewCoin("foocoin", 42)}, } - acc2 := &auth.BaseAccount{ Address: addr2, Coins: sdk.Coins{sdk.NewCoin("foocoin", 42)}, } - accs := []auth.Account{acc1, acc2} - mock.SetGenesis(mapp, accs) + mock.SetGenesis(mapp, []auth.Account{acc1, acc2}) - // Simulate a Block - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{sendMsg2}, []int64{0}, []int64{0}, true, priv1) + testCases := []appTestCase{ + { + msgs: []sdk.Msg{sendMsg2}, + accNums: []int64{0}, + accSeqs: []int64{0}, + expPass: true, + privKeys: []crypto.PrivKey{priv1}, + expectedBalances: []expectedBalance{ + expectedBalance{addr1, sdk.Coins{sdk.NewCoin("foocoin", 32)}}, + expectedBalance{addr2, sdk.Coins{sdk.NewCoin("foocoin", 47)}}, + expectedBalance{addr3, sdk.Coins{sdk.NewCoin("foocoin", 5)}}, + }, + }, + } - // Check balances - mock.CheckBalance(t, mapp, addr1, sdk.Coins{sdk.NewCoin("foocoin", 32)}) - mock.CheckBalance(t, mapp, addr2, sdk.Coins{sdk.NewCoin("foocoin", 47)}) - mock.CheckBalance(t, mapp, addr3, sdk.Coins{sdk.NewCoin("foocoin", 5)}) + for _, tc := range testCases { + mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expPass, tc.privKeys...) + + for _, eb := range tc.expectedBalances { + mock.CheckBalance(t, mapp, eb.addr, eb.coins) + } + } } func TestSengMsgMultipleInOut(t *testing.T) { @@ -179,18 +199,32 @@ func TestSengMsgMultipleInOut(t *testing.T) { Address: addr4, Coins: sdk.Coins{sdk.NewCoin("foocoin", 42)}, } - accs := []auth.Account{acc1, acc2, acc4} - mock.SetGenesis(mapp, accs) + mock.SetGenesis(mapp, []auth.Account{acc1, acc2, acc4}) - // CheckDeliver - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{sendMsg3}, []int64{0, 2}, []int64{0, 0}, true, priv1, priv4) + testCases := []appTestCase{ + { + msgs: []sdk.Msg{sendMsg3}, + accNums: []int64{0, 2}, + accSeqs: []int64{0, 0}, + expPass: true, + privKeys: []crypto.PrivKey{priv1, priv4}, + expectedBalances: []expectedBalance{ + expectedBalance{addr1, sdk.Coins{sdk.NewCoin("foocoin", 32)}}, + expectedBalance{addr4, sdk.Coins{sdk.NewCoin("foocoin", 32)}}, + expectedBalance{addr2, sdk.Coins{sdk.NewCoin("foocoin", 52)}}, + expectedBalance{addr3, sdk.Coins{sdk.NewCoin("foocoin", 10)}}, + }, + }, + } - // Check balances - mock.CheckBalance(t, mapp, addr1, sdk.Coins{sdk.NewCoin("foocoin", 32)}) - mock.CheckBalance(t, mapp, addr4, sdk.Coins{sdk.NewCoin("foocoin", 32)}) - mock.CheckBalance(t, mapp, addr2, sdk.Coins{sdk.NewCoin("foocoin", 52)}) - mock.CheckBalance(t, mapp, addr3, sdk.Coins{sdk.NewCoin("foocoin", 10)}) + for _, tc := range testCases { + mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expPass, tc.privKeys...) + + for _, eb := range tc.expectedBalances { + mock.CheckBalance(t, mapp, eb.addr, eb.coins) + } + } } func TestMsgSendDependent(t *testing.T) { @@ -200,20 +234,38 @@ func TestMsgSendDependent(t *testing.T) { Address: addr1, Coins: sdk.Coins{sdk.NewCoin("foocoin", 42)}, } - accs := []auth.Account{acc1} - mock.SetGenesis(mapp, accs) + mock.SetGenesis(mapp, []auth.Account{acc1}) - // CheckDeliver - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{sendMsg1}, []int64{0}, []int64{0}, true, priv1) + testCases := []appTestCase{ + { + msgs: []sdk.Msg{sendMsg1}, + accNums: []int64{0}, + accSeqs: []int64{0}, + expPass: true, + privKeys: []crypto.PrivKey{priv1}, + expectedBalances: []expectedBalance{ + expectedBalance{addr1, sdk.Coins{sdk.NewCoin("foocoin", 32)}}, + expectedBalance{addr2, sdk.Coins{sdk.NewCoin("foocoin", 10)}}, + }, + }, + { + msgs: []sdk.Msg{sendMsg4}, + accNums: []int64{1}, + accSeqs: []int64{0}, + expPass: true, + privKeys: []crypto.PrivKey{priv2}, + expectedBalances: []expectedBalance{ + expectedBalance{addr1, sdk.Coins{sdk.NewCoin("foocoin", 42)}}, + }, + }, + } - // Check balances - mock.CheckBalance(t, mapp, addr1, sdk.Coins{sdk.NewCoin("foocoin", 32)}) - mock.CheckBalance(t, mapp, addr2, sdk.Coins{sdk.NewCoin("foocoin", 10)}) + for _, tc := range testCases { + mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expPass, tc.privKeys...) - // Simulate a Block - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{sendMsg4}, []int64{1}, []int64{0}, true, priv2) - - // Check balances - mock.CheckBalance(t, mapp, addr1, sdk.Coins{sdk.NewCoin("foocoin", 42)}) + for _, eb := range tc.expectedBalances { + mock.CheckBalance(t, mapp, eb.addr, eb.coins) + } + } } 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..3a7248875 --- /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 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 -} diff --git a/x/gov/client/cli/tx.go b/x/gov/client/cli/tx.go index c1bb62bc7..45b49a542 100644 --- a/x/gov/client/cli/tx.go +++ b/x/gov/client/cli/tx.go @@ -15,7 +15,7 @@ import ( ) const ( - flagProposalID = "proposalID" + flagProposalID = "proposal-id" flagTitle = "title" flagDescription = "description" flagProposalType = "type" @@ -239,3 +239,54 @@ func GetCmdQueryVote(storeName string, cdc *wire.Codec) *cobra.Command { return cmd } + +// Command to Get a Proposal Information +func GetCmdQueryVotes(storeName string, cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "query-votes", + Short: "query votes on a proposal", + RunE: func(cmd *cobra.Command, args []string) error { + proposalID := viper.GetInt64(flagProposalID) + + ctx := context.NewCoreContextFromViper() + + res, err := ctx.QueryStore(gov.KeyProposal(proposalID), storeName) + if len(res) == 0 || err != nil { + return errors.Errorf("proposalID [%d] does not exist", proposalID) + } + + var proposal gov.Proposal + cdc.MustUnmarshalBinary(res, &proposal) + + if proposal.GetStatus() != gov.StatusVotingPeriod { + fmt.Println("Proposal not in voting period.") + return nil + } + + res2, err := ctx.QuerySubspace(cdc, gov.KeyVotesSubspace(proposalID), storeName) + if err != nil { + return err + } + + var votes []gov.Vote + for i := 0; i < len(res2); i++ { + var vote gov.Vote + cdc.MustUnmarshalBinary(res2[i].Value, &vote) + votes = append(votes, vote) + } + + output, err := wire.MarshalJSONIndent(cdc, votes) + if err != nil { + return err + } + + fmt.Println(string(output)) + + return nil + }, + } + + cmd.Flags().String(flagProposalID, "", "proposalID of which proposal's votes are being queried") + + return cmd +} diff --git a/x/gov/client/rest/rest.go b/x/gov/client/rest/rest.go index ffaf42749..7efce9f0b 100644 --- a/x/gov/client/rest/rest.go +++ b/x/gov/client/rest/rest.go @@ -33,6 +33,8 @@ func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec) { r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/deposits/{%s}", RestProposalID, RestDepositer), queryDepositHandlerFn(cdc)).Methods("GET") r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/votes/{%s}", RestProposalID, RestVoter), queryVoteHandlerFn(cdc)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/votes", RestProposalID), queryVotesOnProposalHandlerFn(cdc)).Methods("GET") + r.HandleFunc("/gov/proposals", queryProposalsWithParameterFn(cdc)).Methods("GET") } @@ -335,6 +337,71 @@ func queryVoteHandlerFn(cdc *wire.Codec) http.HandlerFunc { } } +// nolint: gocyclo +// todo: Split this functionality into helper functions to remove the above +func queryVotesOnProposalHandlerFn(cdc *wire.Codec) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + strProposalID := vars[RestProposalID] + + if len(strProposalID) == 0 { + w.WriteHeader(http.StatusBadRequest) + err := errors.New("proposalId required but not specified") + w.Write([]byte(err.Error())) + return + } + + proposalID, err := strconv.ParseInt(strProposalID, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + err := errors.Errorf("proposalID [%s] is not positive", proposalID) + w.Write([]byte(err.Error())) + return + } + + ctx := context.NewCoreContextFromViper() + + res, err := ctx.QueryStore(gov.KeyProposal(proposalID), storeName) + if err != nil || len(res) == 0 { + err := errors.Errorf("proposalID [%d] does not exist", proposalID) + w.Write([]byte(err.Error())) + return + } + + var proposal gov.Proposal + cdc.MustUnmarshalBinary(res, &proposal) + + if proposal.GetStatus() != gov.StatusVotingPeriod { + err := errors.Errorf("proposal is not in Voting Period", proposalID) + w.Write([]byte(err.Error())) + return + } + + res2, err := ctx.QuerySubspace(cdc, gov.KeyVotesSubspace(proposalID), storeName) + if err != nil { + err = errors.New("ProposalID doesn't exist") + w.Write([]byte(err.Error())) + return + } + + var votes []gov.Vote + + for i := 0; i < len(res2); i++ { + var vote gov.Vote + cdc.MustUnmarshalBinary(res2[i].Value, &vote) + votes = append(votes, vote) + } + + output, err := wire.MarshalJSONIndent(cdc, votes) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + w.Write(output) + } +} + // nolint: gocyclo // todo: Split this functionality into helper functions to remove the above func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc { diff --git a/x/mock/app.go b/x/mock/app.go index 17d4d122b..a18383f2b 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 a37913065..000000000 --- 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 000000000..5f507b89c --- /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 50957e1c4..6e1d9f198 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 000000000..14227a1ae --- /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 c3179849d..c97f1c0c8 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 e7168109a..23b58108f 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 000000000..e4869693c --- /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 000000000..87324eed7 --- /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 000000000..391ca1996 --- /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 237340b89..2ef747bae 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 104eae3d3..1ab72119e 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 f177c123d..9c70d69da 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 8d97cbce7..5e0252015 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{})