diff --git a/.circleci/config.yml b/.circleci/config.yml index bf7892a8c..b778653ea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,7 +85,7 @@ jobs: export PATH="$GOBIN:$PATH" make test_cli - test_sim: + test_sim_modules: <<: *defaults parallelism: 1 steps: @@ -96,11 +96,26 @@ jobs: - restore_cache: key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} - run: - name: Test simulation + name: Test individual module simulations command: | export PATH="$GOBIN:$PATH" - export GAIA_SIMULATION_SEED=1531897442166404087 - make test_sim + make test_sim_modules + + test_sim_gaia_fast: + <<: *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 full Gaia simulation + command: | + export PATH="$GOBIN:$PATH" + make test_sim_gaia_fast test_cover: <<: *defaults @@ -118,7 +133,7 @@ jobs: command: | export PATH="$GOBIN:$PATH" make install - for pkg in $(go list github.com/cosmos/cosmos-sdk/... | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test | circleci tests split --split-by=timings); do + for pkg in $(go list github.com/cosmos/cosmos-sdk/... | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test | grep -v '/simulation' | circleci tests split --split-by=timings); do id=$(basename "$pkg") GOCACHE=off go test -timeout 8m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic "$pkg" | tee "/tmp/logs/$id-$RANDOM.log" done @@ -161,7 +176,10 @@ workflows: - test_cli: requires: - setup_dependencies - - test_sim: + - test_sim_modules: + requires: + - setup_dependencies + - test_sim_gaia_fast: requires: - setup_dependencies - test_cover: diff --git a/CHANGELOG.md b/CHANGELOG.md index 930de1332..b4de50494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,9 @@ FEATURES - 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 + - Simulates Tendermint's algorithm for validator set updates + - Simulates validator signing/downtime with a Markov chain, and occaisional double-signatures + - Includes simulated operations & invariants for staking, slashing, governance, and bank modules - [store] \#1481 Add transient store - [baseapp] Initialize validator set on ResponseInitChain - [baseapp] added BaseApp.Seal - ability to seal baseapp parameters once they've been set diff --git a/Gopkg.lock b/Gopkg.lock index cc42ac4f4..c3a540858 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -34,7 +34,7 @@ [[projects]] branch = "master" - digest = "1:6aabc1566d6351115d561d038da82a4c19b46c3b6e17f4a0a2fa60260663dc79" + digest = "1:2c00f064ba355903866cbfbf3f7f4c0fe64af6638cc7d1b8bdcf3181bc67f1d8" name = "github.com/btcsuite/btcd" packages = ["btcec"] pruneopts = "UT" @@ -71,7 +71,7 @@ version = "v1.4.7" [[projects]] - digest = "1:fa30c0652956e159cdb97dcb2ef8b8db63ed668c02a5c3a40961c8f0641252fe" + digest = "1:fdf5169073fb0ad6dc12a70c249145e30f4058647bea25f0abd48b6d9f228a11" name = "github.com/go-kit/kit" packages = [ "log", @@ -103,7 +103,7 @@ version = "v1.7.0" [[projects]] - digest = "1:212285efb97b9ec2e20550d81f0446cb7897e57cbdfd7301b1363ab113d8be45" + digest = "1:35621fe20f140f05a0c4ef662c26c0ab4ee50bca78aa30fe87d33120bd28165e" name = "github.com/gogo/protobuf" packages = [ "gogoproto", @@ -118,7 +118,7 @@ version = "v1.1.1" [[projects]] - digest = "1:cb22af0ed7c72d495d8be1106233ee553898950f15fd3f5404406d44c2e86888" + digest = "1:17fe264ee908afc795734e8c4e63db2accabaf57326dbf21763a7d6b86096260" name = "github.com/golang/protobuf" packages = [ "proto", @@ -165,12 +165,13 @@ [[projects]] branch = "master" - digest = "1:8951fe6e358876736d8fa1f3992624fdbb2dec6bc49401c1381d1ef8abbb544f" + digest = "1:a361611b8c8c75a1091f00027767f7779b29cb37c456a71b8f2604c88057ab40" name = "github.com/hashicorp/hcl" packages = [ ".", "hcl/ast", "hcl/parser", + "hcl/printer", "hcl/scanner", "hcl/strconv", "hcl/token", @@ -262,7 +263,7 @@ version = "v1.0.0" [[projects]] - digest = "1:98225904b7abff96c052b669b25788f18225a36673fba022fb93514bb9a2a64e" + digest = "1:c1a04665f9613e082e1209cf288bf64f4068dcd6c87a64bf1c4ff006ad422ba0" name = "github.com/prometheus/client_golang" packages = [ "prometheus", @@ -273,7 +274,7 @@ [[projects]] branch = "master" - digest = "1:0f37e09b3e92aaeda5991581311f8dbf38944b36a3edec61cc2d1991f527554a" + digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4" name = "github.com/prometheus/client_model" packages = ["go"] pruneopts = "UT" @@ -281,7 +282,7 @@ [[projects]] branch = "master" - digest = "1:dad2e5a2153ee7a6c9ab8fc13673a16ee4fb64434a7da980965a3741b0c981a3" + digest = "1:63b68062b8968092eb86bedc4e68894bd096ea6b24920faca8b9dcf451f54bb5" name = "github.com/prometheus/common" packages = [ "expfmt", @@ -293,7 +294,7 @@ [[projects]] branch = "master" - digest = "1:a37c98f4b7a66bb5c539c0539f0915a74ef1c8e0b3b6f45735289d94cae92bfd" + digest = "1:8c49953a1414305f2ff5465147ee576dd705487c35b15918fcd4efdc0cb7a290" name = "github.com/prometheus/procfs" packages = [ ".", @@ -312,7 +313,7 @@ revision = "e2704e165165ec55d062f5919b4b29494e9fa790" [[projects]] - digest = "1:37ace7f35375adec11634126944bdc45a673415e2fcc07382d03b75ec76ea94c" + digest = "1:bd1ae00087d17c5a748660b8e89e1043e1e5479d0fea743352cda2f8dd8c4f84" name = "github.com/spf13/afero" packages = [ ".", @@ -331,7 +332,7 @@ version = "v1.2.0" [[projects]] - digest = "1:627ab2f549a6a55c44f46fa24a4307f4d0da81bfc7934ed0473bf38b24051d26" + digest = "1:7ffc0983035bc7e297da3688d9fe19d60a420e9c38bef23f845c53788ed6a05e" name = "github.com/spf13/cobra" packages = ["."] pruneopts = "UT" @@ -340,11 +341,11 @@ [[projects]] branch = "master" - digest = "1:080e5f630945ad754f4b920e60b4d3095ba0237ebf88dc462eb28002932e3805" + digest = "1:8a020f916b23ff574845789daee6818daf8d25a4852419aae3f0b12378ba432a" name = "github.com/spf13/jwalterweatherman" packages = ["."] pruneopts = "UT" - revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" + revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2" [[projects]] digest = "1:dab83a1bbc7ad3d7a6ba1a1cc1760f25ac38cdf7d96a5cdd55cd915a4f5ceaf9" @@ -363,7 +364,7 @@ version = "v1.0.0" [[projects]] - digest = "1:73697231b93fb74a73ebd8384b68b9a60c57ea6b13c56d2425414566a72c8e6d" + digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6" name = "github.com/stretchr/testify" packages = [ "assert", @@ -375,7 +376,7 @@ [[projects]] branch = "master" - digest = "1:922191411ad8f61bcd8018ac127589bb489712c1d1a0ab2497aca4b16de417d2" + digest = "1:f2ffd421680b0a3f7887501b3c6974bcf19217ecd301d0e2c9b681940ec363d5" name = "github.com/syndtr/goleveldb" packages = [ "leveldb", @@ -392,11 +393,11 @@ "leveldb/util", ] pruneopts = "UT" - revision = "c4c61651e9e37fa117f53c5a906d3b63090d8445" + revision = "ae2bd5eed72d46b28834ec3f60db3a3ebedd8dbd" [[projects]] branch = "master" - digest = "1:203b409c21115233a576f99e8f13d8e07ad82b25500491f7e1cca12588fb3232" + digest = "1:087aaa7920e5d0bf79586feb57ce01c35c830396ab4392798112e8aae8c47722" name = "github.com/tendermint/ed25519" packages = [ ".", @@ -423,7 +424,7 @@ version = "v0.9.2" [[projects]] - digest = "1:df232b6f3c44554161093af004100f75e564b398ad3ff63ecbc297fe400dcfdb" + digest = "1:26146cdb2811ce481e72138439b9b1aa17a64d54364f96bb92f97a9ef8ba4f01" name = "github.com/tendermint/tendermint" packages = [ "abci/client", @@ -498,7 +499,7 @@ [[projects]] branch = "master" - digest = "1:3c3c47c1f7587c380afcc1d76385a4a03a2917b9ccc1ac50864d8f87e0264ada" + digest = "1:7a71fffde456d746c52f9cd09c50b034533a3180fb1f6320abb149f2ccc579e5" name = "golang.org/x/crypto" packages = [ "blowfish", @@ -520,7 +521,7 @@ revision = "de0752318171da717af4ce24d0a2e8626afaeb11" [[projects]] - digest = "1:04dda8391c3e2397daf254ac68003f30141c069b228d06baec8324a5f81dc1e9" + digest = "1:d36f55a999540d29b6ea3c2ea29d71c76b1d9853fdcd3e5c5cb4836f2ba118f1" name = "golang.org/x/net" packages = [ "context", @@ -537,17 +538,17 @@ [[projects]] branch = "master" - digest = "1:a97b28c54844d6b9848a840ae83d4d263292e831e8c2a586116fcab5c7cfe5f2" + digest = "1:a989b95f72fce8876213e8e20492525b4cf69a9e7fee7f1d9897983ee0d547e9" name = "golang.org/x/sys" packages = [ "cpu", "unix", ] pruneopts = "UT" - revision = "98c5dad5d1a0e8a73845ecc8897d0bd56586511d" + revision = "1c9583448a9c3aa0f9a6a5241bf73c0bd8aafded" [[projects]] - digest = "1:7509ba4347d1f8de6ae9be8818b0cd1abc3deeffe28aeaf4be6d4b6b5178d9ca" + digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" name = "golang.org/x/text" packages = [ "collate", @@ -575,10 +576,10 @@ name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] pruneopts = "UT" - revision = "383e8b2c3b9e36c4076b235b32537292176bae20" + revision = "d0a8f471bba2dbb160885b0000d814ee5d559bad" [[projects]] - digest = "1:4515e3030c440845b046354fd5d57671238428b820deebce2e9dabb5cd3c51ac" + digest = "1:2dab32a43451e320e49608ff4542fdfc653c95dcc35d0065ec9c6c3dd540ed74" name = "google.golang.org/grpc" packages = [ ".", diff --git a/Makefile b/Makefile index 208b59ade..d06d4eb4c 100644 --- a/Makefile +++ b/Makefile @@ -130,15 +130,17 @@ test_unit: test_race: @go test -race $(PACKAGES_NOSIMULATION) -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_sim_modules: + @echo "Running individual module simulations..." + @go test $(PACKAGES_SIMTEST) + +test_sim_gaia_fast: + @echo "Running full Gaia simulation. This may take several minutes..." + @go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationNumBlocks=1000 -v -timeout 24h + +test_sim_gaia_slow: + @echo "Running full Gaia simulation. This may take several minutes..." + @go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationNumBlocks=1000 -SimulationVerbose=true -v -timeout 24h test_cover: @bash tests/test_cover.sh @@ -204,4 +206,4 @@ localnet-stop: 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 \ -format check-ledger test_sim update_tools update_dev_tools +format check-ledger test_sim_modules test_sim_gaia_fast test_sim_gaia_slow update_tools update_dev_tools diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index d7398d899..cf63f1f4d 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -379,7 +379,7 @@ func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeg } else { // In the first block, app.deliverState.ctx will already be initialized // by InitChain. Context is now updated with Header information. - app.deliverState.ctx = app.deliverState.ctx.WithBlockHeader(req.Header) + app.deliverState.ctx = app.deliverState.ctx.WithBlockHeader(req.Header).WithBlockHeight(req.Header.Height) } if app.beginBlocker != nil { diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index f0bea1e17..5d0edcba2 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -8,47 +8,68 @@ import ( "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" banksim "github.com/cosmos/cosmos-sdk/x/bank/simulation" + govsim "github.com/cosmos/cosmos-sdk/x/gov/simulation" "github.com/cosmos/cosmos-sdk/x/mock/simulation" + slashingsim "github.com/cosmos/cosmos-sdk/x/slashing/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 + verbose 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.IntVar(&numBlocks, "SimulationNumBlocks", 500, "Number of blocks") + flag.IntVar(&blockSize, "SimulationBlockSize", 200, "Operations per block") flag.BoolVar(&enabled, "SimulationEnabled", false, "Enable the simulation") + flag.BoolVar(&verbose, "SimulationVerbose", false, "Verbose log output") } -func appStateFn(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage { +func appStateFn(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json.RawMessage { var genesisAccounts []GenesisAccount // Randomly generate some genesis accounts - for _, addr := range accs { + for _, acc := range accs { coins := sdk.Coins{sdk.Coin{"steak", sdk.NewInt(100)}} genesisAccounts = append(genesisAccounts, GenesisAccount{ - Address: addr, + Address: acc, Coins: coins, }) } // Default genesis state stakeGenesis := stake.DefaultGenesisState() - stakeGenesis.Pool.LooseTokens = sdk.NewRat(1000) + var validators []stake.Validator + var delegations []stake.Delegation + // XXX Try different numbers of initially bonded validators + numInitiallyBonded := int64(50) + for i := 0; i < int(numInitiallyBonded); i++ { + validator := stake.NewValidator(accs[i], keys[i].PubKey(), stake.Description{}) + validator.Tokens = sdk.NewRat(100) + validator.DelegatorShares = sdk.NewRat(100) + delegation := stake.Delegation{accs[i], accs[i], sdk.NewRat(100), 0} + validators = append(validators, validator) + delegations = append(delegations, delegation) + } + stakeGenesis.Pool.LooseTokens = sdk.NewRat(int64(100*250) + (numInitiallyBonded * 100)) + stakeGenesis.Validators = validators + stakeGenesis.Bonds = delegations + // No inflation, for now + stakeGenesis.Params.InflationMax = sdk.NewRat(0) + stakeGenesis.Params.InflationMin = sdk.NewRat(0) genesis := GenesisState{ Accounts: genesisAccounts, StakeData: stakeGenesis, @@ -69,16 +90,31 @@ func TestFullGaiaSimulation(t *testing.T) { } // Setup Gaia application - logger := log.NewNopLogger() + var logger log.Logger + if verbose { + logger = log.TestingLogger() + } else { + logger = log.NewNopLogger() + } db := dbm.NewMemDB() app := NewGaiaApp(logger, db, nil) require.Equal(t, "GaiaApp", app.Name()) + allInvariants := func(t *testing.T, baseapp *baseapp.BaseApp, log string) { + banksim.NonnegativeBalanceInvariant(app.accountMapper)(t, baseapp, log) + govsim.AllInvariants()(t, baseapp, log) + stakesim.AllInvariants(app.coinKeeper, app.stakeKeeper, app.accountMapper)(t, baseapp, log) + slashingsim.AllInvariants()(t, baseapp, log) + } + // Run randomized simulation simulation.SimulateFromSeed( t, app.BaseApp, appStateFn, seed, []simulation.TestAndRunTx{ banksim.TestAndRunSingleInputMsgSend(app.accountMapper), + govsim.SimulateMsgSubmitProposal(app.govKeeper, app.stakeKeeper), + govsim.SimulateMsgDeposit(app.govKeeper, app.stakeKeeper), + govsim.SimulateMsgVote(app.govKeeper, app.stakeKeeper), stakesim.SimulateMsgCreateValidator(app.accountMapper, app.stakeKeeper), stakesim.SimulateMsgEditValidator(app.stakeKeeper), stakesim.SimulateMsgDelegate(app.accountMapper, app.stakeKeeper), @@ -86,15 +122,57 @@ func TestFullGaiaSimulation(t *testing.T) { stakesim.SimulateMsgCompleteUnbonding(app.stakeKeeper), stakesim.SimulateMsgBeginRedelegate(app.accountMapper, app.stakeKeeper), stakesim.SimulateMsgCompleteRedelegate(app.stakeKeeper), + slashingsim.SimulateMsgUnrevoke(app.slashingKeeper), }, []simulation.RandSetup{}, []simulation.Invariant{ - banksim.NonnegativeBalanceInvariant(app.accountMapper), - stakesim.AllInvariants(app.coinKeeper, app.stakeKeeper, app.accountMapper), + allInvariants, }, - numKeys, numBlocks, blockSize, ) } + +// TODO: Make this not depend on Gaia or any of the modules, +// and place it in random_simulation_test.go +// +// Test doesn't use `app.ExportAppStateAndValidators` as that panics with the following: +// panic: Stored pool should not have been nil [recovered] +// panic: Stored pool should not have been nil +// Change to `app.ExportAppStateAndValidators` once it is fixed +func TestAppStateDeterminism(t *testing.T) { + numTimesToRun := 5 + appHashList := make([]json.RawMessage, numTimesToRun) + + seed := rand.Int63() + for i := 0; i < numTimesToRun; i++ { + logger := log.NewNopLogger() + db := dbm.NewMemDB() + app := NewGaiaApp(logger, db, nil) + + noOpInvariant := func(t *testing.T, baseapp *baseapp.BaseApp, log string) {} + noOpTestAndRunTx := func(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) { + return "", nil + } + + // Run randomized simulation + simulation.SimulateFromSeed( + t, app.BaseApp, appStateFn, seed, + []simulation.TestAndRunTx{ + noOpTestAndRunTx, + }, + []simulation.RandSetup{}, + []simulation.Invariant{noOpInvariant}, + 0, + 10, + ) + appHash := app.LastCommitID().Hash + appHashList[i] = appHash + } + for i := 1; i < numTimesToRun; i++ { + require.Equal(t, appHashList[0], appHashList[i]) + } +} diff --git a/tests/test_cover.sh b/tests/test_cover.sh index be6215b5a..3fb0ab69c 100644 --- a/tests/test_cover.sh +++ b/tests/test_cover.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -PKGS=$(go list ./... | grep -v /vendor/ | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test) +PKGS=$(go list ./... | grep -v /vendor/ | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test | grep -v '/simulation') set -e echo "mode: atomic" > coverage.txt diff --git a/types/stake.go b/types/stake.go index 4e3cf38a3..f611a2b51 100644 --- a/types/stake.go +++ b/types/stake.go @@ -51,8 +51,9 @@ type Validator interface { // validator which fulfills abci validator interface for use in Tendermint func ABCIValidator(v Validator) abci.Validator { return abci.Validator{ - PubKey: tmtypes.TM2PB.PubKey(v.GetPubKey()), - Power: v.GetPower().RoundInt64(), + PubKey: tmtypes.TM2PB.PubKey(v.GetPubKey()), + Address: v.GetPubKey().Address(), + Power: v.GetPower().RoundInt64(), } } diff --git a/x/bank/simulation/msgs.go b/x/bank/simulation/msgs.go index 3a7248875..43d7e1fd7 100644 --- a/x/bank/simulation/msgs.go +++ b/x/bank/simulation/msgs.go @@ -35,6 +35,10 @@ func TestAndRunSingleInputMsgSend(mapper auth.AccountMapper) simulation.TestAndR toAddr := sdk.AccAddress(toKey.PubKey().Address()) initFromCoins := mapper.GetAccount(ctx, fromAddr).GetCoins() + if len(initFromCoins) == 0 { + return "skipping, no coins at all", nil + } + denomIndex := r.Intn(len(initFromCoins)) amt, goErr := randPositiveInt(r, initFromCoins[denomIndex].Amount) if goErr != nil { diff --git a/x/bank/simulation/sim_test.go b/x/bank/simulation/sim_test.go index 5d76dd058..49e3dfa92 100644 --- a/x/bank/simulation/sim_test.go +++ b/x/bank/simulation/sim_test.go @@ -5,6 +5,8 @@ import ( "math/rand" "testing" + "github.com/tendermint/tendermint/crypto" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/bank" "github.com/cosmos/cosmos-sdk/x/mock" @@ -24,7 +26,7 @@ func TestBankWithRandomMessages(t *testing.T) { panic(err) } - appStateFn := func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage { + appStateFn := func(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json.RawMessage { mock.RandomSetGenesis(r, mapp, accs, []string{"stake"}) return json.RawMessage("{}") } @@ -39,6 +41,6 @@ func TestBankWithRandomMessages(t *testing.T) { NonnegativeBalanceInvariant(mapper), TotalCoinsInvariant(mapper, func() sdk.Coins { return mapp.TotalCoinsSupply }), }, - 100, 30, 30, + 30, 30, ) } diff --git a/x/gov/handler.go b/x/gov/handler.go index a9fdfcb3c..554c06a8a 100644 --- a/x/gov/handler.go +++ b/x/gov/handler.go @@ -96,6 +96,8 @@ func handleMsgVote(ctx sdk.Context, keeper Keeper, msg MsgVote) sdk.Result { // Called every block, process inflation, update validator set func EndBlocker(ctx sdk.Context, keeper Keeper) (resTags sdk.Tags) { + logger := ctx.Logger().With("module", "x/gov") + resTags = sdk.NewTags() // Delete proposals that haven't met minDeposit @@ -109,6 +111,9 @@ func EndBlocker(ctx sdk.Context, keeper Keeper) (resTags sdk.Tags) { keeper.DeleteProposal(ctx, inactiveProposal) resTags.AppendTag(tags.Action, tags.ActionProposalDropped) resTags.AppendTag(tags.ProposalID, proposalIDBytes) + + logger.Info("Proposal %d - \"%s\" - didn't mean minimum deposit (had only %s), deleted", + inactiveProposal.GetProposalID(), inactiveProposal.GetTitle(), inactiveProposal.GetTotalDeposit()) } // Check if earliest Active Proposal ended voting period yet @@ -136,6 +141,9 @@ func EndBlocker(ctx sdk.Context, keeper Keeper) (resTags sdk.Tags) { activeProposal.SetTallyResult(tallyResults) keeper.SetProposal(ctx, activeProposal) + logger.Info("Proposal %d - \"%s\" - tallied, passed: %v", + activeProposal.GetProposalID(), activeProposal.GetTitle(), passes) + for _, valAddr := range nonVotingVals { val := keeper.ds.GetValidatorSet().Validator(ctx, valAddr) keeper.ds.GetValidatorSet().Slash(ctx, @@ -143,6 +151,9 @@ func EndBlocker(ctx sdk.Context, keeper Keeper) (resTags sdk.Tags) { ctx.BlockHeight(), val.GetPower().RoundInt64(), keeper.GetTallyingProcedure(ctx).GovernancePenalty) + + logger.Info("Validator %s failed to vote on proposal %d, slashing", + val.GetOwner(), activeProposal.GetProposalID()) } resTags.AppendTag(tags.Action, action) diff --git a/x/gov/keeper.go b/x/gov/keeper.go index 0034e3d71..8a23ad248 100644 --- a/x/gov/keeper.go +++ b/x/gov/keeper.go @@ -119,6 +119,18 @@ func (keeper Keeper) setInitialProposalID(ctx sdk.Context, proposalID int64) sdk return nil } +// Get the last used proposal ID +func (keeper Keeper) GetLastProposalID(ctx sdk.Context) (proposalID int64) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(KeyNextProposalID) + if bz == nil { + return 0 + } + keeper.cdc.MustUnmarshalBinary(bz, &proposalID) + proposalID-- + return +} + func (keeper Keeper) getNewProposalID(ctx sdk.Context) (proposalID int64, err sdk.Error) { store := ctx.KVStore(keeper.storeKey) bz := store.Get(KeyNextProposalID) diff --git a/x/gov/simulation/invariants.go b/x/gov/simulation/invariants.go new file mode 100644 index 000000000..e9275f3c1 --- /dev/null +++ b/x/gov/simulation/invariants.go @@ -0,0 +1,19 @@ +package simulation + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" +) + +// AllInvariants tests all governance invariants +func AllInvariants() simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + // TODO Add some invariants! + // Checking proposal queues, no passed-but-unexecuted proposals, etc. + require.Nil(t, nil) + } +} diff --git a/x/gov/simulation/msgs.go b/x/gov/simulation/msgs.go new file mode 100644 index 000000000..596a013d3 --- /dev/null +++ b/x/gov/simulation/msgs.go @@ -0,0 +1,132 @@ +package simulation + +import ( + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/crypto" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + "github.com/cosmos/cosmos-sdk/x/stake" +) + +const ( + denom = "steak" +) + +// SimulateMsgSubmitProposal +func SimulateMsgSubmitProposal(k gov.Keeper, sk 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) { + key := simulation.RandomKey(r, keys) + addr := sdk.AccAddress(key.PubKey().Address()) + deposit := randomDeposit(r) + msg := gov.NewMsgSubmitProposal( + simulation.RandStringOfLength(r, 5), + simulation.RandStringOfLength(r, 5), + gov.ProposalTypeText, + addr, + deposit, + ) + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := gov.NewHandler(k)(ctx, msg) + if result.IsOK() { + // Update pool to keep invariants + pool := sk.GetPool(ctx) + pool.LooseTokens = pool.LooseTokens.Sub(sdk.NewRatFromInt(deposit.AmountOf(denom))) + sk.SetPool(ctx, pool) + write() + } + event(fmt.Sprintf("gov/MsgSubmitProposal/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgSubmitProposal: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgDeposit +func SimulateMsgDeposit(k gov.Keeper, sk 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) { + key := simulation.RandomKey(r, keys) + addr := sdk.AccAddress(key.PubKey().Address()) + proposalID, ok := randomProposalID(r, k, ctx) + if !ok { + return "no-operation", nil + } + deposit := randomDeposit(r) + msg := gov.NewMsgDeposit(addr, proposalID, deposit) + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := gov.NewHandler(k)(ctx, msg) + if result.IsOK() { + // Update pool to keep invariants + pool := sk.GetPool(ctx) + pool.LooseTokens = pool.LooseTokens.Sub(sdk.NewRatFromInt(deposit.AmountOf(denom))) + sk.SetPool(ctx, pool) + write() + } + event(fmt.Sprintf("gov/MsgDeposit/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgDeposit: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgVote +func SimulateMsgVote(k gov.Keeper, sk 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) { + key := simulation.RandomKey(r, keys) + addr := sdk.AccAddress(key.PubKey().Address()) + proposalID, ok := randomProposalID(r, k, ctx) + if !ok { + return "no-operation", nil + } + option := randomVotingOption(r) + msg := gov.NewMsgVote(addr, proposalID, option) + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := gov.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("gov/MsgVote/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgVote: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// Pick a random deposit +func randomDeposit(r *rand.Rand) sdk.Coins { + // TODO Choose based on account balance and min deposit + amount := int64(r.Intn(20)) + 1 + return sdk.Coins{sdk.NewInt64Coin(denom, amount)} +} + +// Pick a random proposal ID +func randomProposalID(r *rand.Rand, k gov.Keeper, ctx sdk.Context) (proposalID int64, ok bool) { + lastProposalID := k.GetLastProposalID(ctx) + if lastProposalID < 1 { + return 0, false + } + proposalID = int64(r.Intn(int(lastProposalID))) + return proposalID, true +} + +// Pick a random voting option +func randomVotingOption(r *rand.Rand) gov.VoteOption { + switch r.Intn(4) { + case 0: + return gov.OptionYes + case 1: + return gov.OptionAbstain + case 2: + return gov.OptionNo + case 3: + return gov.OptionNoWithVeto + } + panic("should not happen") +} diff --git a/x/gov/simulation/sim_test.go b/x/gov/simulation/sim_test.go new file mode 100644 index 000000000..e7131f8fc --- /dev/null +++ b/x/gov/simulation/sim_test.go @@ -0,0 +1,68 @@ +package simulation + +import ( + "encoding/json" + "math/rand" + "testing" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/stake" +) + +// TestGovWithRandomMessages +func TestGovWithRandomMessages(t *testing.T) { + mapp := mock.NewApp() + + bank.RegisterWire(mapp.Cdc) + gov.RegisterWire(mapp.Cdc) + mapper := mapp.AccountMapper + coinKeeper := bank.NewKeeper(mapper) + stakeKey := sdk.NewKVStoreKey("stake") + stakeKeeper := stake.NewKeeper(mapp.Cdc, stakeKey, coinKeeper, stake.DefaultCodespace) + paramKey := sdk.NewKVStoreKey("params") + paramKeeper := params.NewKeeper(mapp.Cdc, paramKey) + govKey := sdk.NewKVStoreKey("gov") + govKeeper := gov.NewKeeper(mapp.Cdc, govKey, paramKeeper.Setter(), coinKeeper, stakeKeeper, gov.DefaultCodespace) + mapp.Router().AddRoute("gov", gov.NewHandler(govKeeper)) + mapp.SetEndBlocker(func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + gov.EndBlocker(ctx, govKeeper) + return abci.ResponseEndBlock{} + }) + + err := mapp.CompleteSetup([]*sdk.KVStoreKey{stakeKey, paramKey, govKey}) + if err != nil { + panic(err) + } + + appStateFn := func(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json.RawMessage { + mock.RandomSetGenesis(r, mapp, accs, []string{"stake"}) + return json.RawMessage("{}") + } + + setup := func(r *rand.Rand, privKeys []crypto.PrivKey) { + ctx := mapp.NewContext(false, abci.Header{}) + stake.InitGenesis(ctx, stakeKeeper, stake.DefaultGenesisState()) + gov.InitGenesis(ctx, govKeeper, gov.DefaultGenesisState()) + } + + simulation.Simulate( + t, mapp.BaseApp, appStateFn, + []simulation.TestAndRunTx{ + SimulateMsgSubmitProposal(govKeeper, stakeKeeper), + SimulateMsgDeposit(govKeeper, stakeKeeper), + SimulateMsgVote(govKeeper, stakeKeeper), + }, []simulation.RandSetup{ + setup, + }, []simulation.Invariant{ + AllInvariants(), + }, 10, 100, + ) +} diff --git a/x/mock/app.go b/x/mock/app.go index 53b6345b0..f472c5531 100644 --- a/x/mock/app.go +++ b/x/mock/app.go @@ -11,6 +11,7 @@ import ( abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" ) @@ -173,7 +174,32 @@ func GeneratePrivKeyAddressPairs(n int) (keys []crypto.PrivKey, addrs []sdk.AccA keys = make([]crypto.PrivKey, n, n) addrs = make([]sdk.AccAddress, n, n) for i := 0; i < n; i++ { - keys[i] = ed25519.GenPrivKey() + if rand.Int63()%2 == 0 { + keys[i] = secp256k1.GenPrivKey() + } else { + keys[i] = ed25519.GenPrivKey() + } + addrs[i] = sdk.AccAddress(keys[i].PubKey().Address()) + } + return +} + +// GeneratePrivKeyAddressPairsFromRand generates a total of n private key, address +// pairs using the provided randomness source. +func GeneratePrivKeyAddressPairsFromRand(rand *rand.Rand, n int) (keys []crypto.PrivKey, addrs []sdk.AccAddress) { + keys = make([]crypto.PrivKey, n, n) + addrs = make([]sdk.AccAddress, n, n) + for i := 0; i < n; i++ { + secret := make([]byte, 32) + _, err := rand.Read(secret) + if err != nil { + panic("Could not read randomness") + } + if rand.Int63()%2 == 0 { + keys[i] = secp256k1.GenPrivKeySecp256k1(secret) + } else { + keys[i] = ed25519.GenPrivKeyFromSecret(secret) + } addrs[i] = sdk.AccAddress(keys[i].PubKey().Address()) } return diff --git a/x/mock/simulation/constants.go b/x/mock/simulation/constants.go new file mode 100644 index 000000000..985a22dca --- /dev/null +++ b/x/mock/simulation/constants.go @@ -0,0 +1,31 @@ +package simulation + +const ( + // Fraction of double-signing evidence from a past height + pastEvidenceFraction float64 = 0.5 + + // Minimum time per block + minTimePerBlock int64 = 86400 / 2 + + // Maximum time per block + maxTimePerBlock int64 = 86400 + + // Number of keys + numKeys int = 250 + + // Chance that double-signing evidence is found on a given block + evidenceFraction float64 = 0.01 + + // TODO Remove in favor of binary search for invariant violation + onOperation bool = false +) + +var ( + // Currently there are 3 different liveness types, fully online, spotty connection, offline. + initialLivenessWeightings = []int{40, 5, 5} + livenessTransitionMatrix, _ = CreateTransitionMatrix([][]int{ + {90, 20, 1}, + {10, 50, 5}, + {0, 10, 1000}, + }) +) diff --git a/x/mock/simulation/random_simulate_blocks.go b/x/mock/simulation/random_simulate_blocks.go index 5f507b89c..1b6953631 100644 --- a/x/mock/simulation/random_simulate_blocks.go +++ b/x/mock/simulation/random_simulate_blocks.go @@ -7,75 +7,211 @@ import ( "testing" "time" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + tmtypes "github.com/tendermint/tendermint/types" + "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, + t *testing.T, app *baseapp.BaseApp, appStateFn func(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json.RawMessage, ops []TestAndRunTx, setups []RandSetup, + invariants []Invariant, numBlocks int, blockSize int, ) { time := time.Now().UnixNano() - SimulateFromSeed(t, app, appStateFn, time, ops, setups, invariants, numKeys, numBlocks, blockSize) + SimulateFromSeed(t, app, appStateFn, time, ops, setups, invariants, 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, + t *testing.T, app *baseapp.BaseApp, appStateFn func(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json.RawMessage, seed int64, ops []TestAndRunTx, setups []RandSetup, + invariants []Invariant, numBlocks int, blockSize int, ) { log := fmt.Sprintf("Starting SimulateFromSeed with randomness created with seed %d", int(seed)) - keys, addrs := mock.GeneratePrivKeyAddressPairs(numKeys) + fmt.Printf("%s\n", log) r := rand.New(rand.NewSource(seed)) + keys, accs := mock.GeneratePrivKeyAddressPairsFromRand(r, numKeys) // Setup event stats events := make(map[string]uint) event := func(what string) { + log += "\nevent - " + what events[what]++ } - app.InitChain(abci.RequestInitChain{AppStateBytes: appStateFn(r, addrs)}) + timestamp := time.Unix(0, 0) + timeDiff := maxTimePerBlock - minTimePerBlock + + res := app.InitChain(abci.RequestInitChain{AppStateBytes: appStateFn(r, keys, accs)}) + validators := make(map[string]mockValidator) + for _, validator := range res.Validators { + validators[string(validator.Address)] = mockValidator{validator, GetMemberOfInitialState(r, initialLivenessWeightings)} + } + for i := 0; i < len(setups); i++ { setups[i](r, keys) } - app.Commit() - header := abci.Header{Height: 0} + header := abci.Header{Height: 0, Time: timestamp} + opCount := 0 + + request := abci.RequestBeginBlock{Header: header} + + var pastTimes []time.Time for i := 0; i < numBlocks; i++ { - app.BeginBlock(abci.RequestBeginBlock{}) - // Make sure invariants hold at beginning of block and when nothing was - // done. + // Log the header time for future lookup + pastTimes = append(pastTimes, header.Time) + + // Run the BeginBlock handler + app.BeginBlock(request) + + log += "\nBeginBlock" + + // Make sure invariants hold at beginning of block 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++ { + var thisBlockSize int + load := r.Float64() + switch { + case load < 0.33: + thisBlockSize = 0 + case load < 0.66: + thisBlockSize = r.Intn(blockSize * 2) + default: + thisBlockSize = r.Intn(blockSize * 4) + } + for j := 0; j < thisBlockSize; 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) + if onOperation { + AssertAllInvariants(t, app, invariants, log) + } + if opCount%200 == 0 { + fmt.Printf("\rSimulating... block %d/%d, operation %d.", header.Height, numBlocks, opCount) + } + opCount++ } - app.EndBlock(abci.RequestEndBlock{}) + res := app.EndBlock(abci.RequestEndBlock{}) header.Height++ + header.Time = header.Time.Add(time.Duration(minTimePerBlock) * time.Second).Add(time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second) + + log += "\nEndBlock" + + // Make sure invariants hold at end of block + AssertAllInvariants(t, app, invariants, log) + + // Generate a random RequestBeginBlock with the current validator set for the next block + request = RandomRequestBeginBlock(t, r, validators, livenessTransitionMatrix, evidenceFraction, pastTimes, event, header, log) + + // Update the validator set + validators = updateValidators(t, r, validators, res.ValidatorUpdates, event) } + fmt.Printf("\nSimulation complete. Final height (blocks): %d, final time (seconds): %v\n", header.Height, header.Time) DisplayEvents(events) } +// RandomRequestBeginBlock generates a list of signing validators according to the provided list of validators, signing fraction, and evidence fraction +func RandomRequestBeginBlock(t *testing.T, r *rand.Rand, validators map[string]mockValidator, livenessTransitions TransitionMatrix, evidenceFraction float64, + pastTimes []time.Time, event func(string), header abci.Header, log string) abci.RequestBeginBlock { + if len(validators) == 0 { + return abci.RequestBeginBlock{Header: header} + } + signingValidators := make([]abci.SigningValidator, len(validators)) + i := 0 + for _, mVal := range validators { + mVal.livenessState = livenessTransitions.NextState(r, mVal.livenessState) + signed := true + + if mVal.livenessState == 1 { + // spotty connection, 50% probability of success + // See https://github.com/golang/go/issues/23804#issuecomment-365370418 + // for reasoning behind computing like this + signed = r.Int63()%2 == 0 + } else if mVal.livenessState == 2 { + // offline + signed = false + } + if signed { + event("beginblock/signing/signed") + } else { + event("beginblock/signing/missed") + } + signingValidators[i] = abci.SigningValidator{ + Validator: mVal.val, + SignedLastBlock: signed, + } + i++ + } + evidence := make([]abci.Evidence, 0) + for r.Float64() < evidenceFraction { + height := header.Height + time := header.Time + if r.Float64() < pastEvidenceFraction { + height = int64(r.Intn(int(header.Height))) + time = pastTimes[height] + } + validator := signingValidators[r.Intn(len(signingValidators))].Validator + var currentTotalVotingPower int64 + for _, mVal := range validators { + currentTotalVotingPower += mVal.val.Power + } + evidence = append(evidence, abci.Evidence{ + Type: tmtypes.ABCIEvidenceTypeDuplicateVote, + Validator: validator, + Height: height, + Time: time, + TotalVotingPower: currentTotalVotingPower, + }) + event("beginblock/evidence") + } + return abci.RequestBeginBlock{ + Header: header, + LastCommitInfo: abci.LastCommitInfo{ + Validators: signingValidators, + }, + ByzantineValidators: evidence, + } +} + // 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) } } + +// updateValidators mimicks Tendermint's update logic +func updateValidators(t *testing.T, r *rand.Rand, current map[string]mockValidator, updates []abci.Validator, event func(string)) map[string]mockValidator { + for _, update := range updates { + switch { + case update.Power == 0: + require.NotNil(t, current[string(update.PubKey.Data)], "tried to delete a nonexistent validator") + event("endblock/validatorupdates/kicked") + delete(current, string(update.PubKey.Data)) + default: + // Does validator already exist? + if mVal, ok := current[string(update.PubKey.Data)]; ok { + mVal.val = update + event("endblock/validatorupdates/updated") + } else { + // Set this new validator + current[string(update.PubKey.Data)] = mockValidator{update, GetMemberOfInitialState(r, initialLivenessWeightings)} + event("endblock/validatorupdates/added") + } + } + } + return current +} diff --git a/x/mock/simulation/transition_matrix.go b/x/mock/simulation/transition_matrix.go new file mode 100644 index 000000000..39bdb1e4f --- /dev/null +++ b/x/mock/simulation/transition_matrix.go @@ -0,0 +1,70 @@ +package simulation + +import ( + "fmt" + "math/rand" +) + +// TransitionMatrix is _almost_ a left stochastic matrix. +// It is technically not one due to not normalizing the column values. +// In the future, if we want to find the steady state distribution, +// it will be quite easy to normalize these values to get a stochastic matrix. +// Floats aren't currently used as the default due to non-determinism across +// architectures +type TransitionMatrix struct { + weights [][]int + // total in each column + totals []int + n int +} + +// CreateTransitionMatrix creates a transition matrix from the provided weights. +// TODO: Provide example usage +func CreateTransitionMatrix(weights [][]int) (TransitionMatrix, error) { + n := len(weights) + for i := 0; i < n; i++ { + if len(weights[i]) != n { + return TransitionMatrix{}, fmt.Errorf("Transition Matrix: Non-square matrix provided, error on row %d", i) + } + } + totals := make([]int, n) + for row := 0; row < n; row++ { + for col := 0; col < n; col++ { + totals[col] += weights[row][col] + } + } + return TransitionMatrix{weights, totals, n}, nil +} + +// NextState returns the next state randomly chosen using r, and the weightings provided +// in the transition matrix. +func (t TransitionMatrix) NextState(r *rand.Rand, i int) int { + randNum := r.Intn(t.totals[i]) + for row := 0; row < t.n; row++ { + if randNum < t.weights[row][i] { + return row + } + randNum -= t.weights[row][i] + } + // This line should never get executed + return -1 +} + +// GetMemberOfInitialState takes an initial array of weights, of size n. +// It returns a weighted random number in [0,n). +func GetMemberOfInitialState(r *rand.Rand, weights []int) int { + n := len(weights) + total := 0 + for i := 0; i < n; i++ { + total += weights[i] + } + randNum := r.Intn(total) + for state := 0; state < n; state++ { + if randNum < weights[state] { + return state + } + randNum -= weights[state] + } + // This line should never get executed + return -1 +} diff --git a/x/mock/simulation/types.go b/x/mock/simulation/types.go index 6e1d9f198..35769b0b2 100644 --- a/x/mock/simulation/types.go +++ b/x/mock/simulation/types.go @@ -6,6 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" ) @@ -25,6 +26,11 @@ type ( // If the invariant has been broken, the function should halt the // test and output the log. Invariant func(t *testing.T, app *baseapp.BaseApp, log string) + + mockValidator struct { + val abci.Validator + livenessState int + } ) // PeriodicInvariant returns an Invariant function closure that asserts diff --git a/x/mock/simulation/util.go b/x/mock/simulation/util.go index 14227a1ae..1d64ba30d 100644 --- a/x/mock/simulation/util.go +++ b/x/mock/simulation/util.go @@ -3,8 +3,9 @@ package simulation import ( "fmt" "math/rand" + "sort" - crypto "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -39,8 +40,15 @@ func RandStringOfLength(r *rand.Rand, n int) string { // Pretty-print events as a table func DisplayEvents(events map[string]uint) { - // TODO - fmt.Printf("Events: %v\n", events) + var keys []string + for key := range events { + keys = append(keys, key) + } + sort.Strings(keys) + fmt.Printf("Event statistics: \n") + for _, key := range keys { + fmt.Printf(" % 60s => %d\n", key, events[key]) + } } // Pick a random key from an array diff --git a/x/slashing/simulation/invariants.go b/x/slashing/simulation/invariants.go new file mode 100644 index 000000000..7352aa503 --- /dev/null +++ b/x/slashing/simulation/invariants.go @@ -0,0 +1,18 @@ +package simulation + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" +) + +// AllInvariants tests all slashing invariants +func AllInvariants() simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + // TODO Any invariants to check here? + require.Nil(t, nil) + } +} diff --git a/x/slashing/simulation/msgs.go b/x/slashing/simulation/msgs.go new file mode 100644 index 000000000..b6a093674 --- /dev/null +++ b/x/slashing/simulation/msgs.go @@ -0,0 +1,34 @@ +package simulation + +import ( + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/crypto" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + "github.com/cosmos/cosmos-sdk/x/slashing" +) + +// SimulateMsgUnrevoke +func SimulateMsgUnrevoke(k slashing.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) { + key := simulation.RandomKey(r, keys) + address := sdk.AccAddress(key.PubKey().Address()) + msg := slashing.NewMsgUnrevoke(address) + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := slashing.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("slashing/MsgUnrevoke/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgUnrevoke: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} diff --git a/x/stake/genesis.go b/x/stake/genesis.go index 46bda752d..7a004bccd 100644 --- a/x/stake/genesis.go +++ b/x/stake/genesis.go @@ -33,7 +33,6 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) (res [ // Manually set indexes for the first time keeper.SetValidatorByPubKeyIndex(ctx, validator) - keeper.SetValidatorByPowerIndex(ctx, validator, data.Pool) if validator.Status == sdk.Bonded { diff --git a/x/stake/keeper/validator.go b/x/stake/keeper/validator.go index eaaf3df9e..8b8cea73d 100644 --- a/x/stake/keeper/validator.go +++ b/x/stake/keeper/validator.go @@ -421,6 +421,9 @@ func (k Keeper) UpdateBondedValidators( if !validator.Revoked { if validator.Status != sdk.Bonded { validatorToBond = validator + if newValidatorBonded { + panic("already decided to bond a validator, can't bond another!") + } newValidatorBonded = true } @@ -436,6 +439,10 @@ func (k Keeper) UpdateBondedValidators( iterator.Close() + if newValidatorBonded && bytes.Equal(oldCliffValidatorAddr, validator.Owner) { + panic("cliff validator has not been changed, yet we bonded a new validator") + } + // clear or set the cliff validator if bondedValidatorsCount == int(maxValidators) { k.setCliffValidator(ctx, validator, k.GetPool(ctx)) @@ -556,7 +563,7 @@ func (k Keeper) unbondValidator(ctx sdk.Context, validator types.Validator) type // sanity check if validator.Status == sdk.Unbonded { - panic(fmt.Sprintf("should not already be unbonded, validator: %v\n", validator)) + panic(fmt.Sprintf("should not already be unbonded, validator: %v\n", validator)) } // set the status diff --git a/x/stake/simulation/msgs.go b/x/stake/simulation/msgs.go index e4077a749..ec39f87b0 100644 --- a/x/stake/simulation/msgs.go +++ b/x/stake/simulation/msgs.go @@ -235,7 +235,10 @@ func SimulateMsgCompleteRedelegate(k stake.Keeper) simulation.TestAndRunTx { 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()) + gen := stake.DefaultGenesisState() + gen.Params.InflationMax = sdk.NewRat(0) + gen.Params.InflationMin = sdk.NewRat(0) + stake.InitGenesis(ctx, k, gen) params := k.GetParams(ctx) denom := params.BondDenom loose := sdk.ZeroInt() diff --git a/x/stake/simulation/sim_test.go b/x/stake/simulation/sim_test.go index 391ca1996..3e80e1eb3 100644 --- a/x/stake/simulation/sim_test.go +++ b/x/stake/simulation/sim_test.go @@ -5,12 +5,14 @@ import ( "math/rand" "testing" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + 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 @@ -35,7 +37,7 @@ func TestStakeWithRandomMessages(t *testing.T) { panic(err) } - appStateFn := func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage { + appStateFn := func(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json.RawMessage { mock.RandomSetGenesis(r, mapp, accs, []string{"stake"}) return json.RawMessage("{}") } @@ -54,6 +56,6 @@ func TestStakeWithRandomMessages(t *testing.T) { Setup(mapp, stakeKeeper), }, []simulation.Invariant{ AllInvariants(coinKeeper, stakeKeeper, mapp.AccountMapper), - }, 10, 100, 100, + }, 10, 100, ) } diff --git a/x/stake/types/validator.go b/x/stake/types/validator.go index 837b8f8e8..089e8ea92 100644 --- a/x/stake/types/validator.go +++ b/x/stake/types/validator.go @@ -314,8 +314,9 @@ func (d Description) EnsureLength() (Description, sdk.Error) { // ABCIValidator returns an abci.Validator from a staked validator type. func (v Validator) ABCIValidator() abci.Validator { return abci.Validator{ - PubKey: tmtypes.TM2PB.PubKey(v.PubKey), - Power: v.BondedTokens().RoundInt64(), + PubKey: tmtypes.TM2PB.PubKey(v.PubKey), + Address: v.PubKey.Address(), + Power: v.BondedTokens().RoundInt64(), } } @@ -323,8 +324,9 @@ func (v Validator) ABCIValidator() abci.Validator { // with with zero power used for validator updates. func (v Validator) ABCIValidatorZero() abci.Validator { return abci.Validator{ - PubKey: tmtypes.TM2PB.PubKey(v.PubKey), - Power: 0, + PubKey: tmtypes.TM2PB.PubKey(v.PubKey), + Address: v.PubKey.Address(), + Power: 0, } }