cosmos-sdk/x/gov/simulation/operations.go
2025-04-03 10:42:20 -04:00

661 lines
19 KiB
Go

package simulation
import (
"math"
"math/rand"
"time"
sdkmath "cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/gov/keeper"
"github.com/cosmos/cosmos-sdk/x/gov/types"
v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
"github.com/cosmos/cosmos-sdk/x/simulation"
)
// Governance message types and routes
// will be removed in the future
var (
TypeMsgDeposit = sdk.MsgTypeURL(&v1.MsgDeposit{})
TypeMsgVote = sdk.MsgTypeURL(&v1.MsgVote{})
TypeMsgVoteWeighted = sdk.MsgTypeURL(&v1.MsgVoteWeighted{})
TypeMsgSubmitProposal = sdk.MsgTypeURL(&v1.MsgSubmitProposal{})
TypeMsgCancelProposal = sdk.MsgTypeURL(&v1.MsgCancelProposal{})
)
// Simulation operation weights constants
// will be removed in the future
const (
OpWeightMsgDeposit = "op_weight_msg_deposit"
OpWeightMsgVote = "op_weight_msg_vote"
OpWeightMsgVoteWeighted = "op_weight_msg_weighted_vote"
OpWeightMsgCancelProposal = "op_weight_msg_cancel_proposal"
DefaultWeightMsgDeposit = 100
DefaultWeightMsgVote = 67
DefaultWeightMsgVoteWeighted = 33
DefaultWeightTextProposal = 5
DefaultWeightMsgCancelProposal = 5
)
// WeightedOperations returns all the operations from the module with their respective weights
// will be removed in the future in favor of msg factory
func WeightedOperations(
appParams simtypes.AppParams,
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
wMsgs []simtypes.WeightedProposalMsg,
wContents []simtypes.WeightedProposalContent, //nolint:staticcheck // used for legacy testing
) simulation.WeightedOperations {
var (
weightMsgDeposit int
weightMsgVote int
weightMsgVoteWeighted int
weightMsgCancelProposal int
)
appParams.GetOrGenerate(OpWeightMsgDeposit, &weightMsgDeposit, nil,
func(_ *rand.Rand) {
weightMsgDeposit = DefaultWeightMsgDeposit
},
)
appParams.GetOrGenerate(OpWeightMsgVote, &weightMsgVote, nil,
func(_ *rand.Rand) {
weightMsgVote = DefaultWeightMsgVote
},
)
appParams.GetOrGenerate(OpWeightMsgVoteWeighted, &weightMsgVoteWeighted, nil,
func(_ *rand.Rand) {
weightMsgVoteWeighted = DefaultWeightMsgVoteWeighted
},
)
appParams.GetOrGenerate(OpWeightMsgCancelProposal, &weightMsgCancelProposal, nil,
func(_ *rand.Rand) {
weightMsgCancelProposal = DefaultWeightMsgCancelProposal
},
)
// generate the weighted operations for the proposal msgs
var wProposalOps simulation.WeightedOperations
for _, wMsg := range wMsgs {
var weight int
appParams.GetOrGenerate(wMsg.AppParamsKey(), &weight, nil,
func(_ *rand.Rand) { weight = wMsg.DefaultWeight() },
)
wProposalOps = append(
wProposalOps,
simulation.NewWeightedOperation(
weight,
SimulateMsgSubmitProposal(txGen, ak, bk, k, wMsg.MsgSimulatorFn()),
),
)
}
// generate the weighted operations for the proposal contents
var wLegacyProposalOps simulation.WeightedOperations
for _, wContent := range wContents {
var weight int
appParams.GetOrGenerate(wContent.AppParamsKey(), &weight, nil,
func(_ *rand.Rand) { weight = wContent.DefaultWeight() },
)
wLegacyProposalOps = append(
wLegacyProposalOps,
simulation.NewWeightedOperation(
weight,
SimulateMsgSubmitLegacyProposal(txGen, ak, bk, k, wContent.ContentSimulatorFn()),
),
)
}
state := NewSharedState()
wGovOps := simulation.WeightedOperations{
simulation.NewWeightedOperation(
weightMsgDeposit,
simulateMsgDeposit(txGen, ak, bk, k, state),
),
simulation.NewWeightedOperation(
weightMsgVote,
simulateMsgVote(txGen, ak, bk, k, state),
),
simulation.NewWeightedOperation(
weightMsgVoteWeighted,
simulateMsgVoteWeighted(txGen, ak, bk, k, state),
),
simulation.NewWeightedOperation(
weightMsgCancelProposal,
SimulateMsgCancelProposal(txGen, ak, bk, k),
),
}
return append(wGovOps, append(wProposalOps, wLegacyProposalOps...)...)
}
// SimulateMsgSubmitProposal simulates creating a msg Submit Proposal
// voting on the proposal, and subsequently slashing the proposal. It is implemented using
// future operations.
// will be removed in the future in favor of msg factory
func SimulateMsgSubmitProposal(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
msgSim simtypes.MsgSimulatorFn,
) simtypes.Operation {
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
msgs := []sdk.Msg{}
proposalMsg := msgSim(r, ctx, accs)
if proposalMsg != nil {
msgs = append(msgs, proposalMsg)
}
return simulateMsgSubmitProposal(txGen, ak, bk, k, msgs)(r, app, ctx, accs, chainID)
}
}
// SimulateMsgSubmitLegacyProposal simulates creating a msg Submit Proposal
// voting on the proposal, and subsequently slashing the proposal. It is implemented using
// future operations.
// will be removed in the future in favor of msg factory
func SimulateMsgSubmitLegacyProposal(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
contentSim simtypes.ContentSimulatorFn, //nolint:staticcheck // used for legacy testing
) simtypes.Operation {
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
// 1) submit proposal now
content := contentSim(r, ctx, accs)
if content == nil {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "content is nil"), nil, nil
}
govacc := k.GetGovernanceAccount(ctx)
contentMsg, err := v1.NewLegacyContent(content, govacc.GetAddress().String())
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "error converting legacy content into proposal message"), nil, err
}
return simulateMsgSubmitProposal(txGen, ak, bk, k, []sdk.Msg{contentMsg})(r, app, ctx, accs, chainID)
}
}
func simulateMsgSubmitProposal(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
proposalMsgs []sdk.Msg,
) simtypes.Operation {
// The states are:
// column 1: All validators vote
// column 2: 90% vote
// column 3: 75% vote
// column 4: 40% vote
// column 5: 15% vote
// column 6: no one votes
// All columns sum to 100 for simplicity, values chosen by @valardragon semi-arbitrarily,
// feel free to change.
numVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{
{20, 10, 0, 0, 0, 0},
{55, 50, 20, 10, 0, 0},
{25, 25, 30, 25, 30, 15},
{0, 15, 30, 25, 30, 30},
{0, 0, 20, 30, 30, 30},
{0, 0, 0, 10, 10, 25},
})
statePercentageArray := []float64{1, .9, .75, .4, .15, 0}
curNumVotesState := 1
return func(
r *rand.Rand,
app *baseapp.BaseApp,
ctx sdk.Context,
accs []simtypes.Account,
chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
simAccount, _ := simtypes.RandomAcc(r, accs)
expedited := r.Intn(2) == 0
deposit, skip, err := randomDeposit(r, ctx, ak, bk, k, simAccount.Address, true, expedited)
switch {
case skip:
return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "skip deposit"), nil, nil
case err != nil:
return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "unable to generate deposit"), nil, err
}
msg, err := v1.NewMsgSubmitProposal(
proposalMsgs,
deposit,
simAccount.Address.String(),
simtypes.RandStringOfLength(r, 100),
simtypes.RandStringOfLength(r, 100),
simtypes.RandStringOfLength(r, 100),
expedited,
)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate a submit proposal msg"), nil, err
}
account := ak.GetAccount(ctx, simAccount.Address)
tx, err := simtestutil.GenSignedMockTx(
r,
txGen,
[]sdk.Msg{msg},
sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)},
simtestutil.DefaultGenTxGas,
chainID,
[]uint64{account.GetAccountNumber()},
[]uint64{account.GetSequence()},
simAccount.PrivKey,
)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate mock tx"), nil, err
}
_, _, err = app.SimDeliver(txGen.TxEncoder(), tx)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to deliver tx"), nil, err
}
opMsg := simtypes.NewOperationMsg(msg, true, "")
// get the submitted proposal ID
proposalID, err := k.ProposalID.Peek(ctx)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate proposalID"), nil, err
}
// 2) Schedule operations for votes
// 2.1) first pick a number of people to vote.
curNumVotesState = numVotesTransitionMatrix.NextState(r, curNumVotesState)
numVotes := int(math.Ceil(float64(len(accs)) * statePercentageArray[curNumVotesState]))
// 2.2) select who votes and when
whoVotes := r.Perm(len(accs))
// didntVote := whoVotes[numVotes:]
whoVotes = whoVotes[:numVotes]
params, _ := k.Params.Get(ctx)
votingPeriod := params.VotingPeriod
s := NewSharedState()
fops := make([]simtypes.FutureOperation, numVotes+1)
for i := range numVotes {
whenVote := ctx.BlockHeader().Time.Add(time.Duration(r.Int63n(int64(votingPeriod.Seconds()))) * time.Second)
fops[i] = simtypes.FutureOperation{
BlockTime: whenVote,
Op: operationSimulateMsgVote(txGen, ak, bk, k, accs[whoVotes[i]], int64(proposalID), s),
}
}
return opMsg, fops, nil
}
}
// SimulateMsgDeposit generates a MsgDeposit with random values.
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgDeposit(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
) simtypes.Operation {
return simulateMsgDeposit(txGen, ak, bk, k, NewSharedState())
}
func simulateMsgDeposit(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
s *SharedState,
) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
simAccount, _ := simtypes.RandomAcc(r, accs)
proposalID, ok := randomProposalID(r, k, ctx, v1.StatusDepositPeriod, s)
if !ok {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "unable to generate proposalID"), nil, nil
}
p, err := k.Proposals.Get(ctx, proposalID)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "unable to get proposal"), nil, err
}
deposit, skip, err := randomDeposit(r, ctx, ak, bk, k, simAccount.Address, false, p.Expedited)
switch {
case skip:
return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "skip deposit"), nil, nil
case err != nil:
return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "unable to generate deposit"), nil, err
}
msg := v1.NewMsgDeposit(simAccount.Address, proposalID, deposit)
account := ak.GetAccount(ctx, simAccount.Address)
spendable := bk.SpendableCoins(ctx, account.GetAddress())
var fees sdk.Coins
coins, hasNeg := spendable.SafeSub(deposit...)
if !hasNeg {
fees, err = simtypes.RandomFees(r, ctx, coins)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate fees"), nil, err
}
}
txCtx := simulation.OperationInput{
R: r,
App: app,
TxGen: txGen,
Cdc: nil,
Msg: msg,
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
ModuleName: types.ModuleName,
}
return simulation.GenAndDeliverTx(txCtx, fees)
}
}
// SimulateMsgVote generates a MsgVote with random values.
// Deprecated: this is an internal method and will be removed
func SimulateMsgVote(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
) simtypes.Operation {
return simulateMsgVote(txGen, ak, bk, k, NewSharedState())
}
func simulateMsgVote(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
s *SharedState,
) simtypes.Operation {
return operationSimulateMsgVote(txGen, ak, bk, k, simtypes.Account{}, -1, s)
}
func operationSimulateMsgVote(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
simAccount simtypes.Account,
proposalIDInt int64,
s *SharedState,
) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
if simAccount.Equals(simtypes.Account{}) {
simAccount, _ = simtypes.RandomAcc(r, accs)
}
var proposalID uint64
switch {
case proposalIDInt < 0:
var ok bool
proposalID, ok = randomProposalID(r, k, ctx, v1.StatusVotingPeriod, s)
if !ok {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgVote, "unable to generate proposalID"), nil, nil
}
default:
proposalID = uint64(proposalIDInt)
}
option := randomVotingOption(r)
msg := v1.NewMsgVote(simAccount.Address, proposalID, option, "")
account := ak.GetAccount(ctx, simAccount.Address)
spendable := bk.SpendableCoins(ctx, account.GetAddress())
txCtx := simulation.OperationInput{
R: r,
App: app,
TxGen: txGen,
Cdc: nil,
Msg: msg,
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
Bankkeeper: bk,
ModuleName: types.ModuleName,
CoinsSpentInMsg: spendable,
}
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
// SimulateMsgVoteWeighted generates a MsgVoteWeighted with random values.
// will be removed in the future in favor of msg factory
func SimulateMsgVoteWeighted(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
) simtypes.Operation {
return simulateMsgVoteWeighted(txGen, ak, bk, k, NewSharedState())
}
func simulateMsgVoteWeighted(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
s *SharedState,
) simtypes.Operation {
return operationSimulateMsgVoteWeighted(txGen, ak, bk, k, simtypes.Account{}, -1, s)
}
func operationSimulateMsgVoteWeighted(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
simAccount simtypes.Account,
proposalIDInt int64,
s *SharedState,
) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
if simAccount.Equals(simtypes.Account{}) {
simAccount, _ = simtypes.RandomAcc(r, accs)
}
var proposalID uint64
switch {
case proposalIDInt < 0:
var ok bool
proposalID, ok = randomProposalID(r, k, ctx, v1.StatusVotingPeriod, s)
if !ok {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgVoteWeighted, "unable to generate proposalID"), nil, nil
}
default:
proposalID = uint64(proposalIDInt)
}
options := randomWeightedVotingOptions(r)
msg := v1.NewMsgVoteWeighted(simAccount.Address, proposalID, options, "")
account := ak.GetAccount(ctx, simAccount.Address)
spendable := bk.SpendableCoins(ctx, account.GetAddress())
txCtx := simulation.OperationInput{
R: r,
App: app,
TxGen: txGen,
Cdc: nil,
Msg: msg,
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
Bankkeeper: bk,
ModuleName: types.ModuleName,
CoinsSpentInMsg: spendable,
}
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
// SimulateMsgCancelProposal generates a MsgCancelProposal.
// will be removed in the future in favor of msg factory
func SimulateMsgCancelProposal(
txGen client.TxConfig,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
simAccount := accs[0]
proposal := randomProposal(r, k, ctx)
if proposal == nil {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgCancelProposal, "no proposals found"), nil, nil
}
if proposal.Proposer != simAccount.Address.String() {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgCancelProposal, "invalid proposer"), nil, nil
}
if (proposal.Status != v1.StatusDepositPeriod) && (proposal.Status != v1.StatusVotingPeriod) {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgCancelProposal, "invalid proposal status"), nil, nil
}
account := ak.GetAccount(ctx, simAccount.Address)
spendable := bk.SpendableCoins(ctx, account.GetAddress())
msg := v1.NewMsgCancelProposal(proposal.Id, account.GetAddress().String())
txCtx := simulation.OperationInput{
R: r,
App: app,
TxGen: txGen,
Cdc: nil,
Msg: msg,
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
Bankkeeper: bk,
ModuleName: types.ModuleName,
CoinsSpentInMsg: spendable,
}
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
// Pick a random deposit with a random denomination with a
// deposit amount between (0, min(balance, minDepositAmount))
// This is to simulate multiple users depositing to get the
// proposal above the minimum deposit amount
func randomDeposit(
r *rand.Rand,
ctx sdk.Context,
ak types.AccountKeeper,
bk types.BankKeeper,
k *keeper.Keeper,
addr sdk.AccAddress,
useMinAmount bool,
expedited bool,
) (deposit sdk.Coins, skip bool, err error) {
account := ak.GetAccount(ctx, addr)
spendable := bk.SpendableCoins(ctx, account.GetAddress())
if spendable.Empty() {
return nil, true, nil // skip
}
params, _ := k.Params.Get(ctx)
minDeposit := params.MinDeposit
if expedited {
minDeposit = params.ExpeditedMinDeposit
}
denomIndex := r.Intn(len(minDeposit))
denom := minDeposit[denomIndex].Denom
spendableBalance := spendable.AmountOf(denom)
if spendableBalance.IsZero() {
return nil, true, nil
}
minDepositAmount := minDeposit[denomIndex].Amount
minDepositRatio, err := sdkmath.LegacyNewDecFromStr(params.GetMinDepositRatio())
if err != nil {
return nil, false, err
}
threshold := minDepositAmount.ToLegacyDec().Mul(minDepositRatio).TruncateInt()
minAmount := sdkmath.ZeroInt()
if useMinAmount {
minDepositPercent, err := sdkmath.LegacyNewDecFromStr(params.MinInitialDepositRatio)
if err != nil {
return nil, false, err
}
minAmount = sdkmath.LegacyNewDecFromInt(minDepositAmount).Mul(minDepositPercent).TruncateInt()
}
amount, err := simtypes.RandPositiveInt(r, minDepositAmount.Sub(minAmount))
if err != nil {
return nil, false, err
}
amount = amount.Add(minAmount)
if amount.GT(spendableBalance) || amount.LT(threshold) {
return nil, true, nil
}
return sdk.Coins{sdk.NewCoin(denom, amount)}, false, nil
}
// randomProposal returns a random proposal stored in state
func randomProposal(r *rand.Rand, k *keeper.Keeper, ctx sdk.Context) *v1.Proposal {
var proposals []*v1.Proposal
err := k.Proposals.Walk(ctx, nil, func(key uint64, value v1.Proposal) (stop bool, err error) {
proposals = append(proposals, &value)
return false, nil
})
if err != nil {
panic(err)
}
if len(proposals) == 0 {
return nil
}
randomIndex := r.Intn(len(proposals))
return proposals[randomIndex]
}