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  }