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