From 256ec0f07b0559ab594da5cf9cae42203ccae7bc Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 5 Nov 2018 05:44:43 +0100 Subject: [PATCH] Simulation improvements (logging fix, random genesis parameters) (#2617) * Print out initial update on every block * Randomize simulation parameters * Randomize initial liveness weightings * Randomize genesis parameters * fixed power store invariant * IterateValidatorsBonded -> IterateBondedValidatorsByPower * WriteValidators uses IterateLastValidators rather than IterateBondedValidatorsByPower * fixed democoin interface Closes #2556 Closes #2396 Via #2671: closes #2669 closes #2670 closes #2620 Offshoot issues: #2618 #2619 #2620 #2661 --- PENDING.md | 5 ++ baseapp/baseapp.go | 1 + cmd/gaia/app/sim_test.go | 78 +++++++++++++++++---- examples/democoin/mock/validator.go | 9 ++- scripts/multisim.sh | 3 +- types/stake.go | 6 +- x/gov/tally.go | 2 +- x/mock/simulation/constants.go | 37 ---------- x/mock/simulation/params.go | 66 +++++++++++++++++ x/mock/simulation/random_simulate_blocks.go | 42 +++++------ x/stake/genesis.go | 2 +- x/stake/keeper/sdk_types.go | 26 ++++++- x/stake/keeper/validator.go | 14 ++++ x/stake/simulation/invariants.go | 32 ++++++--- 14 files changed, 236 insertions(+), 87 deletions(-) delete mode 100644 x/mock/simulation/constants.go create mode 100644 x/mock/simulation/params.go diff --git a/PENDING.md b/PENDING.md index 9acbf325b7aa..ad20be35f4d0 100644 --- a/PENDING.md +++ b/PENDING.md @@ -43,6 +43,10 @@ IMPROVEMENTS * SDK - #2573 [x/distribution] add accum invariance + - #2556 [x/mock/simulation] Fix debugging output + - #2396 [x/mock/simulation] Change parameters to get more slashes + - #2617 [x/mock/simulation] Randomize all genesis parameters + - #2669 [x/stake] Added invarant check to make sure validator's power aligns with its spot in the power store. - \#1924 [x/mock/simulation] Use a transition matrix for block size - \#2660 [x/mock/simulation] Staking transactions get tested far more frequently - #2610 [x/stake] Block redelegation to and from the same validator @@ -58,6 +62,7 @@ BUG FIXES * Gaia CLI (`gaiacli`) * Gaia + - #2670 [x/stake] fixed incorrent `IterateBondedValidators` and split into two functions: `IterateBondedValidators` and `IterateLastBlockConsValidators` * SDK - #2625 [x/gov] fix AppendTag function usage error diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index a2b2f4166757..827536d21562 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -394,6 +394,7 @@ func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res ctx := sdk.NewContext(app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.Logger). WithMinimumFees(app.minimumFees) + // Passes the rest of the path as an argument to the querier. // For example, in the path "custom/gov/proposal/test", the gov querier gets []string{"proposal", "test"} as the path resBytes, err := querier(ctx, path[2:], req) diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index 9f94a82f7c26..0216616a88ac 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -8,6 +8,7 @@ import ( "math/rand" "os" "testing" + "time" "github.com/stretchr/testify/require" @@ -50,42 +51,93 @@ func init() { func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { var genesisAccounts []GenesisAccount - amt := int64(10000) + amount := int64(r.Intn(1e6)) + numInitiallyBonded := int64(r.Intn(250)) + numAccs := int64(len(accs)) + if numInitiallyBonded > numAccs { + numInitiallyBonded = numAccs + } + fmt.Printf("Selected randomly generated parameters for simulated genesis: {amount of steak per account: %v, initially bonded validators: %v}\n", amount, numInitiallyBonded) // Randomly generate some genesis accounts for _, acc := range accs { - coins := sdk.Coins{sdk.Coin{"steak", sdk.NewInt(amt)}} + coins := sdk.Coins{sdk.Coin{"steak", sdk.NewInt(amount)}} genesisAccounts = append(genesisAccounts, GenesisAccount{ Address: acc.Address, Coins: coins, }) } - // Default genesis state - govGenesis := gov.DefaultGenesisState() - stakeGenesis := stake.DefaultGenesisState() - slashingGenesis := slashing.DefaultGenesisState() + // Random genesis states + govGenesis := gov.GenesisState{ + StartingProposalID: int64(r.Intn(100)), + DepositProcedure: gov.DepositProcedure{ + MinDeposit: sdk.Coins{sdk.NewInt64Coin("steak", int64(r.Intn(1e3)))}, + MaxDepositPeriod: time.Duration(r.Intn(2*172800)) * time.Second, + }, + VotingProcedure: gov.VotingProcedure{ + VotingPeriod: time.Duration(r.Intn(2*172800)) * time.Second, + }, + TallyingProcedure: gov.TallyingProcedure{ + Threshold: sdk.NewDecWithPrec(5, 1), + Veto: sdk.NewDecWithPrec(334, 3), + GovernancePenalty: sdk.NewDecWithPrec(1, 2), + }, + } + fmt.Printf("Selected randomly generated governance parameters: %+v\n", govGenesis) + stakeGenesis := stake.GenesisState{ + Pool: stake.InitialPool(), + Params: stake.Params{ + UnbondingTime: time.Duration(r.Intn(60*60*24*3*2)) * time.Second, + MaxValidators: uint16(r.Intn(250)), + BondDenom: "steak", + }, + } + fmt.Printf("Selected randomly generated staking parameters: %+v\n", stakeGenesis) + slashingGenesis := slashing.GenesisState{ + Params: slashing.Params{ + MaxEvidenceAge: stakeGenesis.Params.UnbondingTime, + DoubleSignUnbondDuration: time.Duration(r.Intn(60*60*24)) * time.Second, + SignedBlocksWindow: int64(r.Intn(1000)), + DowntimeUnbondDuration: time.Duration(r.Intn(86400)) * time.Second, + MinSignedPerWindow: sdk.NewDecWithPrec(int64(r.Intn(10)), 1), + SlashFractionDoubleSign: sdk.NewDec(1).Quo(sdk.NewDec(int64(r.Intn(50) + 1))), + SlashFractionDowntime: sdk.NewDec(1).Quo(sdk.NewDec(int64(r.Intn(200) + 1))), + }, + } + fmt.Printf("Selected randomly generated slashing parameters: %+v\n", slashingGenesis) + mintGenesis := mint.GenesisState{ + Minter: mint.Minter{ + InflationLastTime: time.Unix(0, 0), + Inflation: sdk.NewDecWithPrec(int64(r.Intn(99)), 2), + }, + Params: mint.Params{ + MintDenom: "steak", + InflationRateChange: sdk.NewDecWithPrec(int64(r.Intn(99)), 2), + InflationMax: sdk.NewDecWithPrec(20, 2), + InflationMin: sdk.NewDecWithPrec(7, 2), + GoalBonded: sdk.NewDecWithPrec(67, 2), + }, + } + fmt.Printf("Selected randomly generated minting parameters: %v\n", mintGenesis) var validators []stake.Validator var delegations []stake.Delegation - // XXX Try different numbers of initially bonded validators - numInitiallyBonded := int64(50) valAddrs := make([]sdk.ValAddress, numInitiallyBonded) for i := 0; i < int(numInitiallyBonded); i++ { valAddr := sdk.ValAddress(accs[i].Address) valAddrs[i] = valAddr validator := stake.NewValidator(valAddr, accs[i].PubKey, stake.Description{}) - validator.Tokens = sdk.NewDec(amt) - validator.DelegatorShares = sdk.NewDec(amt) - delegation := stake.Delegation{accs[i].Address, valAddr, sdk.NewDec(amt), 0} + validator.Tokens = sdk.NewDec(amount) + validator.DelegatorShares = sdk.NewDec(amount) + delegation := stake.Delegation{accs[i].Address, valAddr, sdk.NewDec(amount), 0} validators = append(validators, validator) delegations = append(delegations, delegation) } - stakeGenesis.Pool.LooseTokens = sdk.NewDec(amt*250 + (numInitiallyBonded * amt)) + stakeGenesis.Pool.LooseTokens = sdk.NewDec((amount * numAccs) + (numInitiallyBonded * amount)) stakeGenesis.Validators = validators stakeGenesis.Bonds = delegations - mintGenesis := mint.DefaultGenesisState() genesis := GenesisState{ Accounts: genesisAccounts, diff --git a/examples/democoin/mock/validator.go b/examples/democoin/mock/validator.go index fc00b79be007..1d10c48b2459 100644 --- a/examples/democoin/mock/validator.go +++ b/examples/democoin/mock/validator.go @@ -82,8 +82,13 @@ func (vs *ValidatorSet) IterateValidators(ctx sdk.Context, fn func(index int64, } } -// IterateValidatorsBonded implements sdk.ValidatorSet -func (vs *ValidatorSet) IterateValidatorsBonded(ctx sdk.Context, fn func(index int64, Validator sdk.Validator) bool) { +// IterateBondedValidatorsByPower implements sdk.ValidatorSet +func (vs *ValidatorSet) IterateBondedValidatorsByPower(ctx sdk.Context, fn func(index int64, Validator sdk.Validator) bool) { + vs.IterateValidators(ctx, fn) +} + +// IterateLastValidators implements sdk.ValidatorSet +func (vs *ValidatorSet) IterateLastValidators(ctx sdk.Context, fn func(index int64, Validator sdk.Validator) bool) { vs.IterateValidators(ctx, fn) } diff --git a/scripts/multisim.sh b/scripts/multisim.sh index 8ffa338b84e5..ff5784e4bf0c 100755 --- a/scripts/multisim.sh +++ b/scripts/multisim.sh @@ -1,6 +1,7 @@ #!/bin/bash -seeds=(1 2 4 7 9 20 32 123 124 582 1893 2989 3012 4728 37827 981928 87821 891823782 989182 89182391) +seeds=(1 2 4 7 9 20 32 123 124 582 1893 2989 3012 4728 37827 981928 87821 891823782 989182 89182391 \ +11 22 44 77 99 2020 3232 123123 124124 582582 18931893 29892989 30123012 47284728 37827) blocks=$1 echo "Running multi-seed simulation with seeds ${seeds[@]}" diff --git a/types/stake.go b/types/stake.go index f5d3a4aae0ae..7b10b17f87d5 100644 --- a/types/stake.go +++ b/types/stake.go @@ -64,7 +64,11 @@ type ValidatorSet interface { func(index int64, validator Validator) (stop bool)) // iterate through bonded validators by operator address, execute func for each validator - IterateValidatorsBonded(Context, + IterateBondedValidatorsByPower(Context, + func(index int64, validator Validator) (stop bool)) + + // iterate through the consensus validator set of the last block by operator address, execute func for each validator + IterateLastValidators(Context, func(index int64, validator Validator) (stop bool)) Validator(Context, ValAddress) Validator // get a particular validator by operator address diff --git a/x/gov/tally.go b/x/gov/tally.go index 2bfaa7dd550c..e839bbe039c0 100644 --- a/x/gov/tally.go +++ b/x/gov/tally.go @@ -23,7 +23,7 @@ func tally(ctx sdk.Context, keeper Keeper, proposal Proposal) (passes bool, tall totalVotingPower := sdk.ZeroDec() currValidators := make(map[string]validatorGovInfo) - keeper.vs.IterateValidatorsBonded(ctx, func(index int64, validator sdk.Validator) (stop bool) { + keeper.vs.IterateBondedValidatorsByPower(ctx, func(index int64, validator sdk.Validator) (stop bool) { currValidators[validator.GetOperator().String()] = validatorGovInfo{ Address: validator.GetOperator(), Power: validator.GetPower(), diff --git a/x/mock/simulation/constants.go b/x/mock/simulation/constants.go deleted file mode 100644 index 2c4543b72dac..000000000000 --- a/x/mock/simulation/constants.go +++ /dev/null @@ -1,37 +0,0 @@ -package simulation - -const ( - // Fraction of double-signing evidence from a past height - pastEvidenceFraction float64 = 0.5 - - // Minimum time per block - minTimePerBlock int64 = 1000 / 2 - - // Maximum time per block - maxTimePerBlock int64 = 1000 - - // Number of keys - numKeys int = 250 - - // Chance that double-signing evidence is found on a given block - evidenceFraction float64 = 0.5 - - // 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}, - }) - // 3 states: rand in range [0, 4*provided blocksize], rand in range [0, 2 * provided blocksize], 0 - blockSizeTransitionMatrix, _ = CreateTransitionMatrix([][]int{ - {85, 5, 0}, - {15, 92, 1}, - {0, 3, 99}, - }) -) diff --git a/x/mock/simulation/params.go b/x/mock/simulation/params.go new file mode 100644 index 000000000000..404a85e5441a --- /dev/null +++ b/x/mock/simulation/params.go @@ -0,0 +1,66 @@ +package simulation + +import ( + "math/rand" +) + +const ( + // Minimum time per block + minTimePerBlock int64 = 10000 / 2 + + // Maximum time per block + maxTimePerBlock int64 = 10000 + + // 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. + defaultLivenessTransitionMatrix, _ = CreateTransitionMatrix([][]int{ + {90, 20, 1}, + {10, 50, 5}, + {0, 10, 1000}, + }) + + // 3 states: rand in range [0, 4*provided blocksize], rand in range [0, 2 * provided blocksize], 0 + defaultBlockSizeTransitionMatrix, _ = CreateTransitionMatrix([][]int{ + {85, 5, 0}, + {15, 92, 1}, + {0, 3, 99}, + }) +) + +// Simulation parameters +type Params struct { + PastEvidenceFraction float64 + NumKeys int + EvidenceFraction float64 + InitialLivenessWeightings []int + LivenessTransitionMatrix TransitionMatrix + BlockSizeTransitionMatrix TransitionMatrix +} + +// Return default simulation parameters +func DefaultParams() Params { + return Params{ + PastEvidenceFraction: 0.5, + NumKeys: 250, + EvidenceFraction: 0.5, + InitialLivenessWeightings: []int{40, 5, 5}, + LivenessTransitionMatrix: defaultLivenessTransitionMatrix, + BlockSizeTransitionMatrix: defaultBlockSizeTransitionMatrix, + } +} + +// Return random simulation parameters +func RandomParams(r *rand.Rand) Params { + return Params{ + PastEvidenceFraction: r.Float64(), + NumKeys: r.Intn(250), + EvidenceFraction: r.Float64(), + InitialLivenessWeightings: []int{r.Intn(80), r.Intn(10), r.Intn(10)}, + LivenessTransitionMatrix: defaultLivenessTransitionMatrix, + BlockSizeTransitionMatrix: defaultBlockSizeTransitionMatrix, + } +} diff --git a/x/mock/simulation/random_simulate_blocks.go b/x/mock/simulation/random_simulate_blocks.go index a4da39523e55..5784e7a22306 100644 --- a/x/mock/simulation/random_simulate_blocks.go +++ b/x/mock/simulation/random_simulate_blocks.go @@ -31,13 +31,13 @@ func Simulate(t *testing.T, app *baseapp.BaseApp, return SimulateFromSeed(t, app, appStateFn, time, ops, setups, invariants, numBlocks, blockSize, commit) } -func initChain(r *rand.Rand, accounts []Account, setups []RandSetup, app *baseapp.BaseApp, +func initChain(r *rand.Rand, params Params, accounts []Account, setups []RandSetup, app *baseapp.BaseApp, appStateFn func(r *rand.Rand, accounts []Account) json.RawMessage) (validators map[string]mockValidator) { res := app.InitChain(abci.RequestInitChain{AppStateBytes: appStateFn(r, accounts)}) validators = make(map[string]mockValidator) for _, validator := range res.Validators { str := fmt.Sprintf("%v", validator.PubKey) - validators[str] = mockValidator{validator, GetMemberOfInitialState(r, initialLivenessWeightings)} + validators[str] = mockValidator{validator, GetMemberOfInitialState(r, params.InitialLivenessWeightings)} } for i := 0; i < len(setups); i++ { @@ -65,11 +65,13 @@ func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, testingMode, t, b := getTestingMode(tb) fmt.Printf("Starting SimulateFromSeed with randomness created with seed %d\n", int(seed)) r := rand.New(rand.NewSource(seed)) + params := RandomParams(r) + fmt.Printf("Randomized simulation params: %+v\n", params) timestamp := randTimestamp(r) fmt.Printf("Starting the simulation from time %v, unixtime %v\n", timestamp.UTC().Format(time.UnixDate), timestamp.Unix()) timeDiff := maxTimePerBlock - minTimePerBlock - accs := RandomAccounts(r, numKeys) + accs := RandomAccounts(r, params.NumKeys) // Setup event stats events := make(map[string]uint) @@ -77,7 +79,7 @@ func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, events[what]++ } - validators := initChain(r, accs, setups, app, appStateFn) + validators := initChain(r, params, accs, setups, app, appStateFn) // Second variable to keep pending validator set (delayed one block since TM 0.24) // Initially this is the same as the initial validator set nextValidators := validators @@ -90,7 +92,7 @@ func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) go func() { receivedSignal := <-c - fmt.Printf("Exiting early due to %s, on block %d, operation %d\n", receivedSignal, header.Height, opCount) + fmt.Printf("\nExiting early due to %s, on block %d, operation %d\n", receivedSignal, header.Height, opCount) simError = fmt.Errorf("Exited due to %s", receivedSignal) stopEarly = true }() @@ -98,7 +100,7 @@ func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, var pastTimes []time.Time var pastVoteInfos [][]abci.VoteInfo - request := RandomRequestBeginBlock(r, validators, livenessTransitionMatrix, evidenceFraction, pastTimes, pastVoteInfos, event, header) + request := RandomRequestBeginBlock(r, params, validators, pastTimes, pastVoteInfos, event, header) // These are operations which have been queued by previous operations operationQueue := make(map[int][]Operation) timeOperationQueue := []FutureOperation{} @@ -108,7 +110,7 @@ func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, blockLogBuilders = make([]*strings.Builder, numBlocks) } displayLogs := logPrinter(testingMode, blockLogBuilders) - blockSimulator := createBlockSimulator(testingMode, tb, t, event, invariants, ops, operationQueue, timeOperationQueue, numBlocks, blockSize, displayLogs) + blockSimulator := createBlockSimulator(testingMode, tb, t, params, event, invariants, ops, operationQueue, timeOperationQueue, numBlocks, blockSize, displayLogs) if !testingMode { b.ResetTimer() } else { @@ -181,11 +183,11 @@ func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, } // Generate a random RequestBeginBlock with the current validator set for the next block - request = RandomRequestBeginBlock(r, validators, livenessTransitionMatrix, evidenceFraction, pastTimes, pastVoteInfos, event, header) + request = RandomRequestBeginBlock(r, params, validators, pastTimes, pastVoteInfos, event, header) // Update the validator set, which will be reflected in the application on the next block validators = nextValidators - nextValidators = updateValidators(tb, r, validators, res.ValidatorUpdates, event) + nextValidators = updateValidators(tb, r, params, validators, res.ValidatorUpdates, event) } if stopEarly { DisplayEvents(events) @@ -203,7 +205,7 @@ type blockSimFn func( // Returns a function to simulate blocks. Written like this to avoid constant parameters being passed everytime, to minimize // memory overhead -func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, +func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, params Params, event func(string), invariants []Invariant, ops []WeightedOperation, operationQueue map[int][]Operation, timeOperationQueue []FutureOperation, totalNumBlocks int, avgBlockSize int, displayLogs func()) blockSimFn { @@ -231,7 +233,8 @@ func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []Account, header abci.Header, logWriter func(string)) (opCount int) { - lastBlocksizeState, blocksize = getBlockSize(r, lastBlocksizeState, avgBlockSize) + fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ", header.Height, totalNumBlocks, opCount, blocksize) + lastBlocksizeState, blocksize = getBlockSize(r, params, lastBlocksizeState, avgBlockSize) for j := 0; j < blocksize; j++ { logUpdate, futureOps, err := selectOp(r)(r, app, ctx, accounts, event) logWriter(logUpdate) @@ -272,11 +275,10 @@ func getTestingMode(tb testing.TB) (testingMode bool, t *testing.T, b *testing.B // "over stuffed" blocks with average size of 2 * avgblocksize, // normal sized blocks, hitting avgBlocksize on average, // and empty blocks, with no txs / only txs scheduled from the past. -func getBlockSize(r *rand.Rand, lastBlockSizeState, avgBlockSize int) (state, blocksize int) { - // TODO: Make blockSizeTransitionMatrix non-global +func getBlockSize(r *rand.Rand, params Params, lastBlockSizeState, avgBlockSize int) (state, blocksize int) { // TODO: Make default blocksize transition matrix actually make the average // blocksize equal to avgBlockSize. - state = blockSizeTransitionMatrix.NextState(r, lastBlockSizeState) + state = params.BlockSizeTransitionMatrix.NextState(r, lastBlockSizeState) if state == 0 { blocksize = r.Intn(avgBlockSize * 4) } else if state == 1 { @@ -379,7 +381,7 @@ func randomProposer(r *rand.Rand, validators map[string]mockValidator) common.He // RandomRequestBeginBlock generates a list of signing validators according to the provided list of validators, signing fraction, and evidence fraction // nolint: unparam -func RandomRequestBeginBlock(r *rand.Rand, validators map[string]mockValidator, livenessTransitions TransitionMatrix, evidenceFraction float64, +func RandomRequestBeginBlock(r *rand.Rand, params Params, validators map[string]mockValidator, pastTimes []time.Time, pastVoteInfos [][]abci.VoteInfo, event func(string), header abci.Header) abci.RequestBeginBlock { if len(validators) == 0 { return abci.RequestBeginBlock{Header: header} @@ -388,7 +390,7 @@ func RandomRequestBeginBlock(r *rand.Rand, validators map[string]mockValidator, i := 0 for _, key := range getKeys(validators) { mVal := validators[key] - mVal.livenessState = livenessTransitions.NextState(r, mVal.livenessState) + mVal.livenessState = params.LivenessTransitionMatrix.NextState(r, mVal.livenessState) signed := true if mVal.livenessState == 1 { @@ -422,11 +424,11 @@ func RandomRequestBeginBlock(r *rand.Rand, validators map[string]mockValidator, evidence := make([]abci.Evidence, 0) // Anything but the first block if len(pastTimes) > 0 { - for r.Float64() < evidenceFraction { + for r.Float64() < params.EvidenceFraction { height := header.Height time := header.Time vals := voteInfos - if r.Float64() < pastEvidenceFraction { + if r.Float64() < params.PastEvidenceFraction { height = int64(r.Intn(int(header.Height) - 1)) time = pastTimes[height] vals = pastVoteInfos[height] @@ -457,7 +459,7 @@ func RandomRequestBeginBlock(r *rand.Rand, validators map[string]mockValidator, // updateValidators mimicks Tendermint's update logic // nolint: unparam -func updateValidators(tb testing.TB, r *rand.Rand, current map[string]mockValidator, updates []abci.ValidatorUpdate, event func(string)) map[string]mockValidator { +func updateValidators(tb testing.TB, r *rand.Rand, params Params, current map[string]mockValidator, updates []abci.ValidatorUpdate, event func(string)) map[string]mockValidator { for _, update := range updates { str := fmt.Sprintf("%v", update.PubKey) @@ -476,7 +478,7 @@ func updateValidators(tb testing.TB, r *rand.Rand, current map[string]mockValida event("endblock/validatorupdates/updated") } else { // Set this new validator - current[str] = mockValidator{update, GetMemberOfInitialState(r, initialLivenessWeightings)} + current[str] = mockValidator{update, GetMemberOfInitialState(r, params.InitialLivenessWeightings)} event("endblock/validatorupdates/added") } } diff --git a/x/stake/genesis.go b/x/stake/genesis.go index 28c5973c741b..63b038613de5 100644 --- a/x/stake/genesis.go +++ b/x/stake/genesis.go @@ -72,7 +72,7 @@ func WriteGenesis(ctx sdk.Context, keeper Keeper) types.GenesisState { // WriteValidators returns a slice of bonded genesis validators. func WriteValidators(ctx sdk.Context, keeper Keeper) (vals []tmtypes.GenesisValidator) { - keeper.IterateValidatorsBonded(ctx, func(_ int64, validator sdk.Validator) (stop bool) { + keeper.IterateLastValidators(ctx, func(_ int64, validator sdk.Validator) (stop bool) { vals = append(vals, tmtypes.GenesisValidator{ PubKey: validator.GetConsPubKey(), Power: validator.GetPower().RoundInt64(), diff --git a/x/stake/keeper/sdk_types.go b/x/stake/keeper/sdk_types.go index 4e859a42a887..1dea473f892b 100644 --- a/x/stake/keeper/sdk_types.go +++ b/x/stake/keeper/sdk_types.go @@ -28,9 +28,31 @@ func (k Keeper) IterateValidators(ctx sdk.Context, fn func(index int64, validato } // iterate through the active validator set and perform the provided function -func (k Keeper) IterateValidatorsBonded(ctx sdk.Context, fn func(index int64, validator sdk.Validator) (stop bool)) { +func (k Keeper) IterateBondedValidatorsByPower(ctx sdk.Context, fn func(index int64, validator sdk.Validator) (stop bool)) { store := ctx.KVStore(k.storeKey) - iterator := sdk.KVStorePrefixIterator(store, LastValidatorPowerKey) + maxValidators := k.MaxValidators(ctx) + + iterator := sdk.KVStoreReversePrefixIterator(store, ValidatorsByPowerIndexKey) + defer iterator.Close() + + i := int64(0) + for ; iterator.Valid() && i < int64(maxValidators); iterator.Next() { + address := iterator.Value() + validator := k.mustGetValidator(ctx, address) + + if validator.Status == sdk.Bonded { + stop := fn(i, validator) // XXX is this safe will the validator unexposed fields be able to get written to? + if stop { + break + } + i++ + } + } +} + +// iterate through the active validator set and perform the provided function +func (k Keeper) IterateLastValidators(ctx sdk.Context, fn func(index int64, validator sdk.Validator) (stop bool)) { + iterator := k.LastValidatorsIterator(ctx) i := int64(0) for ; iterator.Valid(); iterator.Next() { address := AddressFromLastValidatorPowerKey(iterator.Key()) diff --git a/x/stake/keeper/validator.go b/x/stake/keeper/validator.go index 5770ada2ff8a..edf781bb5771 100644 --- a/x/stake/keeper/validator.go +++ b/x/stake/keeper/validator.go @@ -261,6 +261,13 @@ func (k Keeper) GetLastValidators(ctx sdk.Context) (validators []types.Validator return validators[:i] // trim } +// returns an iterator for the consensus validators in the last block +func (k Keeper) LastValidatorsIterator(ctx sdk.Context) (iterator sdk.Iterator) { + store := ctx.KVStore(k.storeKey) + iterator = sdk.KVStorePrefixIterator(store, LastValidatorPowerKey) + return iterator +} + // get the current group of bonded validators sorted by power-rank func (k Keeper) GetBondedValidatorsByPower(ctx sdk.Context) []types.Validator { store := ctx.KVStore(k.storeKey) @@ -283,6 +290,13 @@ func (k Keeper) GetBondedValidatorsByPower(ctx sdk.Context) []types.Validator { return validators[:i] // trim } +// returns an iterator for the current validator power store +func (k Keeper) ValidatorsPowerStoreIterator(ctx sdk.Context) (iterator sdk.Iterator) { + store := ctx.KVStore(k.storeKey) + iterator = sdk.KVStoreReversePrefixIterator(store, ValidatorsByPowerIndexKey) + return iterator +} + // gets a specific validator queue timeslice. A timeslice is a slice of ValAddresses corresponding to unbonding validators // that expire at a certain time. func (k Keeper) GetValidatorQueueTimeSlice(ctx sdk.Context, timestamp time.Time) (valAddrs []sdk.ValAddress) { diff --git a/x/stake/simulation/invariants.go b/x/stake/simulation/invariants.go index 8e6f18432112..819bc8b3799d 100644 --- a/x/stake/simulation/invariants.go +++ b/x/stake/simulation/invariants.go @@ -1,6 +1,7 @@ package simulation import ( + "bytes" "fmt" "github.com/cosmos/cosmos-sdk/baseapp" @@ -10,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/distribution" "github.com/cosmos/cosmos-sdk/x/mock/simulation" "github.com/cosmos/cosmos-sdk/x/stake" + "github.com/cosmos/cosmos-sdk/x/stake/keeper" abci "github.com/tendermint/tendermint/abci/types" ) @@ -24,10 +26,12 @@ func AllInvariants(ck bank.Keeper, k stake.Keeper, if err != nil { return err } + err = PositivePowerInvariant(k)(app) if err != nil { return err } + err = ValidatorSetInvariant(k)(app) return err } @@ -100,19 +104,29 @@ func SupplyInvariants(ck bank.Keeper, k stake.Keeper, } } -// PositivePowerInvariant checks that all stored validators have > 0 power +// PositivePowerInvariant checks that all stored validators have > 0 power. func PositivePowerInvariant(k stake.Keeper) simulation.Invariant { return func(app *baseapp.BaseApp) error { ctx := app.NewContext(false, abci.Header{}) - var err error - k.IterateValidatorsBonded(ctx, func(_ int64, validator sdk.Validator) bool { - if !validator.GetPower().GT(sdk.ZeroDec()) { - err = fmt.Errorf("validator with non-positive power stored. (pubkey %v)", validator.GetConsPubKey()) - return true + + iterator := k.ValidatorsPowerStoreIterator(ctx) + pool := k.GetPool(ctx) + + for ; iterator.Valid(); iterator.Next() { + validator, found := k.GetValidator(ctx, iterator.Value()) + if !found { + panic(fmt.Sprintf("validator record not found for address: %X\n", iterator.Value())) } - return false - }) - return err + + powerKey := keeper.GetValidatorsByPowerIndexKey(validator, pool) + + if !bytes.Equal(iterator.Key(), powerKey) { + return fmt.Errorf("power store invariance:\n\tvalidator.Power: %v"+ + "\n\tkey should be: %v\n\tkey in store: %v", validator.GetPower(), powerKey, iterator.Key()) + } + } + iterator.Close() + return nil } }