github.com/Finschia/finschia-sdk@v0.49.1/x/gov/simulation/operations.go (about) 1 package simulation 2 3 import ( 4 "math" 5 "math/rand" 6 "time" 7 8 "github.com/Finschia/finschia-sdk/baseapp" 9 "github.com/Finschia/finschia-sdk/codec" 10 "github.com/Finschia/finschia-sdk/simapp/helpers" 11 simappparams "github.com/Finschia/finschia-sdk/simapp/params" 12 sdk "github.com/Finschia/finschia-sdk/types" 13 simtypes "github.com/Finschia/finschia-sdk/types/simulation" 14 "github.com/Finschia/finschia-sdk/x/gov/keeper" 15 "github.com/Finschia/finschia-sdk/x/gov/types" 16 "github.com/Finschia/finschia-sdk/x/simulation" 17 ) 18 19 var initialProposalID = uint64(100000000000000) 20 21 // Simulation operation weights constants 22 const ( 23 OpWeightMsgDeposit = "op_weight_msg_deposit" 24 OpWeightMsgVote = "op_weight_msg_vote" 25 OpWeightMsgVoteWeighted = "op_weight_msg_weighted_vote" 26 ) 27 28 // WeightedOperations returns all the operations from the module with their respective weights 29 func WeightedOperations( 30 appParams simtypes.AppParams, cdc codec.JSONCodec, ak types.AccountKeeper, 31 bk types.BankKeeper, k keeper.Keeper, wContents []simtypes.WeightedProposalContent, 32 ) simulation.WeightedOperations { 33 var ( 34 weightMsgDeposit int 35 weightMsgVote int 36 weightMsgVoteWeighted int 37 ) 38 39 appParams.GetOrGenerate(cdc, OpWeightMsgDeposit, &weightMsgDeposit, nil, 40 func(_ *rand.Rand) { 41 weightMsgDeposit = simappparams.DefaultWeightMsgDeposit 42 }, 43 ) 44 45 appParams.GetOrGenerate(cdc, OpWeightMsgVote, &weightMsgVote, nil, 46 func(_ *rand.Rand) { 47 weightMsgVote = simappparams.DefaultWeightMsgVote 48 }, 49 ) 50 51 appParams.GetOrGenerate(cdc, OpWeightMsgVoteWeighted, &weightMsgVoteWeighted, nil, 52 func(_ *rand.Rand) { 53 weightMsgVoteWeighted = simappparams.DefaultWeightMsgVoteWeighted 54 }, 55 ) 56 57 // generate the weighted operations for the proposal contents 58 var wProposalOps simulation.WeightedOperations 59 60 for _, wContent := range wContents { 61 var weight int 62 appParams.GetOrGenerate(cdc, wContent.AppParamsKey(), &weight, nil, 63 func(_ *rand.Rand) { weight = wContent.DefaultWeight() }) 64 65 wProposalOps = append( 66 wProposalOps, 67 simulation.NewWeightedOperation( 68 weight, 69 SimulateMsgSubmitProposal(ak, bk, k, wContent.ContentSimulatorFn()), 70 ), 71 ) 72 } 73 74 wGovOps := simulation.WeightedOperations{ 75 simulation.NewWeightedOperation( 76 weightMsgDeposit, 77 SimulateMsgDeposit(ak, bk, k), 78 ), 79 simulation.NewWeightedOperation( 80 weightMsgVote, 81 SimulateMsgVote(ak, bk, k), 82 ), 83 simulation.NewWeightedOperation( 84 weightMsgVoteWeighted, 85 SimulateMsgVoteWeighted(ak, bk, k), 86 ), 87 } 88 89 return append(wProposalOps, wGovOps...) 90 } 91 92 // SimulateMsgSubmitProposal simulates creating a msg Submit Proposal 93 // voting on the proposal, and subsequently slashing the proposal. It is implemented using 94 // future operations. 95 func SimulateMsgSubmitProposal( 96 ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, contentSim simtypes.ContentSimulatorFn, 97 ) simtypes.Operation { 98 // The states are: 99 // column 1: All validators vote 100 // column 2: 90% vote 101 // column 3: 75% vote 102 // column 4: 40% vote 103 // column 5: 15% vote 104 // column 6: noone votes 105 // All columns sum to 100 for simplicity, values chosen by @valardragon semi-arbitrarily, 106 // feel free to change. 107 numVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ 108 {20, 10, 0, 0, 0, 0}, 109 {55, 50, 20, 10, 0, 0}, 110 {25, 25, 30, 25, 30, 15}, 111 {0, 15, 30, 25, 30, 30}, 112 {0, 0, 20, 30, 30, 30}, 113 {0, 0, 0, 10, 10, 25}, 114 }) 115 116 statePercentageArray := []float64{1, .9, .75, .4, .15, 0} 117 curNumVotesState := 1 118 119 return func( 120 r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, 121 accs []simtypes.Account, chainID string, 122 ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { 123 // 1) submit proposal now 124 content := contentSim(r, ctx, accs) 125 if content == nil { 126 return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSubmitProposal, "content is nil"), nil, nil 127 } 128 129 simAccount, _ := simtypes.RandomAcc(r, accs) 130 deposit, skip, err := randomDeposit(r, ctx, ak, bk, k, simAccount.Address) 131 switch { 132 case skip: 133 return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSubmitProposal, "skip deposit"), nil, nil 134 case err != nil: 135 return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgSubmitProposal, "unable to generate deposit"), nil, err 136 } 137 138 msg, err := types.NewMsgSubmitProposal(content, deposit, simAccount.Address) 139 if err != nil { 140 return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate a submit proposal msg"), nil, err 141 } 142 143 account := ak.GetAccount(ctx, simAccount.Address) 144 spendable := bk.SpendableCoins(ctx, account.GetAddress()) 145 146 var fees sdk.Coins 147 coins, hasNeg := spendable.SafeSub(deposit) 148 if !hasNeg { 149 fees, err = simtypes.RandomFees(r, ctx, coins) 150 if err != nil { 151 return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate fees"), nil, err 152 } 153 } 154 155 txGen := simappparams.MakeTestEncodingConfig().TxConfig 156 tx, err := helpers.GenSignedMockTx( 157 r, 158 txGen, 159 []sdk.Msg{msg}, 160 fees, 161 helpers.DefaultGenTxGas, 162 chainID, 163 []uint64{account.GetAccountNumber()}, 164 []uint64{account.GetSequence()}, 165 simAccount.PrivKey, 166 ) 167 if err != nil { 168 return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err 169 } 170 171 _, _, err = app.Deliver(txGen.TxEncoder(), tx) 172 if err != nil { 173 return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err 174 } 175 176 opMsg := simtypes.NewOperationMsg(msg, true, "", nil) 177 178 // get the submitted proposal ID 179 proposalID, err := k.GetProposalID(ctx) 180 if err != nil { 181 return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate proposalID"), nil, err 182 } 183 184 // 2) Schedule operations for votes 185 // 2.1) first pick a number of people to vote. 186 curNumVotesState = numVotesTransitionMatrix.NextState(r, curNumVotesState) 187 numVotes := int(math.Ceil(float64(len(accs)) * statePercentageArray[curNumVotesState])) 188 189 // 2.2) select who votes and when 190 whoVotes := r.Perm(len(accs)) 191 192 // didntVote := whoVotes[numVotes:] 193 whoVotes = whoVotes[:numVotes] 194 votingPeriod := k.GetVotingParams(ctx).VotingPeriod 195 196 fops := make([]simtypes.FutureOperation, numVotes+1) 197 for i := 0; i < numVotes; i++ { 198 whenVote := ctx.BlockHeader().Time.Add(time.Duration(r.Int63n(int64(votingPeriod.Seconds()))) * time.Second) 199 fops[i] = simtypes.FutureOperation{ 200 BlockTime: whenVote, 201 Op: operationSimulateMsgVote(ak, bk, k, accs[whoVotes[i]], int64(proposalID)), 202 } 203 } 204 205 return opMsg, fops, nil 206 } 207 } 208 209 // SimulateMsgDeposit generates a MsgDeposit with random values. 210 func SimulateMsgDeposit(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation { 211 return func( 212 r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, 213 accs []simtypes.Account, chainID string, 214 ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { 215 simAccount, _ := simtypes.RandomAcc(r, accs) 216 proposalID, ok := randomProposalID(r, k, ctx, types.StatusDepositPeriod) 217 if !ok { 218 return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDeposit, "unable to generate proposalID"), nil, nil 219 } 220 221 deposit, skip, err := randomDeposit(r, ctx, ak, bk, k, simAccount.Address) 222 switch { 223 case skip: 224 return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDeposit, "skip deposit"), nil, nil 225 case err != nil: 226 return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDeposit, "unable to generate deposit"), nil, err 227 } 228 229 msg := types.NewMsgDeposit(simAccount.Address, proposalID, deposit) 230 231 account := ak.GetAccount(ctx, simAccount.Address) 232 spendable := bk.SpendableCoins(ctx, account.GetAddress()) 233 234 var fees sdk.Coins 235 coins, hasNeg := spendable.SafeSub(deposit) 236 if !hasNeg { 237 fees, err = simtypes.RandomFees(r, ctx, coins) 238 if err != nil { 239 return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate fees"), nil, err 240 } 241 } 242 243 txCtx := simulation.OperationInput{ 244 R: r, 245 App: app, 246 TxGen: simappparams.MakeTestEncodingConfig().TxConfig, 247 Cdc: nil, 248 Msg: msg, 249 MsgType: msg.Type(), 250 Context: ctx, 251 SimAccount: simAccount, 252 AccountKeeper: ak, 253 ModuleName: types.ModuleName, 254 } 255 256 return simulation.GenAndDeliverTx(txCtx, fees) 257 } 258 } 259 260 // SimulateMsgVote generates a MsgVote with random values. 261 func SimulateMsgVote(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation { 262 return operationSimulateMsgVote(ak, bk, k, simtypes.Account{}, -1) 263 } 264 265 func operationSimulateMsgVote(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, 266 simAccount simtypes.Account, proposalIDInt int64, 267 ) simtypes.Operation { 268 return func( 269 r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, 270 accs []simtypes.Account, chainID string, 271 ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { 272 if simAccount.Equals(simtypes.Account{}) { 273 simAccount, _ = simtypes.RandomAcc(r, accs) 274 } 275 276 var proposalID uint64 277 278 switch { 279 case proposalIDInt < 0: 280 var ok bool 281 proposalID, ok = randomProposalID(r, k, ctx, types.StatusVotingPeriod) 282 if !ok { 283 return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgVote, "unable to generate proposalID"), nil, nil 284 } 285 default: 286 proposalID = uint64(proposalIDInt) 287 } 288 289 option := randomVotingOption(r) 290 msg := types.NewMsgVote(simAccount.Address, proposalID, option) 291 292 account := ak.GetAccount(ctx, simAccount.Address) 293 spendable := bk.SpendableCoins(ctx, account.GetAddress()) 294 295 txCtx := simulation.OperationInput{ 296 R: r, 297 App: app, 298 TxGen: simappparams.MakeTestEncodingConfig().TxConfig, 299 Cdc: nil, 300 Msg: msg, 301 MsgType: msg.Type(), 302 Context: ctx, 303 SimAccount: simAccount, 304 AccountKeeper: ak, 305 Bankkeeper: bk, 306 ModuleName: types.ModuleName, 307 CoinsSpentInMsg: spendable, 308 } 309 310 return simulation.GenAndDeliverTxWithRandFees(txCtx) 311 } 312 } 313 314 // SimulateMsgVoteWeighted generates a MsgVoteWeighted with random values. 315 func SimulateMsgVoteWeighted(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation { 316 return operationSimulateMsgVoteWeighted(ak, bk, k, simtypes.Account{}, -1) 317 } 318 319 func operationSimulateMsgVoteWeighted(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, 320 simAccount simtypes.Account, proposalIDInt int64, 321 ) simtypes.Operation { 322 return func( 323 r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, 324 accs []simtypes.Account, chainID string, 325 ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { 326 if simAccount.Equals(simtypes.Account{}) { 327 simAccount, _ = simtypes.RandomAcc(r, accs) 328 } 329 330 var proposalID uint64 331 332 switch { 333 case proposalIDInt < 0: 334 var ok bool 335 proposalID, ok = randomProposalID(r, k, ctx, types.StatusVotingPeriod) 336 if !ok { 337 return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgVoteWeighted, "unable to generate proposalID"), nil, nil 338 } 339 default: 340 proposalID = uint64(proposalIDInt) 341 } 342 343 options := randomWeightedVotingOptions(r) 344 msg := types.NewMsgVoteWeighted(simAccount.Address, proposalID, options) 345 346 account := ak.GetAccount(ctx, simAccount.Address) 347 spendable := bk.SpendableCoins(ctx, account.GetAddress()) 348 349 txCtx := simulation.OperationInput{ 350 R: r, 351 App: app, 352 TxGen: simappparams.MakeTestEncodingConfig().TxConfig, 353 Cdc: nil, 354 Msg: msg, 355 MsgType: msg.Type(), 356 Context: ctx, 357 SimAccount: simAccount, 358 AccountKeeper: ak, 359 Bankkeeper: bk, 360 ModuleName: types.ModuleName, 361 CoinsSpentInMsg: spendable, 362 } 363 364 return simulation.GenAndDeliverTxWithRandFees(txCtx) 365 } 366 } 367 368 // Pick a random deposit with a random denomination with a 369 // deposit amount between (0, min(balance, minDepositAmount)) 370 // This is to simulate multiple users depositing to get the 371 // proposal above the minimum deposit amount 372 func randomDeposit(r *rand.Rand, ctx sdk.Context, 373 ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, addr sdk.AccAddress, 374 ) (deposit sdk.Coins, skip bool, err error) { 375 account := ak.GetAccount(ctx, addr) 376 spendable := bk.SpendableCoins(ctx, account.GetAddress()) 377 378 if spendable.Empty() { 379 return nil, true, nil // skip 380 } 381 382 minDeposit := k.GetDepositParams(ctx).MinDeposit 383 denomIndex := r.Intn(len(minDeposit)) 384 denom := minDeposit[denomIndex].Denom 385 386 depositCoins := spendable.AmountOf(denom) 387 if depositCoins.IsZero() { 388 return nil, true, nil 389 } 390 391 maxAmt := depositCoins 392 if maxAmt.GT(minDeposit[denomIndex].Amount) { 393 maxAmt = minDeposit[denomIndex].Amount 394 } 395 396 amount, err := simtypes.RandPositiveInt(r, maxAmt) 397 if err != nil { 398 return nil, false, err 399 } 400 401 return sdk.Coins{sdk.NewCoin(denom, amount)}, false, nil 402 } 403 404 // Pick a random proposal ID between the initial proposal ID 405 // (defined in gov GenesisState) and the latest proposal ID 406 // that matches a given Status. 407 // It does not provide a default ID. 408 func randomProposalID(r *rand.Rand, k keeper.Keeper, 409 ctx sdk.Context, status types.ProposalStatus, 410 ) (proposalID uint64, found bool) { 411 proposalID, _ = k.GetProposalID(ctx) 412 413 switch { 414 case proposalID > initialProposalID: 415 // select a random ID between [initialProposalID, proposalID] 416 proposalID = uint64(simtypes.RandIntBetween(r, int(initialProposalID), int(proposalID))) 417 418 default: 419 // This is called on the first call to this funcion 420 // in order to update the global variable 421 initialProposalID = proposalID 422 } 423 424 proposal, ok := k.GetProposal(ctx, proposalID) 425 if !ok || proposal.Status != status { 426 return proposalID, false 427 } 428 429 return proposalID, true 430 } 431 432 // Pick a random voting option 433 func randomVotingOption(r *rand.Rand) types.VoteOption { 434 switch r.Intn(4) { 435 case 0: 436 return types.OptionYes 437 case 1: 438 return types.OptionAbstain 439 case 2: 440 return types.OptionNo 441 case 3: 442 return types.OptionNoWithVeto 443 default: 444 panic("invalid vote option") 445 } 446 } 447 448 // Pick a random weighted voting options 449 func randomWeightedVotingOptions(r *rand.Rand) types.WeightedVoteOptions { 450 w1 := r.Intn(100 + 1) 451 w2 := r.Intn(100 - w1 + 1) 452 w3 := r.Intn(100 - w1 - w2 + 1) 453 w4 := 100 - w1 - w2 - w3 454 weightedVoteOptions := types.WeightedVoteOptions{} 455 if w1 > 0 { 456 weightedVoteOptions = append(weightedVoteOptions, types.WeightedVoteOption{ 457 Option: types.OptionYes, 458 Weight: sdk.NewDecWithPrec(int64(w1), 2), 459 }) 460 } 461 if w2 > 0 { 462 weightedVoteOptions = append(weightedVoteOptions, types.WeightedVoteOption{ 463 Option: types.OptionAbstain, 464 Weight: sdk.NewDecWithPrec(int64(w2), 2), 465 }) 466 } 467 if w3 > 0 { 468 weightedVoteOptions = append(weightedVoteOptions, types.WeightedVoteOption{ 469 Option: types.OptionNo, 470 Weight: sdk.NewDecWithPrec(int64(w3), 2), 471 }) 472 } 473 if w4 > 0 { 474 weightedVoteOptions = append(weightedVoteOptions, types.WeightedVoteOption{ 475 Option: types.OptionNoWithVeto, 476 Weight: sdk.NewDecWithPrec(int64(w4), 2), 477 }) 478 } 479 return weightedVoteOptions 480 }