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  }