diff --git a/CHANGELOG.md b/CHANGELOG.md index 98b322d30a..b63c204d5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (simsx) [#24062](https://github.com/cosmos/cosmos-sdk/pull/24062) [#24145](https://github.com/cosmos/cosmos-sdk/pull/24145) Add new simsx framework on top of simulations for better module dev experience. * (baseapp) [#24069](https://github.com/cosmos/cosmos-sdk/pull/24069) Create CheckTxHandler to allow extending the logic of CheckTx. * (types) [#24093](https://github.com/cosmos/cosmos-sdk/pull/24093) Added a new method, `IsGT`, for `types.Coin`. This method is used to check if a `types.Coin` is greater than another `types.Coin`. * (client/keys) [#24071](https://github.com/cosmos/cosmos-sdk/pull/24071) Add support for importing hex key using standard input. diff --git a/simsx/context.go b/simsx/context.go new file mode 100644 index 0000000000..4f104f7c1b --- /dev/null +++ b/simsx/context.go @@ -0,0 +1,14 @@ +package simsx + +import ( + "context" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// BlockTime read header block time from sdk context +func BlockTime(ctx context.Context) time.Time { + sdkCtx := sdk.UnwrapSDKContext(ctx) + return sdkCtx.BlockTime() +} diff --git a/simsx/environment.go b/simsx/environment.go index a9debe3d3e..142370364d 100644 --- a/simsx/environment.go +++ b/simsx/environment.go @@ -107,7 +107,7 @@ type visitable interface { // RandSubsetCoins return random amounts from the current balance. When the coins are empty, skip is called on the reporter. // The amounts are removed from the liquid balance. func (b *SimsAccountBalance) RandSubsetCoins(reporter SimulationReporter, filters ...CoinsFilter) sdk.Coins { - amount := b.randomAmount(5, reporter, b.Coins, filters...) + amount := b.randomAmount(1, reporter, b.Coins, filters...) b.Coins = b.Sub(amount...) if amount.Empty() { reporter.Skip("got empty amounts") @@ -123,7 +123,7 @@ func (b *SimsAccountBalance) RandSubsetCoin(reporter SimulationReporter, denom s reporter.Skipf("no such coin: %s", denom) return sdk.NewCoin(denom, math.ZeroInt()) } - amounts := b.randomAmount(5, reporter, sdk.Coins{coin}, filters...) + amounts := b.randomAmount(1, reporter, sdk.Coins{coin}, filters...) if amounts.Empty() { reporter.Skip("empty coin") return sdk.NewCoin(denom, math.ZeroInt()) @@ -287,9 +287,16 @@ func NewChainDataSource( } // AnyAccount returns a random SimAccount matching the filter criteria. Module accounts are excluded. -// In case of an error or no matching account found, the reporter is set to skip and an empty value is returned. +// In case of an error or no matching account was found with 1 retry, the reporter is set to skip and an empty value is returned. func (c *ChainDataSource) AnyAccount(r SimulationReporter, filters ...SimAccountFilter) SimAccount { - acc := c.randomAccount(r, 5, filters...) + acc := c.AnyAccountN(1, r, filters...) + return acc +} + +// AnyAccountN returns a random SimAccount matching the filter criteria with given number of retries. Module accounts are excluded. +// In case of an error or no matching account found, the reporter is set to skip and an empty value is returned. +func (c *ChainDataSource) AnyAccountN(retries int, r SimulationReporter, filters ...SimAccountFilter) SimAccount { + acc := c.randomAccount(r, retries, filters...) return acc } diff --git a/simsx/environment_test.go b/simsx/environment_test.go index 957601326b..68d57151df 100644 --- a/simsx/environment_test.go +++ b/simsx/environment_test.go @@ -16,6 +16,7 @@ func TestChainDataSourceAnyAccount(t *testing.T) { accs := simtypes.RandomAccounts(r, 3) specs := map[string]struct { filters []SimAccountFilter + retry int assert func(t *testing.T, got SimAccount, reporter SimulationReporter) }{ "no filters": { @@ -30,6 +31,7 @@ func TestChainDataSourceAnyAccount(t *testing.T) { assert.Equal(t, accs[2].AddressBech32, got.AddressBech32) assert.False(t, reporter.IsSkipped()) }, + retry: 6, }, "no match": { filters: []SimAccountFilter{SimAccountFilterFn(func(a SimAccount) bool { return false })}, @@ -43,7 +45,7 @@ func TestChainDataSourceAnyAccount(t *testing.T) { t.Run(name, func(t *testing.T) { reporter := NewBasicSimulationReporter() c := NewChainDataSource(sdk.Context{}, r, nil, nil, codec, accs...) - a := c.AnyAccount(reporter, spec.filters...) + a := c.AnyAccountN(spec.retry, reporter, spec.filters...) spec.assert(t, a, reporter) }) } diff --git a/x/auth/module.go b/x/auth/module.go index 47495ba6ff..bb563020f5 100644 --- a/x/auth/module.go +++ b/x/auth/module.go @@ -16,6 +16,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -171,10 +172,16 @@ func (am AppModule) GenerateGenesisState(simState *module.SimulationState) { } // ProposalMsgs returns msgs used for governance proposals for simulations. +// migrate to ProposalMsgsX. This method is ignored when ProposalMsgsX exists and will be removed in the future. func (AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { return simulation.ProposalMsgs() } +// ProposalMsgsX registers governance proposal messages in the simulation registry. +func (AppModule) ProposalMsgsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("msg_update_params", 100), simulation.MsgUpdateParamsFactory()) +} + // RegisterStoreDecoder registers a decoder for auth module's types func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { sdr[types.StoreKey] = simtypes.NewStoreDecoderFuncFromCollectionsSchema(am.accountKeeper.Schema) diff --git a/x/auth/simulation/genesis.go b/x/auth/simulation/genesis.go index c4586ee966..df21a3c6d4 100644 --- a/x/auth/simulation/genesis.go +++ b/x/auth/simulation/genesis.go @@ -1,8 +1,6 @@ package simulation import ( - "encoding/json" - "fmt" "math/rand" sdk "github.com/cosmos/cosmos-sdk/types" @@ -112,11 +110,5 @@ func RandomizedGenState(simState *module.SimulationState, randGenAccountsFn type genesisAccs := randGenAccountsFn(simState) authGenesis := types.NewGenesisState(params, genesisAccs) - - bz, err := json.MarshalIndent(&authGenesis.Params, "", " ") - if err != nil { - panic(err) - } - fmt.Printf("Selected randomly generated auth parameters:\n%s\n", bz) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(authGenesis) } diff --git a/x/auth/simulation/msg_factory.go b/x/auth/simulation/msg_factory.go new file mode 100644 index 0000000000..83bcca3029 --- /dev/null +++ b/x/auth/simulation/msg_factory.go @@ -0,0 +1,25 @@ +package simulation + +import ( + "context" + + "github.com/cosmos/cosmos-sdk/simsx" + "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +func MsgUpdateParamsFactory() simsx.SimMsgFactoryFn[*types.MsgUpdateParams] { + return func(_ context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgUpdateParams) { + r := testData.Rand() + params := types.DefaultParams() + params.MaxMemoCharacters = r.Uint64InRange(1, 1000) + params.TxSigLimit = r.Uint64InRange(1, 1000) + params.TxSizeCostPerByte = r.Uint64InRange(1, 1000) + params.SigVerifyCostED25519 = r.Uint64InRange(1, 1000) + params.SigVerifyCostSecp256k1 = r.Uint64InRange(1, 1000) + + return nil, &types.MsgUpdateParams{ + Authority: testData.ModuleAccountAddress(reporter, "gov"), + Params: params, + } + } +} diff --git a/x/auth/simulation/proposals.go b/x/auth/simulation/proposals.go index 0e7cd95797..25f301db39 100644 --- a/x/auth/simulation/proposals.go +++ b/x/auth/simulation/proposals.go @@ -11,13 +11,14 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( DefaultWeightMsgUpdateParams int = 100 - - OpWeightMsgUpdateParams = "op_weight_msg_update_params" + OpWeightMsgUpdateParams = "op_weight_msg_update_params" ) // ProposalMsgs defines the module weighted proposals' contents +// migrate to MsgUpdateParamsFactory instead func ProposalMsgs() []simtypes.WeightedProposalMsg { return []simtypes.WeightedProposalMsg{ simulation.NewWeightedProposalMsg( @@ -29,6 +30,7 @@ func ProposalMsgs() []simtypes.WeightedProposalMsg { } // SimulateMsgUpdateParams returns a random MsgUpdateParams +// will be removed in the future func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { // use the default gov module account address as authority var authority sdk.AccAddress = address.Module("gov") diff --git a/x/authz/module/module.go b/x/authz/module/module.go index 3c80d13c14..dc38807a3d 100644 --- a/x/authz/module/module.go +++ b/x/authz/module/module.go @@ -19,6 +19,7 @@ import ( sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -196,6 +197,7 @@ func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { } // WeightedOperations returns the all the gov module operations with their respective weights. +// This method is ignored when WeightedOperationsX exists and will be removed in the future func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { return simulation.WeightedOperations( am.registry, @@ -203,3 +205,10 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp am.accountKeeper, am.bankKeeper, am.keeper, ) } + +// WeightedOperationsX registers weighted authz module operations for simulation. +func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("msg_grant", 100), simulation.MsgGrantFactory()) + reg.Add(weights.Get("msg_revoke", 90), simulation.MsgRevokeFactory(am.keeper)) + reg.Add(weights.Get("msg_exec", 90), simulation.MsgExecFactory(am.keeper)) +} diff --git a/x/authz/simulation/msg_factory.go b/x/authz/simulation/msg_factory.go new file mode 100644 index 0000000000..3c932f3a43 --- /dev/null +++ b/x/authz/simulation/msg_factory.go @@ -0,0 +1,107 @@ +package simulation + +import ( + "context" + "time" + + "github.com/cosmos/cosmos-sdk/simsx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" + "github.com/cosmos/cosmos-sdk/x/authz/keeper" + banktype "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +func MsgGrantFactory() simsx.SimMsgFactoryFn[*authz.MsgGrant] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *authz.MsgGrant) { + granter := testData.AnyAccount(reporter, simsx.WithSpendableBalance()) + grantee := testData.AnyAccount(reporter, simsx.ExcludeAccounts(granter)) + spendLimit := granter.LiquidBalance().RandSubsetCoins(reporter, simsx.WithSendEnabledCoins()) + + r := testData.Rand() + var expiration *time.Time + if t1 := r.Timestamp(); !t1.Before(simsx.BlockTime(ctx)) { + expiration = &t1 + } + // pick random authorization + authorizations := []authz.Authorization{ + banktype.NewSendAuthorization(spendLimit, nil), + authz.NewGenericAuthorization(sdk.MsgTypeURL(&banktype.MsgSend{})), + } + randomAuthz := simsx.OneOf(r, authorizations) + + msg, err := authz.NewMsgGrant(granter.Address, grantee.Address, randomAuthz, expiration) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + return []simsx.SimAccount{granter}, msg + } +} + +func MsgExecFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*authz.MsgExec] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *authz.MsgExec) { + bankSendOnlyFilter := func(a authz.Authorization) bool { + _, ok := a.(*banktype.SendAuthorization) + return ok + } + granterAddr, granteeAddr, gAuthz := findGrant(ctx, k, reporter, bankSendOnlyFilter) + granter := testData.GetAccountbyAccAddr(reporter, granterAddr) + grantee := testData.GetAccountbyAccAddr(reporter, granteeAddr) + if reporter.IsSkipped() { + return nil, nil + } + amount := granter.LiquidBalance().RandSubsetCoins(reporter, simsx.WithSendEnabledCoins()) + amount = amount.Min(gAuthz.(*banktype.SendAuthorization).SpendLimit) + if !amount.IsAllPositive() { + reporter.Skip("amount is not positive") + return nil, nil + } + payloadMsg := []sdk.Msg{banktype.NewMsgSend(granter.Address, grantee.Address, amount)} + msgExec := authz.NewMsgExec(grantee.Address, payloadMsg) + return []simsx.SimAccount{grantee}, &msgExec + } +} + +func MsgRevokeFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*authz.MsgRevoke] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *authz.MsgRevoke) { + granterAddr, granteeAddr, auth := findGrant(ctx, k, reporter) + granter := testData.GetAccountbyAccAddr(reporter, granterAddr) + grantee := testData.GetAccountbyAccAddr(reporter, granteeAddr) + if reporter.IsSkipped() { + return nil, nil + } + msgExec := authz.NewMsgRevoke(granter.Address, grantee.Address, auth.MsgTypeURL()) + return []simsx.SimAccount{granter}, &msgExec + } +} + +func findGrant( + ctx context.Context, + k keeper.Keeper, + reporter simsx.SimulationReporter, + acceptFilter ...func(a authz.Authorization) bool, +) (granterAddr, granteeAddr sdk.AccAddress, auth authz.Authorization) { + var innerErr error + k.IterateGrants(ctx, func(granter, grantee sdk.AccAddress, grant authz.Grant) bool { + a, err2 := grant.GetAuthorization() + if err2 != nil { + innerErr = err2 + return true + } + for _, filter := range acceptFilter { + if !filter(a) { + return false + } + } + granterAddr, granteeAddr, auth = granter, grantee, a + return true + }) + if innerErr != nil { + reporter.Skip(innerErr.Error()) + return + } + if auth == nil { + reporter.Skip("no grant found") + } + return +} diff --git a/x/authz/simulation/operations.go b/x/authz/simulation/operations.go index 80063b2d09..807c83caf7 100644 --- a/x/authz/simulation/operations.go +++ b/x/authz/simulation/operations.go @@ -19,6 +19,7 @@ import ( ) // authz message types +// will be removed in the future var ( TypeMsgGrant = sdk.MsgTypeURL(&authz.MsgGrant{}) TypeMsgRevoke = sdk.MsgTypeURL(&authz.MsgRevoke{}) @@ -26,6 +27,7 @@ var ( ) // Simulation operation weights constants +// will be removed in the future const ( OpWeightMsgGrant = "op_weight_msg_grant" OpWeightRevoke = "op_weight_msg_revoke" @@ -33,6 +35,7 @@ const ( ) // authz operations weights +// will be removed in the future const ( WeightGrant = 100 WeightRevoke = 90 @@ -40,6 +43,7 @@ const ( ) // 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( registry cdctypes.InterfaceRegistry, appParams simtypes.AppParams, @@ -86,6 +90,7 @@ func WeightedOperations( } // SimulateMsgGrant generates a MsgGrant with random values. +// will be removed in the future in favor of msg factory func SimulateMsgGrant( cdc *codec.ProtoCodec, txCfg client.TxConfig, @@ -159,6 +164,7 @@ func generateRandomAuthorization(r *rand.Rand, spendLimit sdk.Coins) authz.Autho } // SimulateMsgRevoke generates a MsgRevoke with random values. +// will be removed in the future in favor of msg factory func SimulateMsgRevoke( cdc *codec.ProtoCodec, txCfg client.TxConfig, @@ -228,6 +234,7 @@ func SimulateMsgRevoke( } // SimulateMsgExec generates a MsgExec with random values. +// will be removed in the future in favor of msg factory func SimulateMsgExec( cdc *codec.ProtoCodec, txCfg client.TxConfig, diff --git a/x/bank/simulation/genesis.go b/x/bank/simulation/genesis.go index 1dd6fd3dc8..b1a83e55f2 100644 --- a/x/bank/simulation/genesis.go +++ b/x/bank/simulation/genesis.go @@ -1,8 +1,6 @@ package simulation import ( - "encoding/json" - "fmt" "math/rand" sdkmath "cosmossdk.io/math" @@ -86,11 +84,5 @@ func RandomizedGenState(simState *module.SimulationState) { Supply: supply, SendEnabled: sendEnabled, } - - paramsBytes, err := json.MarshalIndent(&bankGenesis.Params, "", " ") - if err != nil { - panic(err) - } - fmt.Printf("Selected randomly generated bank parameters:\n%s\n", paramsBytes) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&bankGenesis) } diff --git a/x/bank/simulation/operations.go b/x/bank/simulation/operations.go index 7555c82cd7..c5be578e47 100644 --- a/x/bank/simulation/operations.go +++ b/x/bank/simulation/operations.go @@ -17,6 +17,7 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( OpWeightMsgSend = "op_weight_msg_send" OpWeightMsgMultiSend = "op_weight_msg_multisend" diff --git a/x/bank/simulation/proposals.go b/x/bank/simulation/proposals.go index d6359acbd7..1d122151e5 100644 --- a/x/bank/simulation/proposals.go +++ b/x/bank/simulation/proposals.go @@ -11,14 +11,14 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( DefaultWeightMsgUpdateParams int = 100 - - OpWeightMsgUpdateParams = "op_weight_msg_update_params" + OpWeightMsgUpdateParams = "op_weight_msg_update_params" ) // ProposalMsgs defines the module weighted proposals' contents -// Deprecated: migrate to MsgUpdateParamsFactory instead +// migrate to the msg factories instead, this method will be removed in the future func ProposalMsgs() []simtypes.WeightedProposalMsg { return []simtypes.WeightedProposalMsg{ simulation.NewWeightedProposalMsg( @@ -30,6 +30,7 @@ func ProposalMsgs() []simtypes.WeightedProposalMsg { } // SimulateMsgUpdateParams returns a random MsgUpdateParams +// will be removed in the future func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { // use the default gov module account address as authority var authority sdk.AccAddress = address.Module("gov") diff --git a/x/distribution/module.go b/x/distribution/module.go index 81108d4279..666d51b944 100644 --- a/x/distribution/module.go +++ b/x/distribution/module.go @@ -17,6 +17,7 @@ import ( sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -173,6 +174,7 @@ func (AppModule) GenerateGenesisState(simState *module.SimulationState) { } // ProposalMsgs returns msgs used for governance proposals for simulations. +// migrate to ProposalMsgsX. This method is ignored when ProposalMsgsX exists and will be removed in the future. func (AppModule) ProposalMsgs(_ module.SimulationState) []simtypes.WeightedProposalMsg { return simulation.ProposalMsgs() } @@ -183,6 +185,7 @@ func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { } // WeightedOperations returns the all the gov module operations with their respective weights. +// migrate to WeightedOperationsX. This method is ignored when WeightedOperationsX exists and will be removed in the future func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { return simulation.WeightedOperations( simState.AppParams, simState.Cdc, simState.TxConfig, @@ -190,6 +193,18 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp ) } +// ProposalMsgsX registers governance proposal messages in the simulation registry. +func (AppModule) ProposalMsgsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("msg_update_params", 100), simulation.MsgUpdateParamsFactory()) +} + +// WeightedOperationsX registers weighted distribution module operations for simulation. +func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("msg_set_withdraw_address", 50), simulation.MsgSetWithdrawAddressFactory(am.keeper)) + reg.Add(weights.Get("msg_withdraw_delegation_reward", 50), simulation.MsgWithdrawDelegatorRewardFactory(am.keeper, am.stakingKeeper)) + reg.Add(weights.Get("msg_withdraw_validator_commission", 50), simulation.MsgWithdrawValidatorCommissionFactory(am.keeper, am.stakingKeeper)) +} + // // App Wiring Setup // diff --git a/x/distribution/simulation/genesis.go b/x/distribution/simulation/genesis.go index 1a509250de..a03af3d05c 100644 --- a/x/distribution/simulation/genesis.go +++ b/x/distribution/simulation/genesis.go @@ -1,8 +1,6 @@ package simulation import ( - "encoding/json" - "fmt" "math/rand" "cosmossdk.io/math" @@ -42,11 +40,5 @@ func RandomizedGenState(simState *module.SimulationState) { WithdrawAddrEnabled: withdrawEnabled, }, } - - bz, err := json.MarshalIndent(&distrGenesis, "", " ") - if err != nil { - panic(err) - } - fmt.Printf("Selected randomly generated distribution parameters:\n%s\n", bz) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&distrGenesis) } diff --git a/x/distribution/simulation/msg_factory.go b/x/distribution/simulation/msg_factory.go new file mode 100644 index 0000000000..a3e43eae58 --- /dev/null +++ b/x/distribution/simulation/msg_factory.go @@ -0,0 +1,125 @@ +package simulation + +import ( + "context" + "errors" + + "cosmossdk.io/collections" + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/simsx" + "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + "github.com/cosmos/cosmos-sdk/x/distribution/types" +) + +func MsgSetWithdrawAddressFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgSetWithdrawAddress] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgSetWithdrawAddress) { + switch enabled, err := k.GetWithdrawAddrEnabled(ctx); { + case err != nil: + reporter.Skip("error getting params") + return nil, nil + case !enabled: + reporter.Skip("withdrawal is not enabled") + return nil, nil + } + delegator := testData.AnyAccount(reporter) + withdrawer := testData.AnyAccount(reporter, simsx.ExcludeAccounts(delegator)) + msg := types.NewMsgSetWithdrawAddress(delegator.Address, withdrawer.Address) + return []simsx.SimAccount{delegator}, msg + } +} + +func MsgWithdrawDelegatorRewardFactory(k keeper.Keeper, sk types.StakingKeeper) simsx.SimMsgFactoryFn[*types.MsgWithdrawDelegatorReward] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgWithdrawDelegatorReward) { + delegator := testData.AnyAccount(reporter) + + delegations, err := sk.GetAllDelegatorDelegations(ctx, delegator.Address) + switch { + case err != nil: + reporter.Skipf("error getting delegations: %v", err) + return nil, nil + case len(delegations) == 0: + reporter.Skip("no delegations found") + return nil, nil + } + delegation := delegations[testData.Rand().Intn(len(delegations))] + + valAddr, err := sk.ValidatorAddressCodec().StringToBytes(delegation.GetValidatorAddr()) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + + var valOper string + switch validator, err := sk.Validator(ctx, valAddr); { + case err != nil: + reporter.Skip(err.Error()) + return nil, nil + case validator == nil: + reporter.Skipf("validator %s not found", delegation.GetValidatorAddr()) + return nil, nil + default: + valOper = validator.GetOperator() + } + // get outstanding rewards so we can first check if the withdrawable coins are sendable + outstanding, err := k.GetValidatorOutstandingRewardsCoins(ctx, valAddr) + if err != nil { + reporter.Skipf("get outstanding rewards: %v", err) + return nil, nil + } + + for _, v := range outstanding { + if !testData.IsSendEnabledDenom(v.Denom) { + reporter.Skipf("denom send not enabled: " + v.Denom) + return nil, nil + } + } + + msg := types.NewMsgWithdrawDelegatorReward(delegator.AddressBech32, valOper) + return []simsx.SimAccount{delegator}, msg + } +} + +func MsgWithdrawValidatorCommissionFactory(k keeper.Keeper, sk types.StakingKeeper) simsx.SimMsgFactoryFn[*types.MsgWithdrawValidatorCommission] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgWithdrawValidatorCommission) { + allVals, err := sk.GetAllValidators(ctx) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + val := simsx.OneOf(testData.Rand(), allVals) + valAddrBz, err := sk.ValidatorAddressCodec().StringToBytes(val.GetOperator()) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + + commission, err := k.GetValidatorAccumulatedCommission(ctx, valAddrBz) + if err != nil && !errors.Is(err, collections.ErrNotFound) { + reporter.Skip(err.Error()) + return nil, nil + } + + if commission.Commission.IsZero() { + reporter.Skip("validator commission is zero") + return nil, nil + } + msg := types.NewMsgWithdrawValidatorCommission(val.GetOperator()) + valAccount := testData.GetAccountbyAccAddr(reporter, valAddrBz) + return []simsx.SimAccount{valAccount}, msg + } +} + +func MsgUpdateParamsFactory() simsx.SimMsgFactoryFn[*types.MsgUpdateParams] { + return func(_ context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgUpdateParams) { + r := testData.Rand() + params := types.DefaultParams() + params.CommunityTax = r.DecN(sdkmath.LegacyNewDec(1)) + params.WithdrawAddrEnabled = r.Intn(2) == 0 + + return nil, &types.MsgUpdateParams{ + Authority: testData.ModuleAccountAddress(reporter, "gov"), + Params: params, + } + } +} diff --git a/x/distribution/simulation/operations.go b/x/distribution/simulation/operations.go index 7918386f3f..34d2ffe1d1 100644 --- a/x/distribution/simulation/operations.go +++ b/x/distribution/simulation/operations.go @@ -19,6 +19,7 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( OpWeightMsgSetWithdrawAddress = "op_weight_msg_set_withdraw_address" OpWeightMsgWithdrawDelegationReward = "op_weight_msg_withdraw_delegation_reward" @@ -32,6 +33,7 @@ const ( ) // WeightedOperations returns all the operations from the module with their respective weights +// migrate to the msg factories instead, this method will be removed in the future func WeightedOperations( appParams simtypes.AppParams, _ codec.JSONCodec, @@ -87,6 +89,7 @@ func WeightedOperations( } // SimulateMsgSetWithdrawAddress generates a MsgSetWithdrawAddress with random values. +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgSetWithdrawAddress(txConfig 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, @@ -127,6 +130,7 @@ func SimulateMsgSetWithdrawAddress(txConfig client.TxConfig, ak types.AccountKee } // SimulateMsgWithdrawDelegatorReward generates a MsgWithdrawDelegatorReward with random values. +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgWithdrawDelegatorReward(txConfig client.TxConfig, ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, sk types.StakingKeeper) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, @@ -178,6 +182,7 @@ func SimulateMsgWithdrawDelegatorReward(txConfig client.TxConfig, ak types.Accou } // SimulateMsgWithdrawValidatorCommission generates a MsgWithdrawValidatorCommission with random values. +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgWithdrawValidatorCommission(txConfig client.TxConfig, ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, sk types.StakingKeeper) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, @@ -239,6 +244,7 @@ func SimulateMsgWithdrawValidatorCommission(txConfig client.TxConfig, ak types.A // SimulateMsgFundCommunityPool simulates MsgFundCommunityPool execution where // a random account sends a random amount of its funds to the community pool. +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgFundCommunityPool(txConfig client.TxConfig, ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, sk types.StakingKeeper) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, diff --git a/x/distribution/simulation/proposals.go b/x/distribution/simulation/proposals.go index bb5488aebd..623284a232 100644 --- a/x/distribution/simulation/proposals.go +++ b/x/distribution/simulation/proposals.go @@ -13,13 +13,14 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( DefaultWeightMsgUpdateParams int = 50 - - OpWeightMsgUpdateParams = "op_weight_msg_update_params" + OpWeightMsgUpdateParams = "op_weight_msg_update_params" ) // ProposalMsgs defines the module weighted proposals' contents +// migrate to MsgUpdateParamsFactory instead func ProposalMsgs() []simtypes.WeightedProposalMsg { return []simtypes.WeightedProposalMsg{ simulation.NewWeightedProposalMsg( @@ -31,6 +32,7 @@ func ProposalMsgs() []simtypes.WeightedProposalMsg { } // SimulateMsgUpdateParams returns a random MsgUpdateParams +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { // use the default gov module account address as authority var authority sdk.AccAddress = address.Module("gov") diff --git a/x/evidence/simulation/genesis.go b/x/evidence/simulation/genesis.go index 24b9562746..725848d721 100644 --- a/x/evidence/simulation/genesis.go +++ b/x/evidence/simulation/genesis.go @@ -1,8 +1,6 @@ package simulation import ( - "encoding/json" - "fmt" "math/rand" "cosmossdk.io/x/evidence/exported" @@ -27,11 +25,5 @@ func RandomizedGenState(simState *module.SimulationState) { simState.AppParams.GetOrGenerate(evidence, &ev, simState.Rand, func(r *rand.Rand) { ev = GenEvidences(r, simState.Accounts) }) evidenceGenesis := types.NewGenesisState(ev) - - bz, err := json.MarshalIndent(&evidenceGenesis, "", " ") - if err != nil { - panic(err) - } - fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, bz) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(evidenceGenesis) } diff --git a/x/feegrant/module/module.go b/x/feegrant/module/module.go index 5baebe3902..8eed869d50 100644 --- a/x/feegrant/module/module.go +++ b/x/feegrant/module/module.go @@ -22,6 +22,7 @@ import ( sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -200,9 +201,19 @@ func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { } // WeightedOperations returns all the feegrant module operations with their respective weights. +// migrate to WeightedOperationsX. This method is ignored when WeightedOperationsX exists and will be removed in the future func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { return simulation.WeightedOperations( am.registry, simState.AppParams, simState.Cdc, simState.TxConfig, am.accountKeeper, am.bankKeeper, am.keeper, am.ac, ) } + +// WeightedOperationsX registers weighted feegrant module operations for simulation. +func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("msg_grant_fee_allowance", 100), simulation.MsgGrantAllowanceFactory(am.keeper)) + // use old misspelled OpWeightMsgRevokeAllowance key for legacy reasons but default to the new key + // so that we can replace it at some point + w := weights.Get("msg_grant_revoke_allowance", weights.Get("msg_revoke_allowance", 100)) + reg.Add(w, simulation.MsgRevokeAllowanceFactory(am.keeper)) +} diff --git a/x/feegrant/simulation/msg_factory.go b/x/feegrant/simulation/msg_factory.go new file mode 100644 index 0000000000..9346cab696 --- /dev/null +++ b/x/feegrant/simulation/msg_factory.go @@ -0,0 +1,65 @@ +package simulation + +import ( + "context" + "time" + + "cosmossdk.io/x/feegrant" + "cosmossdk.io/x/feegrant/keeper" + + "github.com/cosmos/cosmos-sdk/simsx" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func MsgGrantAllowanceFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*feegrant.MsgGrantAllowance] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *feegrant.MsgGrantAllowance) { + granter := testData.AnyAccount(reporter, simsx.WithSpendableBalance()) + grantee := testData.AnyAccount(reporter, simsx.ExcludeAccounts(granter)) + if reporter.IsSkipped() { + return nil, nil + } + if f, _ := k.GetAllowance(ctx, granter.Address, grantee.Address); f != nil { + reporter.Skip("fee allowance exists") + return nil, nil + } + + coins := granter.LiquidBalance().RandSubsetCoins(reporter, simsx.WithSendEnabledCoins()) + oneYear := blockTime(ctx).AddDate(1, 0, 0) + msg, err := feegrant.NewMsgGrantAllowance( + &feegrant.BasicAllowance{SpendLimit: coins, Expiration: &oneYear}, + granter.Address, + grantee.Address, + ) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + return []simsx.SimAccount{granter}, msg + } +} + +func MsgRevokeAllowanceFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*feegrant.MsgRevokeAllowance] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *feegrant.MsgRevokeAllowance) { + var gotGrant *feegrant.Grant + if err := k.IterateAllFeeAllowances(ctx, func(grant feegrant.Grant) bool { + gotGrant = &grant + return true + }); err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + if gotGrant == nil { + reporter.Skip("no grant found") + return nil, nil + } + granter := testData.GetAccount(reporter, gotGrant.Granter) + grantee := testData.GetAccount(reporter, gotGrant.Grantee) + msg := feegrant.NewMsgRevokeAllowance(granter.Address, grantee.Address) + return []simsx.SimAccount{granter}, &msg + } +} + +// temporary solution. use simsx.BlockTime when available +func blockTime(ctx context.Context) time.Time { + return sdk.UnwrapSDKContext(ctx).BlockTime() +} diff --git a/x/feegrant/simulation/operations.go b/x/feegrant/simulation/operations.go index 16d0e7b804..289eaca7ad 100644 --- a/x/feegrant/simulation/operations.go +++ b/x/feegrant/simulation/operations.go @@ -17,6 +17,7 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( OpWeightMsgGrantAllowance = "op_weight_msg_grant_fee_allowance" OpWeightMsgRevokeAllowance = "op_weight_msg_grant_revoke_allowance" @@ -24,11 +25,13 @@ const ( DefaultWeightRevokeAllowance int = 100 ) +// will be removed in the future var ( TypeMsgGrantAllowance = sdk.MsgTypeURL(&feegrant.MsgGrantAllowance{}) TypeMsgRevokeAllowance = sdk.MsgTypeURL(&feegrant.MsgRevokeAllowance{}) ) +// will be removed in the future in favor of msg factory func WeightedOperations( registry codectypes.InterfaceRegistry, appParams simtypes.AppParams, @@ -71,6 +74,7 @@ func WeightedOperations( } // SimulateMsgGrantAllowance generates MsgGrantAllowance with random values. +// will be removed in the future in favor of msg factory func SimulateMsgGrantAllowance( cdc *codec.ProtoCodec, txConfig client.TxConfig, @@ -126,6 +130,7 @@ func SimulateMsgGrantAllowance( } // SimulateMsgRevokeAllowance generates a MsgRevokeAllowance with random values. +// will be removed in the future in favor of msg factory func SimulateMsgRevokeAllowance( cdc *codec.ProtoCodec, txConfig client.TxConfig, diff --git a/x/gov/module.go b/x/gov/module.go index 865e114da6..d671c0f186 100644 --- a/x/gov/module.go +++ b/x/gov/module.go @@ -22,6 +22,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -325,11 +326,13 @@ func (AppModule) GenerateGenesisState(simState *module.SimulationState) { // ProposalContents returns all the gov content functions used to // simulate governance proposals. +// migrate to ProposalMsgsX. This method is ignored when ProposalMsgsX exists and will be removed in the future. func (AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent { //nolint:staticcheck // used for legacy testing return simulation.ProposalContents() } // ProposalMsgs returns all the gov msgs used to simulate governance proposals. +// migrate to ProposalMsgsX. This method is ignored when ProposalMsgsX exists and will be removed in the future. func (AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { return simulation.ProposalMsgs() } @@ -340,6 +343,7 @@ func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { } // WeightedOperations returns the all the gov module operations with their respective weights. +// migrate to WeightedOperationsX. This method is ignored when WeightedOperationsX exists and will be removed in the future func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { return simulation.WeightedOperations( simState.AppParams, simState.TxConfig, @@ -347,3 +351,29 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp simState.ProposalMsgs, simState.LegacyProposalContents, ) } + +// ProposalMsgsX registers governance proposal messages in the simulation registry. +func (AppModule) ProposalMsgsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("submit_text_proposal", 5), simulation.TextProposalFactory()) +} + +// WeightedOperationsX registers weighted gov module operations for simulation. +func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Registry, proposalMsgIter simsx.WeightedProposalMsgIter, + legacyProposals []simtypes.WeightedProposalContent, //nolint:staticcheck // used for legacy proposal types +) { + // submit proposal for each payload message + for weight, factory := range proposalMsgIter { + // use a ratio so that we don't flood with gov ops + reg.Add(weight/25, simulation.MsgSubmitProposalFactory(am.keeper, factory)) + } + for _, wContent := range legacyProposals { + reg.Add(weights.Get(wContent.AppParamsKey(), uint32(wContent.DefaultWeight())), simulation.MsgSubmitLegacyProposalFactory(am.keeper, wContent.ContentSimulatorFn())) + } + + state := simulation.NewSharedState() + reg.Add(weights.Get("msg_deposit", 100), simulation.MsgDepositFactory(am.keeper, state)) + reg.Add(weights.Get("msg_vote", 67), simulation.MsgVoteFactory(am.keeper, state)) + reg.Add(weights.Get("msg_weighted_vote", 33), simulation.MsgWeightedVoteFactory(am.keeper, state)) + reg.Add(weights.Get("cancel_proposal", 5), simulation.MsgCancelProposalFactory(am.keeper, state)) + reg.Add(weights.Get("legacy_text_proposal", 5), simulation.MsgSubmitLegacyProposalFactory(am.keeper, simulation.SimulateLegacyTextProposalContent)) +} diff --git a/x/gov/simulation/genesis.go b/x/gov/simulation/genesis.go index 1606688484..6975a985b5 100644 --- a/x/gov/simulation/genesis.go +++ b/x/gov/simulation/genesis.go @@ -1,8 +1,6 @@ package simulation import ( - "encoding/json" - "fmt" "math/rand" "time" @@ -144,11 +142,5 @@ func RandomizedGenState(simState *module.SimulationState) { startingProposalID, v1.NewParams(minDeposit, expeditedMinDeposit, depositPeriod, votingPeriod, expeditedVotingPeriod, quorum.String(), threshold.String(), expitedVotingThreshold.String(), veto.String(), minInitialDepositRatio.String(), proposalCancelRate.String(), "", simState.Rand.Intn(2) == 0, simState.Rand.Intn(2) == 0, simState.Rand.Intn(2) == 0, minDepositRatio.String()), ) - - bz, err := json.MarshalIndent(&govGenesis, "", " ") - if err != nil { - panic(err) - } - fmt.Printf("Selected randomly generated governance parameters:\n%s\n", bz) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(govGenesis) } diff --git a/x/gov/simulation/msg_factory.go b/x/gov/simulation/msg_factory.go new file mode 100644 index 0000000000..65a99b1e5f --- /dev/null +++ b/x/gov/simulation/msg_factory.go @@ -0,0 +1,370 @@ +package simulation + +import ( + "context" + "math" + "math/rand" + "sync/atomic" + "time" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/simsx" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/gov/keeper" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +func MsgDepositFactory(k *keeper.Keeper, sharedState *SharedState) simsx.SimMsgFactoryFn[*v1.MsgDeposit] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *v1.MsgDeposit) { + r := testData.Rand() + proposalID, ok := randomProposalID(r.Rand, k, sdk.UnwrapSDKContext(ctx), v1.StatusDepositPeriod, sharedState) + if !ok { + reporter.Skip("no proposal in deposit state") + return nil, nil + } + proposal, err := k.Proposals.Get(ctx, proposalID) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + // calculate deposit amount + deposit := randDeposit(ctx, proposal, k, r, reporter) + if reporter.IsSkipped() { + return nil, nil + } + from := testData.AnyAccount(reporter, simsx.WithLiquidBalanceGTE(deposit)) + return []simsx.SimAccount{from}, v1.NewMsgDeposit(from.Address, proposalID, sdk.NewCoins(deposit)) + } +} + +func MsgVoteFactory(k *keeper.Keeper, sharedState *SharedState) simsx.SimMsgFactoryFn[*v1.MsgVote] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *v1.MsgVote) { + r := testData.Rand() + proposalID, ok := randomProposalID(r.Rand, k, sdk.UnwrapSDKContext(ctx), v1.StatusVotingPeriod, sharedState) + if !ok { + reporter.Skip("no proposal in voting state") + return nil, nil + } + from := testData.AnyAccount(reporter, simsx.WithSpendableBalance()) + msg := v1.NewMsgVote(from.Address, proposalID, randomVotingOption(r.Rand), "") + return []simsx.SimAccount{from}, msg + } +} + +func MsgWeightedVoteFactory(k *keeper.Keeper, sharedState *SharedState) simsx.SimMsgFactoryFn[*v1.MsgVoteWeighted] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *v1.MsgVoteWeighted) { + r := testData.Rand() + proposalID, ok := randomProposalID(r.Rand, k, sdk.UnwrapSDKContext(ctx), v1.StatusVotingPeriod, sharedState) + if !ok { + reporter.Skip("no proposal in deposit state") + return nil, nil + } + from := testData.AnyAccount(reporter, simsx.WithSpendableBalance()) + msg := v1.NewMsgVoteWeighted(from.Address, proposalID, randomWeightedVotingOptions(r.Rand), "") + return []simsx.SimAccount{from}, msg + } +} + +func MsgCancelProposalFactory(k *keeper.Keeper, sharedState *SharedState) simsx.SimMsgFactoryFn[*v1.MsgCancelProposal] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *v1.MsgCancelProposal) { + r := testData.Rand() + status := simsx.OneOf(r, []v1.ProposalStatus{v1.StatusDepositPeriod, v1.StatusVotingPeriod}) + proposalID, ok := randomProposalID(r.Rand, k, sdk.UnwrapSDKContext(ctx), status, sharedState) + if !ok { + reporter.Skip("no proposal in deposit state") + return nil, nil + } + proposal, err := k.Proposals.Get(ctx, proposalID) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + // is cancellable? copied from keeper + if proposal.VotingEndTime != nil && proposal.VotingEndTime.Before(sdk.UnwrapSDKContext(ctx).BlockTime()) { + reporter.Skip("not cancellable anymore") + return nil, nil + } + + from := testData.GetAccount(reporter, proposal.Proposer) + if from.LiquidBalance().Empty() { + reporter.Skip("proposer is broke") + return nil, nil + } + msg := v1.NewMsgCancelProposal(proposalID, from.AddressBech32) + return []simsx.SimAccount{from}, msg + } +} + +func MsgSubmitLegacyProposalFactory(k *keeper.Keeper, contentSimFn simtypes.ContentSimulatorFn) simsx.SimMsgFactoryX { //nolint:staticcheck // used for legacy testing + return simsx.NewSimMsgFactoryWithFutureOps[*v1.MsgSubmitProposal](func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter, fOpsReg simsx.FutureOpsRegistry) ([]simsx.SimAccount, *v1.MsgSubmitProposal) { + // 1) submit proposal now + accs := testData.AllAccounts() + content := contentSimFn(testData.Rand().Rand, sdk.UnwrapSDKContext(ctx), accs) + if content == nil { + reporter.Skip("content is nil") + return nil, nil + } + govacc := must(testData.AddressCodec().BytesToString(k.GetGovernanceAccount(ctx).GetAddress())) + contentMsg := must(v1.NewLegacyContent(content, govacc)) + return submitProposalWithVotesScheduled(ctx, k, testData, reporter, fOpsReg, contentMsg) + }) +} + +func MsgSubmitProposalFactory(k *keeper.Keeper, payloadFactory simsx.FactoryMethod) simsx.SimMsgFactoryX { + return simsx.NewSimMsgFactoryWithFutureOps[*v1.MsgSubmitProposal](func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter, fOpsReg simsx.FutureOpsRegistry) ([]simsx.SimAccount, *v1.MsgSubmitProposal) { + _, proposalMsg := payloadFactory(ctx, testData, reporter) + return submitProposalWithVotesScheduled(ctx, k, testData, reporter, fOpsReg, proposalMsg) + }) +} + +func submitProposalWithVotesScheduled( + ctx context.Context, + k *keeper.Keeper, + testData *simsx.ChainDataSource, + reporter simsx.SimulationReporter, + fOpsReg simsx.FutureOpsRegistry, + proposalMsgs ...sdk.Msg, +) ([]simsx.SimAccount, *v1.MsgSubmitProposal) { + r := testData.Rand() + expedited := r.Bool() + params := must(k.Params.Get(ctx)) + minDeposits := params.MinDeposit + if expedited { + minDeposits = params.ExpeditedMinDeposit + } + minDeposit := r.Coin(minDeposits) + + minDepositRatio := must(sdkmath.LegacyNewDecFromStr(params.GetMinDepositRatio())) + threshold := minDeposit.Amount.ToLegacyDec().Mul(minDepositRatio).TruncateInt() + + minDepositPercent := must(sdkmath.LegacyNewDecFromStr(params.MinInitialDepositRatio)) + minAmount := sdkmath.LegacyNewDecFromInt(minDeposit.Amount).Mul(minDepositPercent).TruncateInt() + amount, err := r.PositiveSDKIntn(minDeposit.Amount.Sub(minAmount)) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + if amount.LT(threshold) { + reporter.Skip("below threshold amount for proposal") + return nil, nil + } + deposit := minDeposit + // deposit := sdk.Coin{Amount: amount.Add(minAmount), Denom: minDeposit.Denom} + + proposer := testData.AnyAccount(reporter, simsx.WithLiquidBalanceGTE(deposit)) + if reporter.IsSkipped() || !proposer.LiquidBalance().BlockAmount(deposit) { + return nil, nil + } + msg, err := v1.NewMsgSubmitProposal( + proposalMsgs, + sdk.Coins{deposit}, + proposer.AddressBech32, + r.StringN(100), + r.StringN(100), + r.StringN(100), + expedited, + ) + if err != nil { + reporter.Skip("unable to generate a submit proposal msg") + return nil, nil + } + // futureOps + var ( + // 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 = must(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 + ) + + // get the submitted proposal ID + proposalID := must(k.ProposalID.Peek(ctx)) + + // 2) Schedule operations for votes + // 2.1) first pick a number of people to vote. + curNumVotesState = numVotesTransitionMatrix.NextState(r.Rand, curNumVotesState) + numVotes := int(math.Ceil(float64(testData.AccountsCount()) * statePercentageArray[curNumVotesState])) + + // 2.2) select who votes and when + whoVotes := r.Perm(testData.AccountsCount()) + + // didntVote := whoVotes[numVotes:] + whoVotes = whoVotes[:numVotes] + votingPeriod := params.VotingPeriod + // future ops so that votes do not flood the sims. + if r.Intn(100) == 1 { // 1% chance + now := simsx.BlockTime(ctx) + for i := 0; i < numVotes; i++ { + var vF simsx.SimMsgFactoryFn[*v1.MsgVote] = func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *v1.MsgVote) { + switch p, err := k.Proposals.Get(ctx, proposalID); { + case err != nil: + reporter.Skip(err.Error()) + return nil, nil + case p.Status != v1.ProposalStatus_PROPOSAL_STATUS_VOTING_PERIOD: + reporter.Skip("proposal not in voting period") + return nil, nil + } + voter := testData.AccountAt(reporter, whoVotes[i]) + msg := v1.NewMsgVote(voter.Address, proposalID, randomVotingOption(r.Rand), "") + return []simsx.SimAccount{voter}, msg + } + whenVote := now.Add(time.Duration(r.Int63n(int64(votingPeriod.Seconds()))) * time.Second) + fOpsReg.Add(whenVote, vF) + } + } + return []simsx.SimAccount{proposer}, msg +} + +// TextProposalFactory returns a random text proposal content. +// A text proposal is a proposal that contains no msgs. +func TextProposalFactory() simsx.SimMsgFactoryFn[sdk.Msg] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, sdk.Msg) { + return nil, nil + } +} + +func randDeposit( + ctx context.Context, + proposal v1.Proposal, + k *keeper.Keeper, + r *simsx.XRand, + reporter simsx.SimulationReporter, +) sdk.Coin { + params, err := k.Params.Get(ctx) + if err != nil { + reporter.Skipf("gov params: %s", err) + return sdk.Coin{} + } + minDeposits := params.MinDeposit + if proposal.Expedited { + minDeposits = params.ExpeditedMinDeposit + } + minDeposit := simsx.OneOf(r, minDeposits) + minDepositRatio, err := sdkmath.LegacyNewDecFromStr(params.GetMinDepositRatio()) + if err != nil { + reporter.Skip(err.Error()) + return sdk.Coin{} + } + + threshold := minDeposit.Amount.ToLegacyDec().Mul(minDepositRatio).TruncateInt() + depositAmount, err := r.PositiveSDKIntInRange(threshold, minDeposit.Amount) + if err != nil { + reporter.Skipf("deposit amount: %s", err) + return sdk.Coin{} + } + return sdk.Coin{Denom: minDeposit.Denom, Amount: depositAmount} +} + +// Pick a random proposal ID between the initial proposal ID +// (defined in gov GenesisState) and the latest proposal ID +// that matches a given Status. +// It does not provide a default ID. +func randomProposalID(r *rand.Rand, k *keeper.Keeper, ctx sdk.Context, status v1.ProposalStatus, s *SharedState) (proposalID uint64, found bool) { + proposalID, _ = k.ProposalID.Peek(ctx) + if initialProposalID := s.getMinProposalID(); initialProposalID == unsetProposalID { + s.setMinProposalID(proposalID) + } else if initialProposalID < proposalID { + proposalID = uint64(simtypes.RandIntBetween(r, int(initialProposalID), int(proposalID))) + } + proposal, err := k.Proposals.Get(ctx, proposalID) + if err != nil || proposal.Status != status { + return proposalID, false + } + + return proposalID, true +} + +// Pick a random weighted voting options +func randomWeightedVotingOptions(r *rand.Rand) v1.WeightedVoteOptions { + w1 := r.Intn(100 + 1) + w2 := r.Intn(100 - w1 + 1) + w3 := r.Intn(100 - w1 - w2 + 1) + w4 := 100 - w1 - w2 - w3 + weightedVoteOptions := v1.WeightedVoteOptions{} + if w1 > 0 { + weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ + Option: v1.OptionYes, + Weight: sdkmath.LegacyNewDecWithPrec(int64(w1), 2).String(), + }) + } + if w2 > 0 { + weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ + Option: v1.OptionAbstain, + Weight: sdkmath.LegacyNewDecWithPrec(int64(w2), 2).String(), + }) + } + if w3 > 0 { + weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ + Option: v1.OptionNo, + Weight: sdkmath.LegacyNewDecWithPrec(int64(w3), 2).String(), + }) + } + if w4 > 0 { + weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ + Option: v1.OptionNoWithVeto, + Weight: sdkmath.LegacyNewDecWithPrec(int64(w4), 2).String(), + }) + } + return weightedVoteOptions +} + +func randomVotingOption(r *rand.Rand) v1.VoteOption { + switch r.Intn(4) { + case 0: + return v1.OptionYes + case 1: + return v1.OptionAbstain + case 2: + return v1.OptionNo + case 3: + return v1.OptionNoWithVeto + default: + panic("invalid vote option") + } +} + +func must[T any](r T, err error) T { + if err != nil { + panic(err) + } + return r +} + +const unsetProposalID = 100000000000000 + +// SharedState shared state between message invocations +type SharedState struct { + minProposalID atomic.Uint64 +} + +// NewSharedState constructor +func NewSharedState() *SharedState { + r := &SharedState{} + r.setMinProposalID(unsetProposalID) + return r +} + +func (s *SharedState) getMinProposalID() uint64 { + return s.minProposalID.Load() +} + +func (s *SharedState) setMinProposalID(id uint64) { + s.minProposalID.Store(id) +} diff --git a/x/gov/simulation/operations.go b/x/gov/simulation/operations.go index b99d6179ed..a0cace28f7 100644 --- a/x/gov/simulation/operations.go +++ b/x/gov/simulation/operations.go @@ -3,7 +3,6 @@ package simulation import ( "math" "math/rand" - "sync/atomic" "time" sdkmath "cosmossdk.io/math" @@ -19,9 +18,8 @@ import ( "github.com/cosmos/cosmos-sdk/x/simulation" ) -const unsetProposalID = 100000000000000 - // Governance message types and routes +// will be removed in the future var ( TypeMsgDeposit = sdk.MsgTypeURL(&v1.MsgDeposit{}) TypeMsgVote = sdk.MsgTypeURL(&v1.MsgVote{}) @@ -31,6 +29,7 @@ var ( ) // Simulation operation weights constants +// will be removed in the future const ( OpWeightMsgDeposit = "op_weight_msg_deposit" OpWeightMsgVote = "op_weight_msg_vote" @@ -44,27 +43,8 @@ const ( DefaultWeightMsgCancelProposal = 5 ) -// sharedState shared state between message invocations -type sharedState struct { - minProposalID atomic.Uint64 -} - -// newSharedState constructor -func newSharedState() *sharedState { - r := &sharedState{} - r.setMinProposalID(unsetProposalID) - return r -} - -func (s *sharedState) getMinProposalID() uint64 { - return s.minProposalID.Load() -} - -func (s *sharedState) setMinProposalID(id uint64) { - s.minProposalID.Store(id) -} - // 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, @@ -138,7 +118,7 @@ func WeightedOperations( ), ) } - state := newSharedState() + state := NewSharedState() wGovOps := simulation.WeightedOperations{ simulation.NewWeightedOperation( weightMsgDeposit, @@ -164,6 +144,7 @@ func WeightedOperations( // 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, @@ -186,6 +167,7 @@ func SimulateMsgSubmitProposal( // 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, @@ -310,7 +292,7 @@ func simulateMsgSubmitProposal( whoVotes = whoVotes[:numVotes] params, _ := k.Params.Get(ctx) votingPeriod := params.VotingPeriod - s := newSharedState() + s := NewSharedState() fops := make([]simtypes.FutureOperation, numVotes+1) for i := 0; i < numVotes; i++ { whenVote := ctx.BlockHeader().Time.Add(time.Duration(r.Int63n(int64(votingPeriod.Seconds()))) * time.Second) @@ -325,14 +307,14 @@ func simulateMsgSubmitProposal( } // SimulateMsgDeposit generates a MsgDeposit with random values. -// Deprecated: this is an internal method and will be removed +// 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()) + return simulateMsgDeposit(txGen, ak, bk, k, NewSharedState()) } func simulateMsgDeposit( @@ -340,7 +322,7 @@ func simulateMsgDeposit( ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, @@ -403,7 +385,7 @@ func SimulateMsgVote( bk types.BankKeeper, k *keeper.Keeper, ) simtypes.Operation { - return simulateMsgVote(txGen, ak, bk, k, newSharedState()) + return simulateMsgVote(txGen, ak, bk, k, NewSharedState()) } func simulateMsgVote( @@ -411,7 +393,7 @@ func simulateMsgVote( ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return operationSimulateMsgVote(txGen, ak, bk, k, simtypes.Account{}, -1, s) } @@ -423,7 +405,7 @@ func operationSimulateMsgVote( k *keeper.Keeper, simAccount simtypes.Account, proposalIDInt int64, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, @@ -471,13 +453,14 @@ func operationSimulateMsgVote( } // 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()) + return simulateMsgVoteWeighted(txGen, ak, bk, k, NewSharedState()) } func simulateMsgVoteWeighted( @@ -485,7 +468,7 @@ func simulateMsgVoteWeighted( ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return operationSimulateMsgVoteWeighted(txGen, ak, bk, k, simtypes.Account{}, -1, s) } @@ -497,7 +480,7 @@ func operationSimulateMsgVoteWeighted( k *keeper.Keeper, simAccount simtypes.Account, proposalIDInt int64, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, @@ -545,6 +528,7 @@ func operationSimulateMsgVoteWeighted( } // SimulateMsgCancelProposal generates a MsgCancelProposal. +// will be removed in the future in favor of msg factory func SimulateMsgCancelProposal( txGen client.TxConfig, ak types.AccountKeeper, @@ -674,72 +658,3 @@ func randomProposal(r *rand.Rand, k *keeper.Keeper, ctx sdk.Context) *v1.Proposa randomIndex := r.Intn(len(proposals)) return proposals[randomIndex] } - -// Pick a random proposal ID between the initial proposal ID -// (defined in gov GenesisState) and the latest proposal ID -// that matches a given Status. -// It does not provide a default ID. -func randomProposalID(r *rand.Rand, k *keeper.Keeper, ctx sdk.Context, status v1.ProposalStatus, s *sharedState) (proposalID uint64, found bool) { - proposalID, _ = k.ProposalID.Peek(ctx) - if initialProposalID := s.getMinProposalID(); initialProposalID == unsetProposalID { - s.setMinProposalID(proposalID) - } else if initialProposalID < proposalID { - proposalID = uint64(simtypes.RandIntBetween(r, int(initialProposalID), int(proposalID))) - } - proposal, err := k.Proposals.Get(ctx, proposalID) - if err != nil || proposal.Status != status { - return proposalID, false - } - - return proposalID, true -} - -// Pick a random voting option -func randomVotingOption(r *rand.Rand) v1.VoteOption { - switch r.Intn(4) { - case 0: - return v1.OptionYes - case 1: - return v1.OptionAbstain - case 2: - return v1.OptionNo - case 3: - return v1.OptionNoWithVeto - default: - panic("invalid vote option") - } -} - -// Pick a random weighted voting options -func randomWeightedVotingOptions(r *rand.Rand) v1.WeightedVoteOptions { - w1 := r.Intn(100 + 1) - w2 := r.Intn(100 - w1 + 1) - w3 := r.Intn(100 - w1 - w2 + 1) - w4 := 100 - w1 - w2 - w3 - weightedVoteOptions := v1.WeightedVoteOptions{} - if w1 > 0 { - weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ - Option: v1.OptionYes, - Weight: sdkmath.LegacyNewDecWithPrec(int64(w1), 2).String(), - }) - } - if w2 > 0 { - weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ - Option: v1.OptionAbstain, - Weight: sdkmath.LegacyNewDecWithPrec(int64(w2), 2).String(), - }) - } - if w3 > 0 { - weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ - Option: v1.OptionNo, - Weight: sdkmath.LegacyNewDecWithPrec(int64(w3), 2).String(), - }) - } - if w4 > 0 { - weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ - Option: v1.OptionNoWithVeto, - Weight: sdkmath.LegacyNewDecWithPrec(int64(w4), 2).String(), - }) - } - return weightedVoteOptions -} diff --git a/x/gov/simulation/proposals.go b/x/gov/simulation/proposals.go index 06c6a1d785..510f2f7358 100644 --- a/x/gov/simulation/proposals.go +++ b/x/gov/simulation/proposals.go @@ -10,9 +10,11 @@ import ( ) // OpWeightSubmitTextProposal app params key for text proposal +// will be removed in the future const OpWeightSubmitTextProposal = "op_weight_submit_text_proposal" // ProposalMsgs defines the module weighted proposals' contents +// will be removed in the future func ProposalMsgs() []simtypes.WeightedProposalMsg { return []simtypes.WeightedProposalMsg{ simulation.NewWeightedProposalMsg( @@ -25,6 +27,7 @@ func ProposalMsgs() []simtypes.WeightedProposalMsg { // SimulateTextProposal returns a random text proposal content. // A text proposal is a proposal that contains no msgs. +// will be removed in the future func SimulateTextProposal(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { return nil } diff --git a/x/group/keeper/keeper.go b/x/group/keeper/keeper.go index a085f540ba..b6076622ad 100644 --- a/x/group/keeper/keeper.go +++ b/x/group/keeper/keeper.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "cosmossdk.io/core/address" errorsmod "cosmossdk.io/errors" "cosmossdk.io/log" storetypes "cosmossdk.io/store/types" @@ -221,6 +222,10 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", fmt.Sprintf("x/%s", group.ModuleName)) } +func (k Keeper) AddressCodec() address.Codec { + return k.accKeeper.AddressCodec() +} + // GetGroupSequence returns the current value of the group table sequence func (k Keeper) GetGroupSequence(ctx sdk.Context) uint64 { return k.groupTable.Sequence().CurVal(ctx.KVStore(k.key)) diff --git a/x/group/module/module.go b/x/group/module/module.go index ff9aeb7deb..4c26305c03 100644 --- a/x/group/module/module.go +++ b/x/group/module/module.go @@ -18,6 +18,7 @@ import ( sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -161,6 +162,7 @@ func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { } // WeightedOperations returns the all the gov module operations with their respective weights. +// migrate to WeightedOperationsX. This method is ignored when WeightedOperationsX exists and will be removed in the future func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { return simulation.WeightedOperations( am.registry, @@ -169,6 +171,26 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp ) } +// WeightedOperationsX registers weighted group module operations for simulation. +func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Registry) { + s := simulation.NewSharedState() + // note: using old keys for backwards compatibility + reg.Add(weights.Get("msg_create_group", 100), simulation.MsgCreateGroupFactory()) + reg.Add(weights.Get("msg_update_group_admin", 5), simulation.MsgUpdateGroupAdminFactory(am.keeper, s)) + reg.Add(weights.Get("msg_update_group_metadata", 5), simulation.MsgUpdateGroupMetadataFactory(am.keeper, s)) + reg.Add(weights.Get("msg_update_group_members", 5), simulation.MsgUpdateGroupMembersFactory(am.keeper, s)) + reg.Add(weights.Get("msg_create_group_account", 50), simulation.MsgCreateGroupPolicyFactory(am.keeper, s)) + reg.Add(weights.Get("msg_create_group_with_policy", 50), simulation.MsgCreateGroupWithPolicyFactory()) + reg.Add(weights.Get("msg_update_group_account_admin", 5), simulation.MsgUpdateGroupPolicyAdminFactory(am.keeper, s)) + reg.Add(weights.Get("msg_update_group_account_decision_policy", 5), simulation.MsgUpdateGroupPolicyDecisionPolicyFactory(am.keeper, s)) + reg.Add(weights.Get("msg_update_group_account_metadata", 5), simulation.MsgUpdateGroupPolicyMetadataFactory(am.keeper, s)) + reg.Add(weights.Get("msg_submit_proposal", 2*90), simulation.MsgSubmitProposalFactory(am.keeper, s)) + reg.Add(weights.Get("msg_withdraw_proposal", 20), simulation.MsgWithdrawProposalFactory(am.keeper, s)) + reg.Add(weights.Get("msg_vote", 90), simulation.MsgVoteFactory(am.keeper, s)) + reg.Add(weights.Get("msg_exec", 90), simulation.MsgExecFactory(am.keeper, s)) + reg.Add(weights.Get("msg_leave_group", 5), simulation.MsgLeaveGroupFactory(am.keeper, s)) +} + // // App Wiring Setup // diff --git a/x/group/simulation/msg_factory.go b/x/group/simulation/msg_factory.go new file mode 100644 index 0000000000..90c92d2cc9 --- /dev/null +++ b/x/group/simulation/msg_factory.go @@ -0,0 +1,490 @@ +package simulation + +import ( + "context" + "slices" + "strconv" + "sync/atomic" + "time" + + "github.com/cosmos/cosmos-sdk/simsx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/group" + "github.com/cosmos/cosmos-sdk/x/group/keeper" +) + +func MsgCreateGroupFactory() simsx.SimMsgFactoryFn[*group.MsgCreateGroup] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgCreateGroup) { + admin := testData.AnyAccount(reporter, simsx.WithSpendableBalance()) + members := genGroupMembersX(testData, reporter) + msg := &group.MsgCreateGroup{Admin: admin.AddressBech32, Members: members, Metadata: testData.Rand().StringN(10)} + return []simsx.SimAccount{admin}, msg + } +} + +func MsgCreateGroupPolicyFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgCreateGroupPolicy] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgCreateGroupPolicy) { + groupInfo := randomGroupX(ctx, k, testData, reporter, s) + groupAdmin := testData.GetAccount(reporter, groupInfo.Admin) + if reporter.IsSkipped() { + return nil, nil + } + groupID := groupInfo.Id + + r := testData.Rand() + msg, err := group.NewMsgCreateGroupPolicy( + groupAdmin.Address, + groupID, + r.StringN(10), + &group.ThresholdDecisionPolicy{ + Threshold: strconv.Itoa(r.IntInRange(1, 10)), + Windows: &group.DecisionPolicyWindows{ + VotingPeriod: time.Second * time.Duration(30*24*60*60), + }, + }, + ) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + return []simsx.SimAccount{groupAdmin}, msg + } +} + +func MsgCreateGroupWithPolicyFactory() simsx.SimMsgFactoryFn[*group.MsgCreateGroupWithPolicy] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgCreateGroupWithPolicy) { + admin := testData.AnyAccount(reporter, simsx.WithSpendableBalance()) + members := genGroupMembersX(testData, reporter) + r := testData.Rand() + msg := &group.MsgCreateGroupWithPolicy{ + Admin: admin.AddressBech32, + Members: members, + GroupMetadata: r.StringN(10), + GroupPolicyMetadata: r.StringN(10), + GroupPolicyAsAdmin: r.Float32() < 0.5, + } + decisionPolicy := &group.ThresholdDecisionPolicy{ + Threshold: strconv.Itoa(r.IntInRange(1, 10)), + Windows: &group.DecisionPolicyWindows{ + VotingPeriod: time.Second * time.Duration(30*24*60*60), + }, + } + if err := msg.SetDecisionPolicy(decisionPolicy); err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + return []simsx.SimAccount{admin}, msg + } +} + +func MsgWithdrawProposalFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgWithdrawProposal] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgWithdrawProposal) { + groupInfo, groupPolicy := randomGroupPolicyX(ctx, testData, reporter, k, s) + if reporter.IsSkipped() { + return nil, nil + } + policy, err := groupPolicy.GetDecisionPolicy() + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + err = policy.Validate(*groupInfo, group.DefaultConfig()) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + proposalsResult, err := k.ProposalsByGroupPolicy(ctx, + &group.QueryProposalsByGroupPolicyRequest{Address: groupPolicy.Address}, + ) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + + now := simsx.BlockTime(ctx) + proposal := simsx.First(proposalsResult.GetProposals(), func(p *group.Proposal) bool { + return p.Status == group.PROPOSAL_STATUS_SUBMITTED && p.VotingPeriodEnd.After(now) + }) + if proposal == nil { + reporter.Skip("no proposal found") + return nil, nil + } + // select a random proposer + r := testData.Rand() + proposer := testData.GetAccount(reporter, simsx.OneOf(r, (*proposal).Proposers)) + + msg := &group.MsgWithdrawProposal{ + ProposalId: (*proposal).Id, + Address: proposer.AddressBech32, + } + return []simsx.SimAccount{proposer}, msg + } +} + +func MsgVoteFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgVote] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgVote) { + groupInfo, groupPolicy := randomGroupPolicyX(ctx, testData, reporter, k, s) + if reporter.IsSkipped() { + return nil, nil + } + proposalsResult, err := k.ProposalsByGroupPolicy(ctx, + &group.QueryProposalsByGroupPolicyRequest{Address: groupPolicy.Address}, + ) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + + now := simsx.BlockTime(ctx) + proposal := simsx.First(proposalsResult.GetProposals(), func(p *group.Proposal) bool { + return p.Status == group.PROPOSAL_STATUS_SUBMITTED && p.VotingPeriodEnd.After(now) + }) + if proposal == nil { + reporter.Skip("no proposal found") + return nil, nil + } + // select a random member + r := testData.Rand() + res, err := k.GroupMembers(ctx, &group.QueryGroupMembersRequest{GroupId: groupInfo.Id}) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + if len(res.Members) == 0 { + reporter.Skip("group has no members") + return nil, nil + } + voter := testData.GetAccount(reporter, simsx.OneOf(r, res.Members).Member.Address) + vRes, err := k.VotesByProposal(ctx, &group.QueryVotesByProposalRequest{ + ProposalId: (*proposal).Id, + }) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + if slices.ContainsFunc(vRes.Votes, func(v *group.Vote) bool { return v.Voter == voter.AddressBech32 }) { + reporter.Skip("voted already on proposal") + return nil, nil + } + + msg := &group.MsgVote{ + ProposalId: (*proposal).Id, + Voter: voter.AddressBech32, + Option: group.VOTE_OPTION_YES, + Metadata: r.StringN(10), + } + return []simsx.SimAccount{voter}, msg + } +} + +func MsgSubmitProposalFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgSubmitProposal] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgSubmitProposal) { + groupInfo, groupPolicy := randomGroupPolicyX(ctx, testData, reporter, k, s) + if reporter.IsSkipped() { + return nil, nil + } + // Return a no-op if we know the proposal cannot be created + policy, err := groupPolicy.GetDecisionPolicy() + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + + if err = policy.Validate(*groupInfo, group.DefaultConfig()); err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + + // Pick a random member from the group + r := testData.Rand() + res, err := k.GroupMembers(ctx, &group.QueryGroupMembersRequest{GroupId: groupInfo.Id}) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + if len(res.Members) == 0 { + reporter.Skip("group has no members") + return nil, nil + } + proposer := testData.GetAccount(reporter, simsx.OneOf(r, res.Members).Member.Address) + + msg := &group.MsgSubmitProposal{ + GroupPolicyAddress: groupPolicy.Address, + Proposers: []string{proposer.AddressBech32}, + Metadata: r.StringN(10), + Title: "Test Proposal", + Summary: "Summary of the proposal", + } + return []simsx.SimAccount{proposer}, msg + } +} + +func MsgExecFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgExec] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgExec) { + groupPolicy, policyAdmin := randomGroupPolicyWithAdmin(ctx, testData, reporter, k, s) + if reporter.IsSkipped() { + return nil, nil + } + proposalsResult, err := k.ProposalsByGroupPolicy(ctx, + &group.QueryProposalsByGroupPolicyRequest{Address: groupPolicy.Address}, + ) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + + proposal := simsx.First(proposalsResult.GetProposals(), func(p *group.Proposal) bool { + return p.Status == group.PROPOSAL_STATUS_ACCEPTED + }) + if proposal == nil { + reporter.Skip("no proposal found") + return nil, nil + } + + msg := &group.MsgExec{ + ProposalId: (*proposal).Id, + Executor: policyAdmin.AddressBech32, + } + return []simsx.SimAccount{policyAdmin}, msg + } +} + +func randomGroupPolicyWithAdmin(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter, k keeper.Keeper, s *SharedState) (*group.GroupPolicyInfo, simsx.SimAccount) { + for i := 0; i < 5; i++ { + _, groupPolicy := randomGroupPolicyX(ctx, testData, reporter, k, s) + if groupPolicy != nil && testData.HasAccount(groupPolicy.Admin) { + return groupPolicy, testData.GetAccount(reporter, groupPolicy.Admin) + } + } + reporter.Skip("no group policy found with a sims account") + return nil, simsx.SimAccount{} +} + +func MsgUpdateGroupMetadataFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupMetadata] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupMetadata) { + groupInfo := randomGroupX(ctx, k, testData, reporter, s) + groupAdmin := testData.GetAccount(reporter, groupInfo.Admin) + if reporter.IsSkipped() { + return nil, nil + } + msg := &group.MsgUpdateGroupMetadata{ + GroupId: groupInfo.Id, + Admin: groupAdmin.AddressBech32, + Metadata: testData.Rand().StringN(10), + } + return []simsx.SimAccount{groupAdmin}, msg + } +} + +func MsgUpdateGroupAdminFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupAdmin] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupAdmin) { + groupInfo := randomGroupX(ctx, k, testData, reporter, s) + groupAdmin := testData.GetAccount(reporter, groupInfo.Admin) + if reporter.IsSkipped() { + return nil, nil + } + newAdmin := testData.AnyAccount(reporter, simsx.ExcludeAccounts(groupAdmin)) + msg := &group.MsgUpdateGroupAdmin{ + GroupId: groupInfo.Id, + Admin: groupAdmin.AddressBech32, + NewAdmin: newAdmin.AddressBech32, + } + return []simsx.SimAccount{groupAdmin}, msg + } +} + +func MsgUpdateGroupMembersFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupMembers] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupMembers) { + groupInfo := randomGroupX(ctx, k, testData, reporter, s) + groupAdmin := testData.GetAccount(reporter, groupInfo.Admin) + if reporter.IsSkipped() { + return nil, nil + } + res, err := k.GroupMembers(ctx, &group.QueryGroupMembersRequest{GroupId: groupInfo.Id}) + if err != nil { + reporter.Skip("group members not found") + return nil, nil + } + oldMemberAddrs := simsx.Collect(res.Members, func(a *group.GroupMember) string { return a.Member.Address }) + members := genGroupMembersX(testData, reporter, simsx.ExcludeAddresses(oldMemberAddrs...)) + if len(res.Members) > 1 { + // set existing random group member weight to zero to remove from the group + obsoleteMember := simsx.OneOf(testData.Rand(), res.Members) + obsoleteMember.Member.Weight = "0" + members = append(members, group.MemberToMemberRequest(obsoleteMember.Member)) + } + msg := &group.MsgUpdateGroupMembers{ + GroupId: groupInfo.Id, + Admin: groupAdmin.AddressBech32, + MemberUpdates: members, + } + return []simsx.SimAccount{groupAdmin}, msg + } +} + +func MsgUpdateGroupPolicyAdminFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupPolicyAdmin] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupPolicyAdmin) { + groupPolicy, policyAdmin := randomGroupPolicyWithAdmin(ctx, testData, reporter, k, s) + if reporter.IsSkipped() { + return nil, nil + } + newAdmin := testData.AnyAccount(reporter, simsx.ExcludeAccounts(policyAdmin)) + msg := &group.MsgUpdateGroupPolicyAdmin{ + Admin: policyAdmin.AddressBech32, + GroupPolicyAddress: groupPolicy.Address, + NewAdmin: newAdmin.AddressBech32, + } + return []simsx.SimAccount{policyAdmin}, msg + } +} + +func MsgUpdateGroupPolicyDecisionPolicyFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupPolicyDecisionPolicy] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupPolicyDecisionPolicy) { + groupPolicy, policyAdmin := randomGroupPolicyWithAdmin(ctx, testData, reporter, k, s) + if reporter.IsSkipped() { + return nil, nil + } + r := testData.Rand() + policyAddr, err := k.AddressCodec().StringToBytes(groupPolicy.Address) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + msg, err := group.NewMsgUpdateGroupPolicyDecisionPolicy(policyAdmin.Address, policyAddr, &group.ThresholdDecisionPolicy{ + Threshold: strconv.Itoa(r.IntInRange(1, 10)), + Windows: &group.DecisionPolicyWindows{ + VotingPeriod: time.Second * time.Duration(r.IntInRange(100, 1000)), + }, + }) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + return []simsx.SimAccount{policyAdmin}, msg + } +} + +func MsgUpdateGroupPolicyMetadataFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupPolicyMetadata] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupPolicyMetadata) { + groupPolicy, policyAdmin := randomGroupPolicyWithAdmin(ctx, testData, reporter, k, s) + if reporter.IsSkipped() { + return nil, nil + } + msg := &group.MsgUpdateGroupPolicyMetadata{ + Admin: policyAdmin.AddressBech32, + GroupPolicyAddress: groupPolicy.Address, + Metadata: testData.Rand().StringN(10), + } + return []simsx.SimAccount{policyAdmin}, msg + } +} + +func MsgLeaveGroupFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgLeaveGroup] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgLeaveGroup) { + groupInfo := randomGroupX(ctx, k, testData, reporter, s) + if reporter.IsSkipped() { + return nil, nil + } + res, err := k.GroupMembers(ctx, &group.QueryGroupMembersRequest{GroupId: groupInfo.Id}) + if err != nil { + reporter.Skip("group members not found") + return nil, nil + } + if len(res.Members) == 0 { + reporter.Skip("group has no members") + return nil, nil + } + anyMember := simsx.OneOf(testData.Rand(), res.Members) + leaver := testData.GetAccount(reporter, anyMember.Member.Address) + msg := &group.MsgLeaveGroup{ + GroupId: groupInfo.Id, + Address: leaver.AddressBech32, + } + return []simsx.SimAccount{leaver}, msg + } +} + +func genGroupMembersX(testData *simsx.ChainDataSource, reporter simsx.SimulationReporter, filters ...simsx.SimAccountFilter) []group.MemberRequest { + r := testData.Rand() + membersCount := r.Intn(5) + 1 + members := make([]group.MemberRequest, membersCount) + uniqueAccountsFilter := simsx.UniqueAccounts() + for i := 0; i < membersCount && !reporter.IsSkipped(); i++ { + m := testData.AnyAccount(reporter, append(filters, uniqueAccountsFilter)...) + members[i] = group.MemberRequest{ + Address: m.AddressBech32, + Weight: strconv.Itoa(r.IntInRange(1, 10)), + Metadata: r.StringN(10), + } + } + return members +} + +func randomGroupX(ctx context.Context, k keeper.Keeper, testdata *simsx.ChainDataSource, reporter simsx.SimulationReporter, s *SharedState) *group.GroupInfo { + r := testdata.Rand() + groupID := k.GetGroupSequence(sdk.UnwrapSDKContext(ctx)) + if initialGroupID := s.getMinGroupID(); initialGroupID == unsetGroupID { + s.setMinGroupID(groupID) + } else if initialGroupID < groupID { + groupID = r.Uint64InRange(initialGroupID+1, groupID+1) + } + + // when groupID is 0, it proves that SimulateMsgCreateGroup has never been called. that is, no group exists in the chain + if groupID == 0 { + reporter.Skip("no group exists") + return nil + } + + res, err := k.GroupInfo(ctx, &group.QueryGroupInfoRequest{GroupId: groupID}) + if err != nil { + reporter.Skip(err.Error()) + return nil + } + return res.Info +} + +func randomGroupPolicyX( + ctx context.Context, + testdata *simsx.ChainDataSource, + reporter simsx.SimulationReporter, + k keeper.Keeper, + s *SharedState, +) (*group.GroupInfo, *group.GroupPolicyInfo) { + for i := 0; i < 5; i++ { + groupInfo := randomGroupX(ctx, k, testdata, reporter, s) + if reporter.IsSkipped() { + return nil, nil + } + groupID := groupInfo.Id + result, err := k.GroupPoliciesByGroup(ctx, &group.QueryGroupPoliciesByGroupRequest{GroupId: groupID}) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + if len(result.GroupPolicies) != 0 { + return groupInfo, simsx.OneOf(testdata.Rand(), result.GroupPolicies) + } + } + reporter.Skip("no group policies") + return nil, nil +} + +// SharedState shared state between message invocations +type SharedState struct { + minGroupID atomic.Uint64 +} + +// NewSharedState constructor +func NewSharedState() *SharedState { + r := &SharedState{} + r.setMinGroupID(unsetGroupID) + return r +} + +func (s *SharedState) getMinGroupID() uint64 { + return s.minGroupID.Load() +} + +func (s *SharedState) setMinGroupID(id uint64) { + s.minGroupID.Store(id) +} diff --git a/x/group/simulation/operations.go b/x/group/simulation/operations.go index 39beb76d00..b0a42d454d 100644 --- a/x/group/simulation/operations.go +++ b/x/group/simulation/operations.go @@ -5,7 +5,6 @@ import ( "fmt" "math/rand" "strings" - "sync/atomic" "time" "github.com/cosmos/cosmos-sdk/baseapp" @@ -23,6 +22,7 @@ import ( const unsetGroupID = 100000000000000 // group message types +// will be removed in the future var ( TypeMsgCreateGroup = sdk.MsgTypeURL(&group.MsgCreateGroup{}) TypeMsgUpdateGroupMembers = sdk.MsgTypeURL(&group.MsgUpdateGroupMembers{}) @@ -41,6 +41,7 @@ var ( ) // Simulation operation weights constants +// will be removed in the future const ( OpMsgCreateGroup = "op_weight_msg_create_group" OpMsgUpdateGroupAdmin = "op_weight_msg_update_group_admin" @@ -60,6 +61,7 @@ const ( // If update group or group policy txn's executed, `SimulateMsgVote` & `SimulateMsgExec` txn's returns `noOp`. // That's why we have less weight for update group & group-policy txn's. +// will be removed in the future const ( WeightMsgCreateGroup = 100 WeightMsgCreateGroupPolicy = 50 @@ -77,27 +79,8 @@ const ( WeightMsgCreateGroupWithPolicy = 50 ) -// sharedState shared state between message invocations -type sharedState struct { - minGroupID atomic.Uint64 -} - -// newSharedState constructor -func newSharedState() *sharedState { - r := &sharedState{} - r.setMinGroupID(unsetGroupID) - return r -} - -func (s *sharedState) getMinGroupID() uint64 { - return s.minGroupID.Load() -} - -func (s *sharedState) setMinGroupID(id uint64) { - s.minGroupID.Store(id) -} - // WeightedOperations returns all the operations from the module with their respective weights +// migrate to the msg factories instead, this method will be removed in the future func WeightedOperations( registry cdctypes.InterfaceRegistry, appParams simtypes.AppParams, _ codec.JSONCodec, txGen client.TxConfig, @@ -166,7 +149,7 @@ func WeightedOperations( pCdc := codec.NewProtoCodec(registry) - state := newSharedState() + state := NewSharedState() // create two proposals for weightedOperations var createProposalOps simulation.WeightedOperations @@ -239,7 +222,7 @@ func WeightedOperations( } // SimulateMsgCreateGroup generates a MsgCreateGroup with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgCreateGroup( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -287,7 +270,7 @@ func SimulateMsgCreateGroup( } // SimulateMsgCreateGroupWithPolicy generates a MsgCreateGroupWithPolicy with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgCreateGroupWithPolicy( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -352,7 +335,7 @@ func SimulateMsgCreateGroupWithPolicy( } // SimulateMsgCreateGroupPolicy generates a NewMsgCreateGroupPolicy with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgCreateGroupPolicy( cdc *codec.ProtoCodec, txGen client.TxConfig, @@ -360,7 +343,7 @@ func SimulateMsgCreateGroupPolicy( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgCreateGroupPolicy(cdc, txGen, ak, bk, k, newSharedState()) + return simulateMsgCreateGroupPolicy(cdc, txGen, ak, bk, k, NewSharedState()) } func simulateMsgCreateGroupPolicy( @@ -369,7 +352,7 @@ func simulateMsgCreateGroupPolicy( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -430,7 +413,7 @@ func simulateMsgCreateGroupPolicy( } // SimulateMsgSubmitProposal generates a NewMsgSubmitProposal with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgSubmitProposal( cdc *codec.ProtoCodec, txGen client.TxConfig, @@ -438,7 +421,7 @@ func SimulateMsgSubmitProposal( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgSubmitProposal(cdc, txGen, ak, bk, k, newSharedState()) + return simulateMsgSubmitProposal(cdc, txGen, ak, bk, k, NewSharedState()) } func simulateMsgSubmitProposal( @@ -447,7 +430,7 @@ func simulateMsgSubmitProposal( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -523,7 +506,7 @@ func simulateMsgSubmitProposal( } // SimulateMsgUpdateGroupAdmin generates a MsgUpdateGroupAdmin with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUpdateGroupAdmin( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -531,7 +514,7 @@ func SimulateMsgUpdateGroupAdmin( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgUpdateGroupAdmin(txGen, ak, bk, k, newSharedState()) + return simulateMsgUpdateGroupAdmin(txGen, ak, bk, k, NewSharedState()) } func simulateMsgUpdateGroupAdmin( @@ -539,7 +522,7 @@ func simulateMsgUpdateGroupAdmin( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -599,7 +582,7 @@ func simulateMsgUpdateGroupAdmin( } // SimulateMsgUpdateGroupMetadata generates a MsgUpdateGroupMetadata with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUpdateGroupMetadata( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -607,7 +590,7 @@ func SimulateMsgUpdateGroupMetadata( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgUpdateGroupMetadata(txGen, ak, bk, k, newSharedState()) + return simulateMsgUpdateGroupMetadata(txGen, ak, bk, k, NewSharedState()) } func simulateMsgUpdateGroupMetadata( @@ -615,7 +598,7 @@ func simulateMsgUpdateGroupMetadata( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -666,7 +649,7 @@ func simulateMsgUpdateGroupMetadata( } // SimulateMsgUpdateGroupMembers generates a MsgUpdateGroupMembers with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUpdateGroupMembers( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -674,7 +657,7 @@ func SimulateMsgUpdateGroupMembers( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgUpdateGroupMembers(txGen, ak, bk, k, newSharedState()) + return simulateMsgUpdateGroupMembers(txGen, ak, bk, k, NewSharedState()) } func simulateMsgUpdateGroupMembers( @@ -682,7 +665,7 @@ func simulateMsgUpdateGroupMembers( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -760,7 +743,7 @@ func simulateMsgUpdateGroupMembers( } // SimulateMsgUpdateGroupPolicyAdmin generates a MsgUpdateGroupPolicyAdmin with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUpdateGroupPolicyAdmin( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -768,7 +751,7 @@ func SimulateMsgUpdateGroupPolicyAdmin( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgUpdateGroupPolicyAdmin(txGen, ak, bk, k, newSharedState()) + return simulateMsgUpdateGroupPolicyAdmin(txGen, ak, bk, k, NewSharedState()) } func simulateMsgUpdateGroupPolicyAdmin( @@ -776,7 +759,7 @@ func simulateMsgUpdateGroupPolicyAdmin( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -836,7 +819,7 @@ func simulateMsgUpdateGroupPolicyAdmin( } // SimulateMsgUpdateGroupPolicyDecisionPolicy generates a NewMsgUpdateGroupPolicyDecisionPolicy with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUpdateGroupPolicyDecisionPolicy( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -844,7 +827,7 @@ func SimulateMsgUpdateGroupPolicyDecisionPolicy( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgUpdateGroupPolicyDecisionPolicy(txGen, ak, bk, k, newSharedState()) + return simulateMsgUpdateGroupPolicyDecisionPolicy(txGen, ak, bk, k, NewSharedState()) } func simulateMsgUpdateGroupPolicyDecisionPolicy( @@ -852,7 +835,7 @@ func simulateMsgUpdateGroupPolicyDecisionPolicy( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -911,7 +894,7 @@ func simulateMsgUpdateGroupPolicyDecisionPolicy( } // SimulateMsgUpdateGroupPolicyMetadata generates a MsgUpdateGroupPolicyMetadata with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUpdateGroupPolicyMetadata( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -919,7 +902,7 @@ func SimulateMsgUpdateGroupPolicyMetadata( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgUpdateGroupPolicyMetadata(txGen, ak, bk, k, newSharedState()) + return simulateMsgUpdateGroupPolicyMetadata(txGen, ak, bk, k, NewSharedState()) } func simulateMsgUpdateGroupPolicyMetadata( @@ -927,7 +910,7 @@ func simulateMsgUpdateGroupPolicyMetadata( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -978,7 +961,7 @@ func simulateMsgUpdateGroupPolicyMetadata( } // SimulateMsgWithdrawProposal generates a MsgWithdrawProposal with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgWithdrawProposal( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -986,7 +969,7 @@ func SimulateMsgWithdrawProposal( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgWithdrawProposal(txGen, ak, bk, k, newSharedState()) + return simulateMsgWithdrawProposal(txGen, ak, bk, k, NewSharedState()) } // simulateMsgWithdrawProposal generates a MsgWithdrawProposal with random values @@ -995,7 +978,7 @@ func simulateMsgWithdrawProposal( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -1097,7 +1080,7 @@ func simulateMsgWithdrawProposal( } // SimulateMsgVote generates a MsgVote with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgVote( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -1105,7 +1088,7 @@ func SimulateMsgVote( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgVote(txGen, ak, bk, k, newSharedState()) + return simulateMsgVote(txGen, ak, bk, k, NewSharedState()) } func simulateMsgVote( @@ -1113,7 +1096,7 @@ func simulateMsgVote( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -1215,7 +1198,7 @@ func simulateMsgVote( } // SimulateMsgExec generates a MsgExec with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgExec( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -1223,7 +1206,7 @@ func SimulateMsgExec( bk group.BankKeeper, k keeper.Keeper, ) simtypes.Operation { - return simulateMsgExec(txGen, ak, bk, k, newSharedState()) + return simulateMsgExec(txGen, ak, bk, k, NewSharedState()) } func simulateMsgExec( @@ -1231,7 +1214,7 @@ func simulateMsgExec( ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -1306,7 +1289,7 @@ func simulateMsgExec( } // SimulateMsgLeaveGroup generates a MsgLeaveGroup with random values -// Deprecated: this is an internal method and will be removed +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgLeaveGroup( _ *codec.ProtoCodec, txGen client.TxConfig, @@ -1314,7 +1297,7 @@ func SimulateMsgLeaveGroup( ak group.AccountKeeper, bk group.BankKeeper, ) simtypes.Operation { - return simulateMsgLeaveGroup(txGen, k, ak, bk, newSharedState()) + return simulateMsgLeaveGroup(txGen, k, ak, bk, NewSharedState()) } func simulateMsgLeaveGroup( @@ -1322,7 +1305,7 @@ func simulateMsgLeaveGroup( k keeper.Keeper, ak group.AccountKeeper, bk group.BankKeeper, - s *sharedState, + s *SharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, @@ -1381,7 +1364,7 @@ func simulateMsgLeaveGroup( } func randomGroup(r *rand.Rand, k keeper.Keeper, ak group.AccountKeeper, - ctx sdk.Context, accounts []simtypes.Account, s *sharedState, + ctx sdk.Context, accounts []simtypes.Account, s *SharedState, ) (groupInfo *group.GroupInfo, acc simtypes.Account, account sdk.AccountI, err error) { groupID := k.GetGroupSequence(ctx) @@ -1419,7 +1402,7 @@ func randomGroup(r *rand.Rand, k keeper.Keeper, ak group.AccountKeeper, } func randomGroupPolicy(r *rand.Rand, k keeper.Keeper, ak group.AccountKeeper, - ctx sdk.Context, accounts []simtypes.Account, s *sharedState, + ctx sdk.Context, accounts []simtypes.Account, s *SharedState, ) (groupInfo *group.GroupInfo, groupPolicyInfo *group.GroupPolicyInfo, acc simtypes.Account, account sdk.AccountI, err error) { groupInfo, _, _, err = randomGroup(r, k, ak, ctx, accounts, s) if err != nil { diff --git a/x/mint/module.go b/x/mint/module.go index 7d10342d96..035e04236e 100644 --- a/x/mint/module.go +++ b/x/mint/module.go @@ -15,6 +15,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -170,10 +171,16 @@ func (AppModule) GenerateGenesisState(simState *module.SimulationState) { } // ProposalMsgs returns msgs used for governance proposals for simulations. +// migrate to ProposalMsgsX. This method is ignored when ProposalMsgsX exists and will be removed in the future. func (AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { return simulation.ProposalMsgs() } +// ProposalMsgsX returns msgs used for governance proposals for simulations. +func (AppModule) ProposalMsgsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("msg_update_params", 100), simulation.MsgUpdateParamsFactory()) +} + // RegisterStoreDecoder registers a decoder for mint module's types. func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { sdr[types.StoreKey] = simtypes.NewStoreDecoderFuncFromCollectionsSchema(am.keeper.Schema) diff --git a/x/mint/simulation/genesis.go b/x/mint/simulation/genesis.go index 48ae9c3acf..6b89e0f6f6 100644 --- a/x/mint/simulation/genesis.go +++ b/x/mint/simulation/genesis.go @@ -1,8 +1,6 @@ package simulation import ( - "encoding/json" - "fmt" "math/rand" "cosmossdk.io/math" @@ -69,11 +67,5 @@ func RandomizedGenState(simState *module.SimulationState) { params := types.NewParams(mintDenom, inflationRateChange, inflationMax, inflationMin, goalBonded, blocksPerYear) mintGenesis := types.NewGenesisState(types.InitialMinter(inflation), params) - - bz, err := json.MarshalIndent(&mintGenesis, "", " ") - if err != nil { - panic(err) - } - fmt.Printf("Selected randomly generated minting parameters:\n%s\n", bz) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(mintGenesis) } diff --git a/x/mint/simulation/msg_factory.go b/x/mint/simulation/msg_factory.go new file mode 100644 index 0000000000..66a11e8747 --- /dev/null +++ b/x/mint/simulation/msg_factory.go @@ -0,0 +1,29 @@ +package simulation + +import ( + "context" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/simsx" + "github.com/cosmos/cosmos-sdk/x/mint/types" +) + +// MsgUpdateParamsFactory creates a gov proposal for param updates +func MsgUpdateParamsFactory() simsx.SimMsgFactoryFn[*types.MsgUpdateParams] { + return func(_ context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgUpdateParams) { + r := testData.Rand() + params := types.DefaultParams() + params.BlocksPerYear = r.Uint64InRange(1, 1_000_000) + params.GoalBonded = sdkmath.LegacyNewDecWithPrec(int64(r.IntInRange(1, 100)), 2) + params.InflationMin = sdkmath.LegacyNewDecWithPrec(int64(r.IntInRange(1, 50)), 2) + params.InflationMax = sdkmath.LegacyNewDecWithPrec(int64(r.IntInRange(50, 100)), 2) + params.InflationRateChange = sdkmath.LegacyNewDecWithPrec(int64(r.IntInRange(1, 100)), 2) + params.MintDenom = r.StringN(10) + + return nil, &types.MsgUpdateParams{ + Authority: testData.ModuleAccountAddress(reporter, "gov"), + Params: params, + } + } +} diff --git a/x/mint/simulation/proposals.go b/x/mint/simulation/proposals.go index f39a150585..4b5b5cb8dd 100644 --- a/x/mint/simulation/proposals.go +++ b/x/mint/simulation/proposals.go @@ -13,6 +13,7 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( DefaultWeightMsgUpdateParams int = 100 @@ -20,6 +21,7 @@ const ( ) // ProposalMsgs defines the module weighted proposals' contents +// migrate to the msg factories instead, this method will be removed in the future func ProposalMsgs() []simtypes.WeightedProposalMsg { return []simtypes.WeightedProposalMsg{ simulation.NewWeightedProposalMsg( @@ -31,6 +33,7 @@ func ProposalMsgs() []simtypes.WeightedProposalMsg { } // SimulateMsgUpdateParams returns a random MsgUpdateParams +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { // use the default gov module account address as authority var authority sdk.AccAddress = address.Module("gov") diff --git a/x/nft/module/module.go b/x/nft/module/module.go index 2728032358..9278f8a89c 100644 --- a/x/nft/module/module.go +++ b/x/nft/module/module.go @@ -20,6 +20,7 @@ import ( sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -144,6 +145,7 @@ func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { } // WeightedOperations returns the all the nft module operations with their respective weights. +// migrate to WeightedOperationsX. This method is ignored when WeightedOperationsX exists and will be removed in the future func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { return simulation.WeightedOperations( am.registry, @@ -152,6 +154,11 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp ) } +// WeightedOperationsX registers weighted nft module operations for simulation. +func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("msg_send", 100), simulation.MsgSendFactory(am.keeper)) +} + // // App Wiring Setup // diff --git a/x/nft/simulation/msg_factory.go b/x/nft/simulation/msg_factory.go new file mode 100644 index 0000000000..732a7de0f8 --- /dev/null +++ b/x/nft/simulation/msg_factory.go @@ -0,0 +1,34 @@ +package simulation + +import ( + "context" + + "cosmossdk.io/x/nft" + "cosmossdk.io/x/nft/keeper" + + "github.com/cosmos/cosmos-sdk/simsx" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func MsgSendFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*nft.MsgSend] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *nft.MsgSend) { + from := testData.AnyAccount(reporter, simsx.WithSpendableBalance()) + to := testData.AnyAccount(reporter, simsx.ExcludeAccounts(from)) + if reporter.IsSkipped() { + return nil, nil + } + n, err := randNFT(sdk.UnwrapSDKContext(ctx), testData.Rand().Rand, k, from.Address) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + msg := &nft.MsgSend{ + ClassId: n.ClassId, + Id: n.Id, + Sender: from.AddressBech32, + Receiver: to.AddressBech32, + } + + return []simsx.SimAccount{from}, msg + } +} diff --git a/x/nft/simulation/operations.go b/x/nft/simulation/operations.go index c7bdbaa2e7..64483f743e 100644 --- a/x/nft/simulation/operations.go +++ b/x/nft/simulation/operations.go @@ -16,6 +16,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/simulation" ) +// will be removed in the future const ( // OpWeightMsgSend Simulation operation weights constants OpWeightMsgSend = "op_weight_msg_send" @@ -24,9 +25,11 @@ const ( WeightSend = 100 ) +// will be removed in the future var TypeMsgSend = sdk.MsgTypeURL(&nft.MsgSend{}) // WeightedOperations returns all the operations from the module with their respective weights +// migrate to the msg factories instead, this method will be removed in the future func WeightedOperations( registry cdctypes.InterfaceRegistry, appParams simtypes.AppParams, @@ -53,6 +56,7 @@ func WeightedOperations( } // SimulateMsgSend generates a MsgSend with random values. +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgSend( _ *codec.ProtoCodec, txCfg client.TxConfig, diff --git a/x/params/simulation/operations.go b/x/params/simulation/operations.go index 2d9a862dc4..9e1a2f19b7 100644 --- a/x/params/simulation/operations.go +++ b/x/params/simulation/operations.go @@ -19,6 +19,7 @@ func min(a, b int) int { // SimulateParamChangeProposalContent returns random parameter change content. // It will generate a ParameterChangeProposal object with anywhere between 1 and // the total amount of defined parameters changes, all of which have random valid values. +// Deprecated: This method will be removed in the future func SimulateParamChangeProposalContent(paramChangePool []simulation.LegacyParamChange) simulation.ContentSimulatorFn { //nolint:staticcheck // used for legacy testing numProposals := 0 // Bound the maximum number of simultaneous parameter changes diff --git a/x/params/simulation/proposals.go b/x/params/simulation/proposals.go index 8dc636fa05..512313ff75 100644 --- a/x/params/simulation/proposals.go +++ b/x/params/simulation/proposals.go @@ -5,6 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/simulation" ) +// will be removed in the future const ( // OpWeightSubmitParamChangeProposal app params key for param change proposal OpWeightSubmitParamChangeProposal = "op_weight_submit_param_change_proposal" @@ -13,6 +14,8 @@ const ( // ProposalContents defines the module weighted proposals' contents // +// will be removed in the future +// //nolint:staticcheck // used for legacy testing func ProposalContents(paramChanges []simtypes.LegacyParamChange) []simtypes.WeightedProposalContent { return []simtypes.WeightedProposalContent{ diff --git a/x/protocolpool/module.go b/x/protocolpool/module.go index ba7641533c..9a029e913e 100644 --- a/x/protocolpool/module.go +++ b/x/protocolpool/module.go @@ -12,6 +12,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -141,6 +142,7 @@ func (AppModule) GenerateGenesisState(simState *module.SimulationState) { } // ProposalMsgs returns msgs used for governance proposals for simulations. +// migrate to ProposalMsgsX. This method is ignored when ProposalMsgsX exists and will be removed in the future. func (AppModule) ProposalMsgs(_ module.SimulationState) []simtypes.WeightedProposalMsg { return simulation.ProposalMsgs() } @@ -149,6 +151,8 @@ func (AppModule) ProposalMsgs(_ module.SimulationState) []simtypes.WeightedPropo func (am AppModule) RegisterStoreDecoder(_ simtypes.StoreDecoderRegistry) { } +// WeightedOperations returns the all the protocolpool module operations with their respective weights. +// migrate to WeightedOperationsX. This method is ignored when WeightedOperationsX exists and will be removed in the future func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { return simulation.WeightedOperations( simState.AppParams, @@ -158,3 +162,13 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp am.keeper, ) } + +// ProposalMsgsX registers governance proposal messages in the simulation registry. +func (am AppModule) ProposalMsgsX(weight simsx.WeightSource, reg simsx.Registry) { + reg.Add(weight.Get("msg_community_pool_spend", 50), simulation.MsgCommunityPoolSpendFactory()) +} + +// WeightedOperationsX registers weighted protocolpool module operations for simulation. +func (am AppModule) WeightedOperationsX(weight simsx.WeightSource, reg simsx.Registry) { + reg.Add(weight.Get("msg_fund_community_pool", 50), simulation.MsgFundCommunityPoolFactory()) +} diff --git a/x/protocolpool/simulation/msg_factory.go b/x/protocolpool/simulation/msg_factory.go new file mode 100644 index 0000000000..d9327a0d41 --- /dev/null +++ b/x/protocolpool/simulation/msg_factory.go @@ -0,0 +1,36 @@ +package simulation + +import ( + "context" + + "github.com/cosmos/cosmos-sdk/simsx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/protocolpool/types" +) + +func MsgFundCommunityPoolFactory() simsx.SimMsgFactoryFn[*types.MsgFundCommunityPool] { + return func(_ context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgFundCommunityPool) { + funder := testData.AnyAccount(reporter, simsx.WithSpendableBalance()) + fundAmount := funder.LiquidBalance().RandSubsetCoins(reporter) + msg := types.NewMsgFundCommunityPool(fundAmount, funder.AddressBech32) + return []simsx.SimAccount{funder}, msg + } +} + +// MsgCommunityPoolSpendFactory creates a gov proposal to send tokens from the community pool to a random account +func MsgCommunityPoolSpendFactory() simsx.SimMsgFactoryFn[*types.MsgCommunityPoolSpend] { + return func(_ context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgCommunityPoolSpend) { + return nil, &types.MsgCommunityPoolSpend{ + Authority: testData.ModuleAccountAddress(reporter, "gov"), + Recipient: testData.AnyAccount(reporter).AddressBech32, + Amount: must(sdk.ParseCoinsNormalized("100stake,2testtoken")), + } + } +} + +func must[T any](r T, err error) T { + if err != nil { + panic(err) + } + return r +} diff --git a/x/slashing/module.go b/x/slashing/module.go index 6fd9b89b01..51572037d7 100644 --- a/x/slashing/module.go +++ b/x/slashing/module.go @@ -15,6 +15,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -174,6 +175,7 @@ func (AppModule) GenerateGenesisState(simState *module.SimulationState) { } // ProposalMsgs returns msgs used for governance proposals for simulations. +// migrate to WeightedOperationsX. This method is ignored when WeightedOperationsX exists and will be removed in the future func (AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { return simulation.ProposalMsgs() } @@ -183,6 +185,11 @@ func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) } +// ProposalMsgsX returns msgs used for governance proposals for simulations. +func (AppModule) ProposalMsgsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("msg_update_params", 100), simulation.MsgUpdateParamsFactory()) +} + // WeightedOperations returns the all the slashing module operations with their respective weights. func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { return simulation.WeightedOperations( @@ -191,6 +198,12 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp ) } +// WeightedOperationsX registers weighted slashing module operations for simulation. +func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Registry) { + // note: using old keys for backwards compatibility + reg.Add(weights.Get("msg_unjail", 20), simulation.MsgUnjailFactory(am.keeper, am.stakingKeeper)) +} + // // App Wiring Setup // diff --git a/x/slashing/simulation/genesis.go b/x/slashing/simulation/genesis.go index 533cad0ecb..af2e9fcc69 100644 --- a/x/slashing/simulation/genesis.go +++ b/x/slashing/simulation/genesis.go @@ -1,8 +1,6 @@ package simulation import ( - "encoding/json" - "fmt" "math/rand" "time" @@ -70,11 +68,5 @@ func RandomizedGenState(simState *module.SimulationState) { ) slashingGenesis := types.NewGenesisState(params, []types.SigningInfo{}, []types.ValidatorMissedBlocks{}) - - bz, err := json.MarshalIndent(&slashingGenesis, "", " ") - if err != nil { - panic(err) - } - fmt.Printf("Selected randomly generated slashing parameters:\n%s\n", bz) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(slashingGenesis) } diff --git a/x/slashing/simulation/msg_factory.go b/x/slashing/simulation/msg_factory.go new file mode 100644 index 0000000000..f635da98a5 --- /dev/null +++ b/x/slashing/simulation/msg_factory.go @@ -0,0 +1,98 @@ +package simulation + +import ( + "context" + "errors" + "time" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/simsx" + "github.com/cosmos/cosmos-sdk/x/slashing/keeper" + "github.com/cosmos/cosmos-sdk/x/slashing/types" +) + +func MsgUnjailFactory(k keeper.Keeper, sk types.StakingKeeper) simsx.SimMsgFactoryX { + return simsx.NewSimMsgFactoryWithDeliveryResultHandler[*types.MsgUnjail](func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgUnjail, simsx.SimDeliveryResultHandler) { + allVals, err := sk.GetAllValidators(ctx) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil, nil + } + validator := simsx.OneOf(testData.Rand(), allVals) + if !validator.IsJailed() { + reporter.Skip("validator not jailed") + return nil, nil, nil + } + if validator.InvalidExRate() { + reporter.Skip("validator with invalid exchange rate") + return nil, nil, nil + } + + info, err := k.GetValidatorSigningInfo(ctx, must(validator.GetConsAddr())) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil, nil + } + valOperBz := must(sk.ValidatorAddressCodec().StringToBytes(validator.GetOperator())) + valOper := testData.GetAccountbyAccAddr(reporter, valOperBz) + if reporter.IsSkipped() { + return nil, nil, nil + } + + selfDel, err := sk.Delegation(ctx, valOper.Address, valOperBz) + if selfDel == nil || err != nil { + reporter.Skip("no self delegation") + return nil, nil, nil + } + var handler simsx.SimDeliveryResultHandler + // result should fail if: + // - validator cannot be unjailed due to tombstone + // - validator is still in jailed period + // - self delegation too low + if info.Tombstoned || + simsx.BlockTime(ctx).Before(info.JailedUntil) || + selfDel.GetShares().IsNil() || + validator.TokensFromShares(selfDel.GetShares()).TruncateInt().LT(validator.GetMinSelfDelegation()) { + handler = func(err error) error { + if err == nil { + switch { + case info.Tombstoned: + return errors.New("validator should not have been unjailed if validator tombstoned") + case simsx.BlockTime(ctx).Before(info.JailedUntil): + return errors.New("validator unjailed while validator still in jail period") + case selfDel.GetShares().IsNil() || validator.TokensFromShares(selfDel.GetShares()).TruncateInt().LT(validator.GetMinSelfDelegation()): + return errors.New("validator unjailed even though self-delegation too low") + } + } + return nil + } + } + return []simsx.SimAccount{valOper}, types.NewMsgUnjail(validator.GetOperator()), handler + }) +} + +// MsgUpdateParamsFactory creates a gov proposal for param updates +func MsgUpdateParamsFactory() simsx.SimMsgFactoryFn[*types.MsgUpdateParams] { + return func(_ context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgUpdateParams) { + r := testData.Rand() + params := types.DefaultParams() + params.DowntimeJailDuration = time.Duration(r.Timestamp().UnixNano()) + params.SignedBlocksWindow = int64(r.IntInRange(1, 1000)) + params.MinSignedPerWindow = sdkmath.LegacyNewDecWithPrec(int64(r.IntInRange(1, 100)), 2) + params.SlashFractionDoubleSign = sdkmath.LegacyNewDecWithPrec(int64(r.IntInRange(1, 100)), 2) + params.SlashFractionDowntime = sdkmath.LegacyNewDecWithPrec(int64(r.IntInRange(1, 100)), 2) + + return nil, &types.MsgUpdateParams{ + Authority: testData.ModuleAccountAddress(reporter, "gov"), + Params: params, + } + } +} + +func must[T any](r T, err error) T { + if err != nil { + panic(err) + } + return r +} diff --git a/x/slashing/simulation/operations.go b/x/slashing/simulation/operations.go index 65fc2ca9d3..e2d6383cec 100644 --- a/x/slashing/simulation/operations.go +++ b/x/slashing/simulation/operations.go @@ -18,6 +18,7 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( OpWeightMsgUnjail = "op_weight_msg_unjail" @@ -25,6 +26,7 @@ const ( ) // WeightedOperations returns all the operations from the module with their respective weights +// migrate to the msg factories instead, this method will be removed in the future func WeightedOperations( registry codectypes.InterfaceRegistry, appParams simtypes.AppParams, @@ -49,6 +51,7 @@ func WeightedOperations( } // SimulateMsgUnjail generates a MsgUnjail with random values +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUnjail( cdc *codec.ProtoCodec, txGen client.TxConfig, diff --git a/x/slashing/simulation/proposals.go b/x/slashing/simulation/proposals.go index 7985e2290e..05180f649d 100644 --- a/x/slashing/simulation/proposals.go +++ b/x/slashing/simulation/proposals.go @@ -14,6 +14,7 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( DefaultWeightMsgUpdateParams int = 100 @@ -21,6 +22,7 @@ const ( ) // ProposalMsgs defines the module weighted proposals' contents +// migrate to the msg factories instead, this method will be removed in the future func ProposalMsgs() []simtypes.WeightedProposalMsg { return []simtypes.WeightedProposalMsg{ simulation.NewWeightedProposalMsg( @@ -32,6 +34,7 @@ func ProposalMsgs() []simtypes.WeightedProposalMsg { } // SimulateMsgUpdateParams returns a random MsgUpdateParams +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { // use the default gov module account address as authority var authority sdk.AccAddress = address.Module("gov") diff --git a/x/staking/module.go b/x/staking/module.go index 206fd867da..de72aa9125 100644 --- a/x/staking/module.go +++ b/x/staking/module.go @@ -21,6 +21,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/simsx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -280,19 +281,36 @@ func (AppModule) GenerateGenesisState(simState *module.SimulationState) { } // ProposalMsgs returns msgs used for governance proposals for simulations. +// migrate to ProposalMsgsX. This method is ignored when ProposalMsgsX exists and will be removed in the future. func (AppModule) ProposalMsgs(_ module.SimulationState) []simtypes.WeightedProposalMsg { return simulation.ProposalMsgs() } +// ProposalMsgsX registers governance proposal messages in the simulation registry. +func (AppModule) ProposalMsgsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("msg_update_params", 100), simulation.MsgUpdateParamsFactory()) +} + // RegisterStoreDecoder registers a decoder for staking module's types func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) } // WeightedOperations returns the all the staking module operations with their respective weights. +// migrate to WeightedOperationsX. This method is ignored when WeightedOperationsX exists and will be removed in the future func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { return simulation.WeightedOperations( simState.AppParams, simState.Cdc, simState.TxConfig, am.accountKeeper, am.bankKeeper, am.keeper, ) } + +// WeightedOperationsX registers weighted staking module operations for simulation. +func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Registry) { + reg.Add(weights.Get("msg_create_validator", 100), simulation.MsgCreateValidatorFactory(am.keeper)) + reg.Add(weights.Get("msg_delegate", 100), simulation.MsgDelegateFactory(am.keeper)) + reg.Add(weights.Get("msg_undelegate", 100), simulation.MsgUndelegateFactory(am.keeper)) + reg.Add(weights.Get("msg_edit_validator", 5), simulation.MsgEditValidatorFactory(am.keeper)) + reg.Add(weights.Get("msg_begin_redelegate", 100), simulation.MsgBeginRedelegateFactory(am.keeper)) + reg.Add(weights.Get("msg_cancel_unbonding_delegation", 100), simulation.MsgCancelUnbondingDelegationFactory(am.keeper)) +} diff --git a/x/staking/simulation/genesis.go b/x/staking/simulation/genesis.go index 08cc7dd05c..420342be7b 100644 --- a/x/staking/simulation/genesis.go +++ b/x/staking/simulation/genesis.go @@ -1,8 +1,6 @@ package simulation import ( - "encoding/json" - "fmt" "math/rand" "time" @@ -91,11 +89,5 @@ func RandomizedGenState(simState *module.SimulationState) { } stakingGenesis := types.NewGenesisState(params, validators, delegations) - - bz, err := json.MarshalIndent(&stakingGenesis.Params, "", " ") - if err != nil { - panic(err) - } - fmt.Printf("Selected randomly generated staking parameters:\n%s\n", bz) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(stakingGenesis) } diff --git a/x/staking/simulation/msg_factory.go b/x/staking/simulation/msg_factory.go new file mode 100644 index 0000000000..71c393ddef --- /dev/null +++ b/x/staking/simulation/msg_factory.go @@ -0,0 +1,334 @@ +package simulation + +import ( + "context" + "slices" + "time" + + "cosmossdk.io/math" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/simsx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func MsgCreateValidatorFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgCreateValidator] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgCreateValidator) { + r := testData.Rand() + withoutValidators := simsx.SimAccountFilterFn(func(a simsx.SimAccount) bool { + _, err := k.GetValidator(ctx, sdk.ValAddress(a.Address)) + return err != nil + }) + withoutConsAddrUsed := simsx.SimAccountFilterFn(func(a simsx.SimAccount) bool { + consPubKey := sdk.GetConsAddress(a.ConsKey.PubKey()) + _, err := k.GetValidatorByConsAddr(ctx, consPubKey) + return err != nil + }) + bondDenom := must(k.BondDenom(ctx)) + valOper := testData.AnyAccount(reporter, withoutValidators, withoutConsAddrUsed, simsx.WithDenomBalance(bondDenom)) + if reporter.IsSkipped() { + return nil, nil + } + + newPubKey := valOper.ConsKey.PubKey() + assertKeyUnused(ctx, reporter, k, newPubKey) + if reporter.IsSkipped() { + return nil, nil + } + + selfDelegation := valOper.LiquidBalance().RandSubsetCoin(reporter, bondDenom) + + description := types.NewDescription( + r.StringN(10), + r.StringN(10), + r.StringN(10), + r.StringN(10), + r.StringN(10), + ) + + maxCommission := math.LegacyNewDecWithPrec(int64(r.IntInRange(0, 100)), 2) + commission := types.NewCommissionRates( + r.DecN(maxCommission), + maxCommission, + r.DecN(maxCommission), + ) + + addr := must(k.ValidatorAddressCodec().BytesToString(valOper.Address)) + msg, err := types.NewMsgCreateValidator(addr, newPubKey, selfDelegation, description, commission, math.OneInt()) + if err != nil { + reporter.Skip(err.Error()) + return nil, nil + } + + return []simsx.SimAccount{valOper}, msg + } +} + +func MsgDelegateFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgDelegate] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgDelegate) { + r := testData.Rand() + bondDenom := must(k.BondDenom(ctx)) + val := randomValidator(ctx, reporter, k, r) + if reporter.IsSkipped() { + return nil, nil + } + + if val.InvalidExRate() { + reporter.Skip("validator's invalid exchange rate") + return nil, nil + } + sender := testData.AnyAccount(reporter) + delegation := sender.LiquidBalance().RandSubsetCoin(reporter, bondDenom) + return []simsx.SimAccount{sender}, types.NewMsgDelegate(sender.AddressBech32, val.GetOperator(), delegation) + } +} + +func MsgUndelegateFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgUndelegate] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgUndelegate) { + r := testData.Rand() + bondDenom := must(k.BondDenom(ctx)) + val := randomValidator(ctx, reporter, k, r) + if reporter.IsSkipped() { + return nil, nil + } + + // select delegator and amount for undelegate + valAddr := must(k.ValidatorAddressCodec().StringToBytes(val.GetOperator())) + delegations := must(k.GetValidatorDelegations(ctx, valAddr)) + if delegations == nil { + reporter.Skip("no delegation entries") + return nil, nil + } + // get random delegator from validator + delegation := delegations[r.Intn(len(delegations))] + delAddr := delegation.GetDelegatorAddr() + delegator := testData.GetAccount(reporter, delAddr) + + if hasMaxUD := must(k.HasMaxUnbondingDelegationEntries(ctx, delegator.Address, valAddr)); hasMaxUD { + reporter.Skipf("max unbodings") + return nil, nil + } + + totalBond := val.TokensFromShares(delegation.GetShares()).TruncateInt() + if !totalBond.IsPositive() { + reporter.Skip("total bond is negative") + return nil, nil + } + + unbondAmt := must(r.PositiveSDKIntn(totalBond)) + msg := types.NewMsgUndelegate(delAddr, val.GetOperator(), sdk.NewCoin(bondDenom, unbondAmt)) + return []simsx.SimAccount{delegator}, msg + } +} + +func MsgEditValidatorFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgEditValidator] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgEditValidator) { + r := testData.Rand() + val := randomValidator(ctx, reporter, k, r) + if reporter.IsSkipped() { + return nil, nil + } + + newCommissionRate := r.DecN(val.Commission.MaxRate) + if err := val.Commission.ValidateNewRate(newCommissionRate, simsx.BlockTime(ctx)); err != nil { + // skip as the commission is invalid + reporter.Skip("invalid commission rate") + return nil, nil + } + valOpAddrBz := must(k.ValidatorAddressCodec().StringToBytes(val.GetOperator())) + valOper := testData.GetAccountbyAccAddr(reporter, valOpAddrBz) + d := types.NewDescription(r.StringN(10), r.StringN(10), r.StringN(10), r.StringN(10), r.StringN(10)) + + msg := types.NewMsgEditValidator(val.GetOperator(), d, &newCommissionRate, nil) + return []simsx.SimAccount{valOper}, msg + } +} + +func MsgBeginRedelegateFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgBeginRedelegate] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgBeginRedelegate) { + bondDenom := must(k.BondDenom(ctx)) + if !testData.IsSendEnabledDenom(bondDenom) { + reporter.Skip("bond denom send not enabled") + return nil, nil + } + + r := testData.Rand() + // select random validator as src + vals := must(k.GetAllValidators(ctx)) + if len(vals) < 2 { + reporter.Skip("insufficient number of validators") + return nil, nil + } + srcVal := simsx.OneOf(r, vals) + srcValOpAddrBz := must(k.ValidatorAddressCodec().StringToBytes(srcVal.GetOperator())) + delegations := must(k.GetValidatorDelegations(ctx, srcValOpAddrBz)) + if delegations == nil { + reporter.Skip("no delegations") + return nil, nil + } + // get random delegator from src validator + delegation := simsx.OneOf(r, delegations) + totalBond := srcVal.TokensFromShares(delegation.GetShares()).TruncateInt() + if !totalBond.IsPositive() { + reporter.Skip("total bond is negative") + return nil, nil + } + redAmount, err := r.PositiveSDKIntn(totalBond) + if err != nil || redAmount.IsZero() { + reporter.Skip("unable to generate positive amount") + return nil, nil + } + if totalBond.Sub(redAmount).IsZero() { + reporter.Skip("can not redelegate all") + return nil, nil + } + + // check if the shares truncate to zero + shares := must(srcVal.SharesFromTokens(redAmount)) + if srcVal.TokensFromShares(shares).TruncateInt().IsZero() { + reporter.Skip("shares truncate to zero") + return nil, nil + } + + // pick a random delegator + delAddr := delegation.GetDelegatorAddr() + delAddrBz := must(testData.AddressCodec().StringToBytes(delAddr)) + if hasRecRedel := must(k.HasReceivingRedelegation(ctx, delAddrBz, srcValOpAddrBz)); hasRecRedel { + reporter.Skip("receiving redelegation is not allowed") + return nil, nil + } + delegator := testData.GetAccountbyAccAddr(reporter, delAddrBz) + if reporter.IsSkipped() { + return nil, nil + } + + // get random destination validator + destVal := simsx.OneOf(r, vals) + if srcVal.Equal(&destVal) { + destVal = simsx.OneOf(r, slices.DeleteFunc(vals, func(v types.Validator) bool { return srcVal.Equal(&v) })) + } + if destVal.InvalidExRate() { + reporter.Skip("invalid delegation rate") + return nil, nil + } + + destAddrBz := must(k.ValidatorAddressCodec().StringToBytes(destVal.GetOperator())) + if hasMaxRedel := must(k.HasMaxRedelegationEntries(ctx, delAddrBz, srcValOpAddrBz, destAddrBz)); hasMaxRedel { + reporter.Skip("maximum redelegation entries reached") + return nil, nil + } + + msg := types.NewMsgBeginRedelegate( + delAddr, srcVal.GetOperator(), destVal.GetOperator(), + sdk.NewCoin(bondDenom, redAmount), + ) + return []simsx.SimAccount{delegator}, msg + } +} + +func MsgCancelUnbondingDelegationFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgCancelUnbondingDelegation] { + return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgCancelUnbondingDelegation) { + r := testData.Rand() + val := randomValidator(ctx, reporter, k, r) + if reporter.IsSkipped() { + return nil, nil + } + if val.IsJailed() || val.InvalidExRate() { + reporter.Skip("validator is jailed") + return nil, nil + } + valOpAddrBz := must(k.ValidatorAddressCodec().StringToBytes(val.GetOperator())) + valOper := testData.GetAccountbyAccAddr(reporter, valOpAddrBz) + unbondingDelegation, err := k.GetUnbondingDelegation(ctx, valOper.Address, valOpAddrBz) + if err != nil { + reporter.Skip("no unbonding delegation") + return nil, nil + } + + // This is a temporary fix to make staking simulation pass. We should fetch + // the first unbondingDelegationEntry that matches the creationHeight, because + // currently the staking msgServer chooses the first unbondingDelegationEntry + // with the matching creationHeight. + // + // ref: https://github.com/cosmos/cosmos-sdk/issues/12932 + creationHeight := unbondingDelegation.Entries[r.Intn(len(unbondingDelegation.Entries))].CreationHeight + + var unbondingDelegationEntry types.UnbondingDelegationEntry + for _, entry := range unbondingDelegation.Entries { + if entry.CreationHeight == creationHeight { + unbondingDelegationEntry = entry + break + } + } + if unbondingDelegationEntry.CompletionTime.Before(simsx.BlockTime(ctx)) { + reporter.Skip("unbonding delegation is already processed") + return nil, nil + } + + if !unbondingDelegationEntry.Balance.IsPositive() { + reporter.Skip("delegator receiving balance is negative") + return nil, nil + } + cancelBondAmt := r.Amount(unbondingDelegationEntry.Balance) + if cancelBondAmt.IsZero() { + reporter.Skip("cancelBondAmt amount is zero") + return nil, nil + } + + msg := types.NewMsgCancelUnbondingDelegation( + valOper.AddressBech32, + val.GetOperator(), + unbondingDelegationEntry.CreationHeight, + sdk.NewCoin(must(k.BondDenom(ctx)), cancelBondAmt), + ) + + return []simsx.SimAccount{valOper}, msg + } +} + +// MsgUpdateParamsFactory creates a gov proposal for param updates +func MsgUpdateParamsFactory() simsx.SimMsgFactoryFn[*types.MsgUpdateParams] { + return func(_ context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgUpdateParams) { + r := testData.Rand() + params := types.DefaultParams() + // do not modify denom or staking will break + params.HistoricalEntries = r.Uint32InRange(0, 1000) + params.MaxEntries = r.Uint32InRange(1, 1000) + params.MaxValidators = r.Uint32InRange(1, 1000) + params.UnbondingTime = time.Duration(r.Timestamp().UnixNano()) + // modifying commission rate can cause issues for proposals within the same block + // params.MinCommissionRate = r.DecN(sdkmath.LegacyNewDec(1)) + + return nil, &types.MsgUpdateParams{ + Authority: testData.ModuleAccountAddress(reporter, "gov"), + Params: params, + } + } +} + +func randomValidator(ctx context.Context, reporter simsx.SimulationReporter, k *keeper.Keeper, r *simsx.XRand) types.Validator { + vals, err := k.GetAllValidators(ctx) + if err != nil || len(vals) == 0 { + reporter.Skipf("unable to get validators or empty list: %s", err) + return types.Validator{} + } + return simsx.OneOf(r, vals) +} + +// skips execution if there's another key rotation for the same key in the same block +func assertKeyUnused(ctx context.Context, reporter simsx.SimulationReporter, k *keeper.Keeper, newPubKey cryptotypes.PubKey) { + newConsAddr := sdk.ConsAddress(newPubKey.Address()) + if _, err := k.GetValidatorByConsAddr(ctx, newConsAddr); err == nil { + reporter.Skip("cons key already used") + return + } +} + +func must[T any](r T, err error) T { + if err != nil { + panic(err) + } + return r +} diff --git a/x/staking/simulation/operations.go b/x/staking/simulation/operations.go index 7da52f4d54..fefda50283 100644 --- a/x/staking/simulation/operations.go +++ b/x/staking/simulation/operations.go @@ -19,6 +19,7 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( DefaultWeightMsgCreateValidator int = 100 DefaultWeightMsgEditValidator int = 5 @@ -36,6 +37,7 @@ const ( ) // WeightedOperations returns all the operations from the module with their respective weights +// migrate to the msg factories instead, this method will be removed in the future func WeightedOperations( appParams simtypes.AppParams, cdc codec.JSONCodec, @@ -106,6 +108,7 @@ func WeightedOperations( } // SimulateMsgCreateValidator generates a MsgCreateValidator with random values +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgCreateValidator( txGen client.TxConfig, ak types.AccountKeeper, @@ -193,6 +196,7 @@ func SimulateMsgCreateValidator( } // SimulateMsgEditValidator generates a MsgEditValidator with random values +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgEditValidator( txGen client.TxConfig, ak types.AccountKeeper, @@ -268,6 +272,7 @@ func SimulateMsgEditValidator( } // SimulateMsgDelegate generates a MsgDelegate with random values +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgDelegate( txGen client.TxConfig, ak types.AccountKeeper, @@ -346,6 +351,7 @@ func SimulateMsgDelegate( } // SimulateMsgUndelegate generates a MsgUndelegate with random values +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUndelegate( txGen client.TxConfig, ak types.AccountKeeper, @@ -461,6 +467,7 @@ func SimulateMsgUndelegate( } // SimulateMsgCancelUnbondingDelegate generates a MsgCancelUnbondingDelegate with random values +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgCancelUnbondingDelegate( txGen client.TxConfig, ak types.AccountKeeper, @@ -561,6 +568,7 @@ func SimulateMsgCancelUnbondingDelegate( } // SimulateMsgBeginRedelegate generates a MsgBeginRedelegate with random values +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgBeginRedelegate( txGen client.TxConfig, ak types.AccountKeeper, diff --git a/x/staking/simulation/proposals.go b/x/staking/simulation/proposals.go index ca9bf09ee3..9eb91c22b7 100644 --- a/x/staking/simulation/proposals.go +++ b/x/staking/simulation/proposals.go @@ -14,6 +14,7 @@ import ( ) // Simulation operation weights constants +// will be removed in the future const ( DefaultWeightMsgUpdateParams int = 100 @@ -21,6 +22,7 @@ const ( ) // ProposalMsgs defines the module weighted proposals' contents +// migrate to the msg factories instead, this method will be removed in the future func ProposalMsgs() []simtypes.WeightedProposalMsg { return []simtypes.WeightedProposalMsg{ simulation.NewWeightedProposalMsg( @@ -32,6 +34,7 @@ func ProposalMsgs() []simtypes.WeightedProposalMsg { } // SimulateMsgUpdateParams returns a random MsgUpdateParams +// migrate to the msg factories instead, this method will be removed in the future func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { // use the default gov module account address as authority var authority sdk.AccAddress = address.Module("gov")