From 855222e8c343fa10e2ff46cf968614ae2e493625 Mon Sep 17 00:00:00 2001 From: ValarDragon Date: Mon, 27 Aug 2018 14:27:00 -0700 Subject: [PATCH] simulation: Allow operations to specify future operations The intent of this is to allow for simulating things like slashing for not voting on a governance proposal. To test this, you would queue all the validator votes in future blocks, and keep track of which ones you didn't slash. Then you could add queue a "check governance slashing operation" after the voting period is over. --- PENDING.md | 1 + cmd/gaia/app/sim_test.go | 6 +-- x/bank/simulation/msgs.go | 12 +++--- x/bank/simulation/sim_test.go | 2 +- x/gov/simulation/msgs.go | 19 +++++----- x/mock/simulation/random_simulate_blocks.go | 42 ++++++++++++++++++++- x/mock/simulation/types.go | 14 ++++++- x/slashing/simulation/msgs.go | 4 +- x/stake/simulation/msgs.go | 36 +++++++++--------- 9 files changed, 94 insertions(+), 42 deletions(-) diff --git a/PENDING.md b/PENDING.md index cee2ae14c..f413a1800 100644 --- a/PENDING.md +++ b/PENDING.md @@ -41,6 +41,7 @@ FEATURES * SDK * [querier] added custom querier functionality, so ABCI query requests can be handled by keepers + * [simulation] \#1924 allow operations to specify future operations * Tendermint diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index 9036c7e14..0ca936bdb 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -3,7 +3,6 @@ package app import ( "encoding/json" "flag" - "fmt" "math/rand" "testing" @@ -87,7 +86,7 @@ func appStateFn(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json func testAndRunTxs(app *GaiaApp) []simulation.Operation { return []simulation.Operation{ - banksim.TestAndRunSingleInputMsgSend(app.accountMapper), + banksim.SimulateSingleInputMsgSend(app.accountMapper), govsim.SimulateMsgSubmitProposal(app.govKeeper, app.stakeKeeper), govsim.SimulateMsgDeposit(app.govKeeper, app.stakeKeeper), govsim.SimulateMsgVote(app.govKeeper, app.stakeKeeper), @@ -171,11 +170,10 @@ func TestAppStateDeterminism(t *testing.T) { true, ) appHash := app.LastCommitID().Hash - fmt.Printf(">>> APP HASH: %v, %X\n", appHash, appHash) appHashList[j] = appHash } for k := 1; k < numTimesToRunPerSeed; k++ { - require.Equal(t, appHashList[0], appHashList[k]) + require.Equal(t, appHashList[0], appHashList[k], "appHash list: %v", appHashList) } } } diff --git a/x/bank/simulation/msgs.go b/x/bank/simulation/msgs.go index 633f6787f..e3f3ab77b 100644 --- a/x/bank/simulation/msgs.go +++ b/x/bank/simulation/msgs.go @@ -18,10 +18,10 @@ import ( "github.com/tendermint/tendermint/crypto" ) -// TestAndRunSingleInputMsgSend tests and runs a single msg send, with one input and one output, where both +// SimulateSingleInputMsgSend tests and runs a single msg send, with one input and one output, where both // accounts already exist. -func TestAndRunSingleInputMsgSend(mapper auth.AccountMapper) simulation.Operation { - 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) { +func SimulateSingleInputMsgSend(mapper auth.AccountMapper) simulation.Operation { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOps []simulation.FutureOperation, err sdk.Error) { fromKey := simulation.RandomKey(r, keys) fromAddr := sdk.AccAddress(fromKey.PubKey().Address()) toKey := simulation.RandomKey(r, keys) @@ -36,13 +36,13 @@ func TestAndRunSingleInputMsgSend(mapper auth.AccountMapper) simulation.Operatio initFromCoins := mapper.GetAccount(ctx, fromAddr).GetCoins() if len(initFromCoins) == 0 { - return "skipping, no coins at all", nil + return "skipping, no coins at all", nil, nil } 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 + return "skipping bank send due to account having no coins of denomination " + initFromCoins[denomIndex].Denom, nil, nil } action = fmt.Sprintf("%s is sending %s %s to %s", @@ -61,7 +61,7 @@ func TestAndRunSingleInputMsgSend(mapper auth.AccountMapper) simulation.Operatio sendAndVerifyMsgSend(t, app, mapper, msg, ctx, log, []crypto.PrivKey{fromKey}) event("bank/sendAndVerifyMsgSend/ok") - return action, nil + return action, nil, nil } } diff --git a/x/bank/simulation/sim_test.go b/x/bank/simulation/sim_test.go index 88e7a5f3a..f61f72b4f 100644 --- a/x/bank/simulation/sim_test.go +++ b/x/bank/simulation/sim_test.go @@ -34,7 +34,7 @@ func TestBankWithRandomMessages(t *testing.T) { simulation.Simulate( t, mapp.BaseApp, appStateFn, []simulation.Operation{ - TestAndRunSingleInputMsgSend(mapper), + SimulateSingleInputMsgSend(mapper), }, []simulation.RandSetup{}, []simulation.Invariant{ diff --git a/x/gov/simulation/msgs.go b/x/gov/simulation/msgs.go index f270d3a7d..0b1530138 100644 --- a/x/gov/simulation/msgs.go +++ b/x/gov/simulation/msgs.go @@ -20,9 +20,10 @@ const ( denom = "steak" ) -// SimulateMsgSubmitProposal +// SimulateMsgSubmitProposal simulates a msg Submit Proposal +// Note: Currently doesn't ensure that the proposal txt is in JSON form func SimulateMsgSubmitProposal(k gov.Keeper, sk stake.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOps []simulation.FutureOperation, err sdk.Error) { key := simulation.RandomKey(r, keys) addr := sdk.AccAddress(key.PubKey().Address()) deposit := randomDeposit(r) @@ -45,18 +46,18 @@ func SimulateMsgSubmitProposal(k gov.Keeper, sk stake.Keeper) simulation.Operati } event(fmt.Sprintf("gov/MsgSubmitProposal/%v", result.IsOK())) action = fmt.Sprintf("TestMsgSubmitProposal: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) - return action, nil + return action, nil, nil } } // SimulateMsgDeposit func SimulateMsgDeposit(k gov.Keeper, sk stake.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, 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 + return "no-operation", nil, nil } deposit := randomDeposit(r) msg := gov.NewMsgDeposit(addr, proposalID, deposit) @@ -72,18 +73,18 @@ func SimulateMsgDeposit(k gov.Keeper, sk stake.Keeper) simulation.Operation { } event(fmt.Sprintf("gov/MsgDeposit/%v", result.IsOK())) action = fmt.Sprintf("TestMsgDeposit: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) - return action, nil + return action, nil, nil } } // SimulateMsgVote func SimulateMsgVote(k gov.Keeper, sk stake.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, 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 + return "no-operation", nil, nil } option := randomVotingOption(r) msg := gov.NewMsgVote(addr, proposalID, option) @@ -95,7 +96,7 @@ func SimulateMsgVote(k gov.Keeper, sk stake.Keeper) simulation.Operation { } event(fmt.Sprintf("gov/MsgVote/%v", result.IsOK())) action = fmt.Sprintf("TestMsgVote: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) - return action, nil + return action, nil, nil } } diff --git a/x/mock/simulation/random_simulate_blocks.go b/x/mock/simulation/random_simulate_blocks.go index 5b1d4030c..995013ef8 100644 --- a/x/mock/simulation/random_simulate_blocks.go +++ b/x/mock/simulation/random_simulate_blocks.go @@ -70,6 +70,8 @@ func SimulateFromSeed( request := abci.RequestBeginBlock{Header: header} var pastTimes []time.Time + // These are operations which have been queued by previous operations + operationQueue := make(map[int][]Operation) for i := 0; i < numBlocks; i++ { @@ -96,9 +98,14 @@ func SimulateFromSeed( default: thisBlockSize = r.Intn(blockSize * 4) } + // Run queued operations. Ignores blocksize if blocksize is too small + log, numQueuedOpsRan := runQueuedOperations(operationQueue, int(header.Height), t, r, app, ctx, keys, log, event) + opCount += numQueuedOpsRan + thisBlockSize -= numQueuedOpsRan for j := 0; j < thisBlockSize; j++ { - logUpdate, err := ops[r.Intn(len(ops))](t, r, app, ctx, keys, log, event) + logUpdate, futureOps, err := ops[r.Intn(len(ops))](t, r, app, ctx, keys, log, event) log += "\n" + logUpdate + queueOperations(operationQueue, futureOps) require.Nil(t, err, log) if onOperation { @@ -134,6 +141,39 @@ func SimulateFromSeed( DisplayEvents(events) } +// adds all future operations into the operation queue. +func queueOperations(queuedOperations map[int][]Operation, futureOperations []FutureOperation) { + if futureOperations == nil { + return + } + for _, futureOp := range futureOperations { + if val, ok := queuedOperations[futureOp.BlockHeight]; ok { + queuedOperations[futureOp.BlockHeight] = append(val, futureOp.Op) + } else { + queuedOperations[futureOp.BlockHeight] = []Operation{futureOp.Op} + } + } +} + +func runQueuedOperations(queueOperations map[int][]Operation, height int, t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + privKeys []crypto.PrivKey, log string, event func(string)) (updatedLog string, numOpsRan int) { + updatedLog = log + if queuedOps, ok := queueOperations[height]; ok { + numOps := len(queuedOps) + for i := 0; i < numOps; i++ { + // For now, queued operations cannot queue more operations. + // If a need arises for us to support queued messages to queue more messages, this can + // be changed. + logUpdate, _, err := queuedOps[i](t, r, app, ctx, privKeys, updatedLog, event) + updatedLog += "\n" + logUpdate + require.Nil(t, err, updatedLog) + } + delete(queueOperations, height) + return updatedLog, numOps + } + return log, 0 +} + func getKeys(validators map[string]mockValidator) []string { keys := make([]string, len(validators)) i := 0 diff --git a/x/mock/simulation/types.go b/x/mock/simulation/types.go index 3ece3c2e9..2516e07ae 100644 --- a/x/mock/simulation/types.go +++ b/x/mock/simulation/types.go @@ -19,10 +19,13 @@ type ( // For ease of debugging, // an operation returns a descriptive message "action", // which details what this fuzzed state machine transition actually did. + // + // Operations can optionally provide a list of "FutureOperations" to run later + // These will be ran at the beginning of the corresponding block. Operation 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) + ) (action string, futureOperations []FutureOperation, err sdk.Error) // RandSetup performs the random setup the mock module needs. RandSetup func(r *rand.Rand, privKeys []crypto.PrivKey) @@ -36,6 +39,15 @@ type ( val abci.Validator livenessState int } + + // FutureOperation is an operation which will be ran at the + // beginning of the provided BlockHeight. + // In the (likely) event that multiple operations are queued at the same + // block height, they will execute in a FIFO pattern. + FutureOperation struct { + BlockHeight int + Op Operation + } ) // PeriodicInvariant returns an Invariant function closure that asserts diff --git a/x/slashing/simulation/msgs.go b/x/slashing/simulation/msgs.go index e694bd1d7..e4900889e 100644 --- a/x/slashing/simulation/msgs.go +++ b/x/slashing/simulation/msgs.go @@ -17,7 +17,7 @@ import ( // SimulateMsgUnjail func SimulateMsgUnjail(k slashing.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { key := simulation.RandomKey(r, keys) address := sdk.AccAddress(key.PubKey().Address()) msg := slashing.NewMsgUnjail(address) @@ -29,6 +29,6 @@ func SimulateMsgUnjail(k slashing.Keeper) simulation.Operation { } event(fmt.Sprintf("slashing/MsgUnjail/%v", result.IsOK())) action = fmt.Sprintf("TestMsgUnjail: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) - return action, nil + return action, nil, nil } } diff --git a/x/stake/simulation/msgs.go b/x/stake/simulation/msgs.go index 995e4f358..4280e70c1 100644 --- a/x/stake/simulation/msgs.go +++ b/x/stake/simulation/msgs.go @@ -19,7 +19,7 @@ import ( // SimulateMsgCreateValidator func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { denom := k.GetParams(ctx).BondDenom description := stake.Description{ Moniker: simulation.RandStringOfLength(r, 10), @@ -32,7 +32,7 @@ func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) simulation amount = simulation.RandomAmount(r, amount) } if amount.Equal(sdk.ZeroInt()) { - return "no-operation", nil + return "no-operation", nil, nil } msg := stake.MsgCreateValidator{ Description: description, @@ -50,13 +50,13 @@ func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) simulation 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 + return action, nil, nil } } // SimulateMsgEditValidator func SimulateMsgEditValidator(k stake.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { description := stake.Description{ Moniker: simulation.RandStringOfLength(r, 10), Identity: simulation.RandStringOfLength(r, 10), @@ -78,13 +78,13 @@ func SimulateMsgEditValidator(k stake.Keeper) simulation.Operation { } event(fmt.Sprintf("stake/MsgEditValidator/%v", result.IsOK())) action = fmt.Sprintf("TestMsgEditValidator: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) - return action, nil + return action, nil, nil } } // SimulateMsgDelegate func SimulateMsgDelegate(m auth.AccountMapper, k stake.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { denom := k.GetParams(ctx).BondDenom validatorKey := simulation.RandomKey(r, keys) validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address()) @@ -95,7 +95,7 @@ func SimulateMsgDelegate(m auth.AccountMapper, k stake.Keeper) simulation.Operat amount = simulation.RandomAmount(r, amount) } if amount.Equal(sdk.ZeroInt()) { - return "no-operation", nil + return "no-operation", nil, nil } msg := stake.MsgDelegate{ DelegatorAddr: delegatorAddress, @@ -110,13 +110,13 @@ func SimulateMsgDelegate(m auth.AccountMapper, k stake.Keeper) simulation.Operat } event(fmt.Sprintf("stake/MsgDelegate/%v", result.IsOK())) action = fmt.Sprintf("TestMsgDelegate: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) - return action, nil + return action, nil, nil } } // SimulateMsgBeginUnbonding func SimulateMsgBeginUnbonding(m auth.AccountMapper, k stake.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { denom := k.GetParams(ctx).BondDenom validatorKey := simulation.RandomKey(r, keys) validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address()) @@ -127,7 +127,7 @@ func SimulateMsgBeginUnbonding(m auth.AccountMapper, k stake.Keeper) simulation. amount = simulation.RandomAmount(r, amount) } if amount.Equal(sdk.ZeroInt()) { - return "no-operation", nil + return "no-operation", nil, nil } msg := stake.MsgBeginUnbonding{ DelegatorAddr: delegatorAddress, @@ -142,13 +142,13 @@ func SimulateMsgBeginUnbonding(m auth.AccountMapper, k stake.Keeper) simulation. } event(fmt.Sprintf("stake/MsgBeginUnbonding/%v", result.IsOK())) action = fmt.Sprintf("TestMsgBeginUnbonding: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) - return action, nil + return action, nil, nil } } // SimulateMsgCompleteUnbonding func SimulateMsgCompleteUnbonding(k stake.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { validatorKey := simulation.RandomKey(r, keys) validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address()) delegatorKey := simulation.RandomKey(r, keys) @@ -165,13 +165,13 @@ func SimulateMsgCompleteUnbonding(k stake.Keeper) simulation.Operation { } event(fmt.Sprintf("stake/MsgCompleteUnbonding/%v", result.IsOK())) action = fmt.Sprintf("TestMsgCompleteUnbonding: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) - return action, nil + return action, nil, nil } } // SimulateMsgBeginRedelegate func SimulateMsgBeginRedelegate(m auth.AccountMapper, k stake.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { denom := k.GetParams(ctx).BondDenom sourceValidatorKey := simulation.RandomKey(r, keys) sourceValidatorAddress := sdk.AccAddress(sourceValidatorKey.PubKey().Address()) @@ -185,7 +185,7 @@ func SimulateMsgBeginRedelegate(m auth.AccountMapper, k stake.Keeper) simulation amount = simulation.RandomAmount(r, amount) } if amount.Equal(sdk.ZeroInt()) { - return "no-operation", nil + return "no-operation", nil, nil } msg := stake.MsgBeginRedelegate{ DelegatorAddr: delegatorAddress, @@ -201,13 +201,13 @@ func SimulateMsgBeginRedelegate(m auth.AccountMapper, k stake.Keeper) simulation } event(fmt.Sprintf("stake/MsgBeginRedelegate/%v", result.IsOK())) action = fmt.Sprintf("TestMsgBeginRedelegate: %s", msg.GetSignBytes()) - return action, nil + return action, nil, nil } } // SimulateMsgCompleteRedelegate func SimulateMsgCompleteRedelegate(k stake.Keeper) simulation.Operation { - 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) { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { validatorSrcKey := simulation.RandomKey(r, keys) validatorSrcAddress := sdk.AccAddress(validatorSrcKey.PubKey().Address()) validatorDstKey := simulation.RandomKey(r, keys) @@ -227,7 +227,7 @@ func SimulateMsgCompleteRedelegate(k stake.Keeper) simulation.Operation { } event(fmt.Sprintf("stake/MsgCompleteRedelegate/%v", result.IsOK())) action = fmt.Sprintf("TestMsgCompleteRedelegate: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) - return action, nil + return action, nil, nil } }