From 8bacbcaf00a986f0aecf550f930da4deac7676ce Mon Sep 17 00:00:00 2001 From: Riccardo <9119016+RiccardoM@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:44:50 +0900 Subject: [PATCH] feat(tests): add operators simulation tests (#190) ## Description This PR adds the simulation tests for the `x/operators` module. Closes: MILK-37 --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] added `!` to the type prefix if API or client breaking change - [ ] targeted the correct branch (see [PR Targeting](https://github.com/milkyway-labs/milkyway/blob/master/CONTRIBUTING.md#pr-targeting)) - [ ] provided a link to the relevant issue or specification - [ ] followed the guidelines for [building modules](https://docs.cosmos.network/v0.44/building-modules/intro.html) - [ ] included the necessary unit and integration [tests](https://github.com/milkyway-labs/milkyway/blob/master/CONTRIBUTING.md#testing) - [ ] added a changelog entry to `CHANGELOG.md` - [ ] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [ ] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable) --- app/modules.go | 3 +- testutils/simtesting/utils.go | 19 +- utils/slices.go | 10 + x/operators/client/cli/tx.go | 2 +- x/operators/keeper/genesis.go | 5 +- x/operators/keeper/keeper.go | 4 +- x/operators/keeper/msg_server_test.go | 78 ++++-- x/operators/module.go | 58 +++- x/operators/simulation/decoder.go | 37 +++ x/operators/simulation/genesis.go | 60 ++++ x/operators/simulation/msg_factory.go | 380 ++++++++++++++++++++++++++ x/operators/simulation/proposals.go | 50 ++++ x/operators/simulation/utils.go | 73 +++++ x/operators/types/genesis.go | 19 +- x/operators/types/genesis_test.go | 38 ++- x/operators/types/messages.go | 4 +- 16 files changed, 802 insertions(+), 38 deletions(-) create mode 100644 x/operators/simulation/decoder.go create mode 100644 x/operators/simulation/genesis.go create mode 100644 x/operators/simulation/msg_factory.go create mode 100644 x/operators/simulation/proposals.go create mode 100644 x/operators/simulation/utils.go diff --git a/app/modules.go b/app/modules.go index 8c6b824be..00f98845d 100644 --- a/app/modules.go +++ b/app/modules.go @@ -155,7 +155,7 @@ func appModules( // MilkyWay modules services.NewAppModule(appCodec, app.ServicesKeeper, app.AccountKeeper, app.BankKeeper), - operators.NewAppModule(appCodec, app.OperatorsKeeper), + operators.NewAppModule(appCodec, app.OperatorsKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper), pools.NewAppModule(appCodec, app.PoolsKeeper), restaking.NewAppModule(appCodec, app.RestakingKeeper), assets.NewAppModule(appCodec, app.AssetsKeeper), @@ -210,6 +210,7 @@ func simulationModules( // MilkyWay modules services.NewAppModule(appCodec, app.ServicesKeeper, app.AccountKeeper, app.BankKeeper), + operators.NewAppModule(appCodec, app.OperatorsKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper), } } diff --git a/testutils/simtesting/utils.go b/testutils/simtesting/utils.go index 2226c0fae..778ee8234 100644 --- a/testutils/simtesting/utils.go +++ b/testutils/simtesting/utils.go @@ -2,6 +2,7 @@ package simtesting import ( "math/rand" + "time" sdkmath "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/baseapp" @@ -18,7 +19,11 @@ import ( // SendMsg sends a transaction with the specified message. func SendMsg( - r *rand.Rand, moduleName string, app *baseapp.BaseApp, ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, + r *rand.Rand, + moduleName string, + app *baseapp.BaseApp, + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, msg sdk.Msg, ctx sdk.Context, simAccount simtypes.Account, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { @@ -63,6 +68,18 @@ func GetSimAccount(address sdk.Address, accs []simtypes.Account) (simtypes.Accou return simtypes.Account{}, false } +// -------------------------------------------------------------------------------------------------------------------- + +// RandomFutureTime returns a random future time +func RandomFutureTime(r *rand.Rand, currentTime time.Time) time.Time { + return currentTime.Add(time.Duration(r.Int63n(1e9))) +} + +// RandomDuration returns a random duration between the min and max +func RandomDuration(r *rand.Rand, min time.Duration, max time.Duration) time.Duration { + return time.Duration(r.Int63n(int64(max-min))) + min +} + // RandomCoin returns a random coin having the specified denomination and the max given amount func RandomCoin(r *rand.Rand, denom string, maxAmount int) sdk.Coin { return sdk.NewCoin( diff --git a/utils/slices.go b/utils/slices.go index cc661f136..f26a7d0df 100644 --- a/utils/slices.go +++ b/utils/slices.go @@ -4,6 +4,16 @@ import ( "slices" ) +// Find returns the first element in the slice that satisfies the given predicate, or false if none is found. +func Find[T any](slice []T, searchFunction func(T) bool) (*T, bool) { + for i, v := range slice { + if searchFunction(v) { + return &slice[i], true + } + } + return nil, false +} + // FindDuplicate returns the first duplicate element in the slice. // If no duplicates are found, it returns nil instead. func FindDuplicate[T comparable](slice []T) *T { diff --git a/x/operators/client/cli/tx.go b/x/operators/client/cli/tx.go index a48da0e05..dbe82857e 100644 --- a/x/operators/client/cli/tx.go +++ b/x/operators/client/cli/tx.go @@ -58,7 +58,7 @@ func GetCmdSetOperatorParams() *cobra.Command { creator := clientCtx.FromAddress.String() // Create and validate the message - msg := types.NewMsgSetOperatorParams(creator, id, types.NewOperatorParams(commissionRete)) + msg := types.NewMsgSetOperatorParams(id, types.NewOperatorParams(commissionRete), creator) if err = msg.ValidateBasic(); err != nil { return fmt.Errorf("message validation failed: %w", err) } diff --git a/x/operators/keeper/genesis.go b/x/operators/keeper/genesis.go index cd4544a45..511bd374a 100644 --- a/x/operators/keeper/genesis.go +++ b/x/operators/keeper/genesis.go @@ -82,7 +82,10 @@ func (k *Keeper) InitGenesis(ctx sdk.Context, state *types.GenesisState) error { // Store the inactivating operators for _, entry := range state.UnbondingOperators { - k.setOperatorAsInactivating(ctx, entry.OperatorID, entry.UnbondingCompletionTime) + err = k.setOperatorAsInactivating(ctx, entry.OperatorID, entry.UnbondingCompletionTime) + if err != nil { + return err + } } // Store params diff --git a/x/operators/keeper/keeper.go b/x/operators/keeper/keeper.go index fd9b41ece..6082edde5 100644 --- a/x/operators/keeper/keeper.go +++ b/x/operators/keeper/keeper.go @@ -19,7 +19,7 @@ type Keeper struct { accountKeeper types.AccountKeeper poolKeeper types.CommunityPoolKeeper - schema collections.Schema + Schema collections.Schema nextOperatorID collections.Sequence // Next operator ID operators collections.Map[uint32, types.Operator] // operator ID -> operator @@ -85,7 +85,7 @@ func NewKeeper( if err != nil { panic(err) } - k.schema = schema + k.Schema = schema return k } diff --git a/x/operators/keeper/msg_server_test.go b/x/operators/keeper/msg_server_test.go index 58eb38d0a..bd53e90be 100644 --- a/x/operators/keeper/msg_server_test.go +++ b/x/operators/keeper/msg_server_test.go @@ -882,7 +882,6 @@ func (suite *KeeperTestSuite) TestMsgServer_DeleteOperator() { } func (suite *KeeperTestSuite) TestMsgServer_SetOperatorParams() { - testOperatorId := uint32(2) operatorAdmin := "cosmos167x6ehhple8gwz5ezy9x0464jltvdpzl6qfdt4" testCases := []struct { @@ -895,37 +894,85 @@ func (suite *KeeperTestSuite) TestMsgServer_SetOperatorParams() { }{ { name: "set invalid params fails", + store: func(ctx sdk.Context) { + // Register a test operator + err := suite.k.CreateOperator(ctx, types.NewOperator( + 1, + types.OPERATOR_STATUS_ACTIVE, + "MilkyWay Operator", + "https://milkyway.com", + "https://milkyway.com/picture", + operatorAdmin, + )) + suite.Require().NoError(err) + }, msg: types.NewMsgSetOperatorParams( - operatorAdmin, - testOperatorId, + 1, types.NewOperatorParams(sdkmath.LegacyNewDec(-1)), + operatorAdmin, ), shouldErr: true, }, { name: "not admin can't set params", + store: func(ctx sdk.Context) { + // Register a test operator + err := suite.k.CreateOperator(ctx, types.NewOperator( + 1, + types.OPERATOR_STATUS_ACTIVE, + "MilkyWay Operator", + "https://milkyway.com", + "https://milkyway.com/picture", + operatorAdmin, + )) + suite.Require().NoError(err) + }, msg: types.NewMsgSetOperatorParams( - "cosmos1d03wa9qd8flfjtvldndw5csv94tvg5hzfcmcgn", - testOperatorId, + 1, types.NewOperatorParams(sdkmath.LegacyMustNewDecFromStr("0.2")), + "cosmos1d03wa9qd8flfjtvldndw5csv94tvg5hzfcmcgn", ), shouldErr: true, }, { name: "set params for not existing operator fails", + store: func(ctx sdk.Context) { + // Register a test operator + err := suite.k.CreateOperator(ctx, types.NewOperator( + 1, + types.OPERATOR_STATUS_ACTIVE, + "MilkyWay Operator", + "https://milkyway.com", + "https://milkyway.com/picture", + operatorAdmin, + )) + suite.Require().NoError(err) + }, msg: types.NewMsgSetOperatorParams( - operatorAdmin, 3, types.NewOperatorParams(sdkmath.LegacyMustNewDecFromStr("0.2")), + operatorAdmin, ), shouldErr: true, }, { name: "set params works properly", + store: func(ctx sdk.Context) { + // Register a test operator + err := suite.k.CreateOperator(ctx, types.NewOperator( + 1, + types.OPERATOR_STATUS_ACTIVE, + "MilkyWay Operator", + "https://milkyway.com", + "https://milkyway.com/picture", + operatorAdmin, + )) + suite.Require().NoError(err) + }, msg: types.NewMsgSetOperatorParams( - operatorAdmin, - testOperatorId, + 1, types.NewOperatorParams(sdkmath.LegacyMustNewDecFromStr("0.2")), + operatorAdmin, ), expEvents: []sdk.Event{ sdk.NewEvent( @@ -934,7 +981,7 @@ func (suite *KeeperTestSuite) TestMsgServer_SetOperatorParams() { }, shouldErr: false, check: func(ctx sdk.Context) { - params, err := suite.k.GetOperatorParams(ctx, testOperatorId) + params, err := suite.k.GetOperatorParams(ctx, 1) suite.Require().Nil(err) suite.Require().Equal(types.NewOperatorParams( sdkmath.LegacyMustNewDecFromStr("0.2"), @@ -947,23 +994,12 @@ func (suite *KeeperTestSuite) TestMsgServer_SetOperatorParams() { tc := tc suite.Run(tc.name, func() { suite.SetupTest() - ctx := suite.ctx + ctx, _ := suite.ctx.CacheContext() if tc.store != nil { tc.store(ctx) } - // Register a test operator - err := suite.k.CreateOperator(ctx, types.NewOperator( - testOperatorId, - types.OPERATOR_STATUS_ACTIVE, - "MilkyWay Operator", - "https://milkyway.com", - "https://milkyway.com/picture", - operatorAdmin, - )) - suite.Require().NoError(err) - msgServer := keeper.NewMsgServer(suite.k) res, err := msgServer.SetOperatorParams(ctx, tc.msg) if tc.shouldErr { diff --git a/x/operators/module.go b/x/operators/module.go index ee83747ab..890edbea1 100644 --- a/x/operators/module.go +++ b/x/operators/module.go @@ -6,6 +6,10 @@ import ( "fmt" "cosmossdk.io/core/appmodule" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" @@ -19,6 +23,7 @@ import ( "github.com/milkyway-labs/milkyway/x/operators/client/cli" "github.com/milkyway-labs/milkyway/x/operators/keeper" + "github.com/milkyway-labs/milkyway/x/operators/simulation" "github.com/milkyway-labs/milkyway/x/operators/types" ) @@ -27,8 +32,9 @@ const ( ) var ( - _ appmodule.AppModule = AppModule{} - _ module.AppModuleBasic = AppModuleBasic{} + _ appmodule.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + _ module.AppModuleSimulation = AppModule{} ) // ---------------------------------------------------------------------------- @@ -93,14 +99,27 @@ func (a AppModuleBasic) GetTxCmd() *cobra.Command { type AppModule struct { AppModuleBasic - // To ensure setting hooks properly, keeper must be a reference keeper *keeper.Keeper -} -func NewAppModule(cdc codec.Codec, keeper *keeper.Keeper) AppModule { + // To ensure setting hooks properly, keeper must be a reference + accountKeeper authkeeper.AccountKeeper + bankKeeper bankkeeper.Keeper + stakingKeeper *stakingkeeper.Keeper +} + +func NewAppModule( + cdc codec.Codec, + keeper *keeper.Keeper, + accountKeeper authkeeper.AccountKeeper, + bankKeeper bankkeeper.Keeper, + stakingKeeper *stakingkeeper.Keeper, +) AppModule { return AppModule{ AppModuleBasic: NewAppModuleBasic(cdc), keeper: keeper, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + stakingKeeper: stakingKeeper, } } @@ -150,3 +169,32 @@ func (am AppModule) BeginBlock(ctx context.Context) error { func (am AppModule) IsOnePerModuleType() {} func (am AppModule) IsAppModule() {} + +// ---------------------------------------------------------------------------- +// AppModuleSimulation +// ---------------------------------------------------------------------------- + +// GenerateGenesisState creates a randomized GenState of the services module. +func (AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// ProposalMsgs returns msgs used for governance proposals for simulations. +func (am AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { + return simulation.ProposalMsgs(am.stakingKeeper) +} + +// RegisterStoreDecoder registers a decoder for services module's types. +func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.NewDecodeStore(am.keeper) +} + +// WeightedOperations returns the all the services module operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations( + simState.AppParams, + am.accountKeeper, + am.bankKeeper, + am.keeper, + ) +} diff --git a/x/operators/simulation/decoder.go b/x/operators/simulation/decoder.go new file mode 100644 index 000000000..cd20e056a --- /dev/null +++ b/x/operators/simulation/decoder.go @@ -0,0 +1,37 @@ +package simulation + +import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/types/kv" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/milkyway-labs/milkyway/x/operators/keeper" + "github.com/milkyway-labs/milkyway/x/operators/types" +) + +// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's +// Value to the corresponding operators type. +func NewDecodeStore(keeper *keeper.Keeper) func(kvA kv.Pair, kvB kv.Pair) string { + collectionsDecoder := simtypes.NewStoreDecoderFuncFromCollectionsSchema(keeper.Schema) + + return func(kvA, kvB kv.Pair) string { + switch { + case bytes.Equal(kvA.Key[:1], types.ParamsKey), + bytes.Equal(kvA.Key[:1], types.NextOperatorIDKey), + bytes.Equal(kvA.Key[:1], types.OperatorPrefix), + bytes.Equal(kvA.Key[:1], types.OperatorAddressSetPrefix), + bytes.Equal(kvA.Key[:1], types.OperatorParamsMapPrefix): + return collectionsDecoder(kvA, kvB) + + case bytes.Equal(kvA.Key[:1], types.InactivatingOperatorQueuePrefix): + valueA := types.GetOperatorIDFromBytes(kvA.Value) + valueB := types.GetOperatorIDFromBytes(kvB.Value) + return fmt.Sprintf("operatorIDA: %d\noperatorIDB: %d", valueA, valueB) + + default: + panic(fmt.Sprintf("invalid operators key prefix %X", kvA.Key[:1])) + } + } +} diff --git a/x/operators/simulation/genesis.go b/x/operators/simulation/genesis.go new file mode 100644 index 000000000..b231607cd --- /dev/null +++ b/x/operators/simulation/genesis.go @@ -0,0 +1,60 @@ +package simulation + +import ( + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/milkyway-labs/milkyway/testutils/simtesting" + "github.com/milkyway-labs/milkyway/utils" + "github.com/milkyway-labs/milkyway/x/operators/types" +) + +// RandomizedGenState generates a random GenesisState for the operators module +func RandomizedGenState(simState *module.SimulationState) { + // Generate a random list of operators + var operators []types.Operator + for i := 0; i < simState.Rand.Intn(100); i++ { + operators = append(operators, RandomOperator(simState.Rand, simState.Accounts)) + } + + // Get the next operator ID + var nextOperatorID uint32 = 1 + for _, operator := range operators { + if operator.ID >= nextOperatorID { + nextOperatorID = operator.ID + 1 + } + } + + // Generate the operator params + var operatorParams []types.OperatorParamsRecord + for _, operator := range operators { + // 50% chance of having default params + if simState.Rand.Intn(2) == 0 { + continue + } + + operatorParams = append(operatorParams, types.NewOperatorParamsRecord( + operator.ID, + RandomOperatorParams(simState.Rand), + )) + } + + // Set the inactivating operators to be unbonded + inactivatingOperators := utils.Filter(operators, func(operator types.Operator) bool { + return operator.Status == types.OPERATOR_STATUS_INACTIVATING + }) + + var unbondingOperators []types.UnbondingOperator + for _, operator := range inactivatingOperators { + unbondingOperators = append(unbondingOperators, types.NewUnbondingOperator( + operator.ID, + simtesting.RandomFutureTime(simState.Rand, simState.GenTimestamp), + )) + } + + // Generate the params + params := RandomParams(simState.Rand, simState.BondDenom) + + // Set the genesis state inside the simulation + genesis := types.NewGenesisState(nextOperatorID, operators, operatorParams, unbondingOperators, params) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) +} diff --git a/x/operators/simulation/msg_factory.go b/x/operators/simulation/msg_factory.go new file mode 100644 index 000000000..d0654341a --- /dev/null +++ b/x/operators/simulation/msg_factory.go @@ -0,0 +1,380 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/milkyway-labs/milkyway/testutils/simtesting" + "github.com/milkyway-labs/milkyway/x/operators/keeper" + "github.com/milkyway-labs/milkyway/x/operators/types" +) + +// Simulation operation weights constants +const ( + DefaultWeightMsgRegisterOperator int = 80 + DefaultWeightMsgUpdateOperator int = 40 + DefaultWeightMsgDeactivateOperator int = 20 + DefaultWeightMsgReactivateOperator int = 30 + DefaultWeightMsgTransferOperatorOwnership int = 10 + DefaultWeightMsgDeleteOperator int = 25 + DefaultWeightMsgSetOperatorParams int = 10 + + OperationWeightMsgRegisterOperator = "op_weight_msg_register_operator" + OperationWeightMsgDeactivateOperator = "op_weight_msg_deactivate_operator" + OperationWeightMsgUpdateOperator = "op_weight_msg_update_operator" + OperationWeightMsgReactivateOperator = "op_weight_msg_reactivate_operator" + OperationWeightMsgTransferOperatorOwnership = "op_weight_msg_transfer_operator_ownership" + OperationWeightMsgDeleteOperator = "op_weight_msg_delete_operator" + OperationWeightMsgSetOperatorParams = "op_weight_msg_set_operator_params" +) + +func WeightedOperations( + appParams simtypes.AppParams, + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + k *keeper.Keeper, +) simulation.WeightedOperations { + var ( + weightMsgRegisterOperator int + weightMsgUpdateOperator int + weightMsgDeactivateOperator int + weightMsgReactivateOperator int + weightMsgTransferOperatorOwnership int + weightMsgDeleteOperator int + weightMsgSetOperatorParams int + ) + + // Generate the weights for the messages + appParams.GetOrGenerate(OperationWeightMsgRegisterOperator, &weightMsgRegisterOperator, nil, func(_ *rand.Rand) { + weightMsgRegisterOperator = DefaultWeightMsgRegisterOperator + }) + + appParams.GetOrGenerate(OperationWeightMsgUpdateOperator, &weightMsgUpdateOperator, nil, func(_ *rand.Rand) { + weightMsgUpdateOperator = DefaultWeightMsgUpdateOperator + }) + + appParams.GetOrGenerate(OperationWeightMsgDeactivateOperator, &weightMsgDeactivateOperator, nil, func(_ *rand.Rand) { + weightMsgDeactivateOperator = DefaultWeightMsgDeactivateOperator + }) + + appParams.GetOrGenerate(OperationWeightMsgReactivateOperator, &weightMsgReactivateOperator, nil, func(_ *rand.Rand) { + weightMsgReactivateOperator = DefaultWeightMsgReactivateOperator + }) + + appParams.GetOrGenerate(OperationWeightMsgTransferOperatorOwnership, &weightMsgTransferOperatorOwnership, nil, func(_ *rand.Rand) { + weightMsgTransferOperatorOwnership = DefaultWeightMsgTransferOperatorOwnership + }) + + appParams.GetOrGenerate(OperationWeightMsgDeleteOperator, &weightMsgDeleteOperator, nil, func(_ *rand.Rand) { + weightMsgDeleteOperator = DefaultWeightMsgDeleteOperator + }) + + appParams.GetOrGenerate(OperationWeightMsgSetOperatorParams, &weightMsgSetOperatorParams, nil, func(_ *rand.Rand) { + weightMsgSetOperatorParams = DefaultWeightMsgSetOperatorParams + }) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation(weightMsgRegisterOperator, SimulateMsgRegisterOperator(ak, bk, k)), + simulation.NewWeightedOperation(weightMsgUpdateOperator, SimulateUpdateOperator(ak, bk, k)), + simulation.NewWeightedOperation(weightMsgDeactivateOperator, SimulateDeactivateOperator(ak, bk, k)), + simulation.NewWeightedOperation(weightMsgReactivateOperator, SimulateReactivateOperator(ak, bk, k)), + simulation.NewWeightedOperation(weightMsgTransferOperatorOwnership, SimulateTransferOperatorOwnership(ak, bk, k)), + simulation.NewWeightedOperation(weightMsgDeleteOperator, SimulateDeleteOperator(ak, bk, k)), + simulation.NewWeightedOperation(weightMsgSetOperatorParams, SimulateSetOperatorParams(ak, bk, k)), + } +} + +// -------------------------------------------------------------------------------------------------------------------- + +func SimulateMsgRegisterOperator(ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var msg = &types.MsgRegisterOperator{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random operator + operator := RandomOperator(r, accs) + + // Get the admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(operator.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "invalid admin address"), nil, nil + } + + // Make sure the admin has enough funds to pay for the creation fees + params, err := k.GetParams(ctx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get params"), nil, nil + } + + if params.OperatorRegistrationFee.IsAnyGTE(bk.GetAllBalances(ctx, adminAddress)) { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "insufficient funds"), nil, nil + } + + signer, found := simtesting.GetSimAccount(adminAddress, accs) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "admin account not found"), nil, nil + } + + // Create the message + msg = types.NewMsgRegisterOperator( + operator.Moniker, + operator.Website, + operator.PictureURL, + operator.Admin, + ) + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateUpdateOperator(ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var msg = &types.MsgUpdateOperator{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random operator from the one existing + operator, found := GetRandomExistingOperator(r, ctx, k, nil) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get operator"), nil, nil + } + + // Generate a new data + updatedMoniker := types.DoNotModify + if r.Intn(100) < 50 { + // 50% chance of changing the moniker + updatedMoniker = simtypes.RandStringOfLength(r, 20) + } + + updatedWebsite := types.DoNotModify + if r.Intn(100) < 50 { + // 50% chance of changing the website + updatedWebsite = simtypes.RandStringOfLength(r, 20) + } + + updatedPictureURL := types.DoNotModify + if r.Intn(100) < 50 { + // 50% chance of changing the picture URL + updatedPictureURL = simtypes.RandStringOfLength(r, 20) + } + + // Get the admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(operator.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "invalid admin address"), nil, nil + } + + signer, found := simtesting.GetSimAccount(adminAddress, accs) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "admin account not found"), nil, nil + } + + // Create the message + msg = types.NewMsgUpdateOperator( + operator.ID, + updatedMoniker, + updatedWebsite, + updatedPictureURL, + operator.Admin, + ) + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateDeactivateOperator(ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var msg = &types.MsgDeactivateOperator{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random operator from the one existing + operator, found := GetRandomExistingOperator(r, ctx, k, func(operator types.Operator) bool { + return operator.Status == types.OPERATOR_STATUS_ACTIVE + }) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get operator"), nil, nil + } + + // Get the admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(operator.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "invalid admin address"), nil, nil + } + + signer, found := simtesting.GetSimAccount(adminAddress, accs) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "admin account not found"), nil, nil + } + + // Create the message + msg = types.NewMsgDeactivateOperator(operator.ID, operator.Admin) + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateReactivateOperator(ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var msg = &types.MsgReactivateOperator{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random operator from the one existing + operator, found := GetRandomExistingOperator(r, ctx, k, func(operator types.Operator) bool { + return operator.Status == types.OPERATOR_STATUS_INACTIVE + }) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get inactive operator"), nil, nil + } + + // Get the admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(operator.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "invalid admin address"), nil, nil + } + + signer, found := simtesting.GetSimAccount(adminAddress, accs) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "admin account not found"), nil, nil + } + + // Create the message + msg = types.NewMsgReactivateOperator(operator.ID, operator.Admin) + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateTransferOperatorOwnership(ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var msg = &types.MsgTransferOperatorOwnership{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random operator from the one existing + operator, found := GetRandomExistingOperator(r, ctx, k, nil) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get operator"), nil, nil + } + + // Get a random new admin + newAdminAccount, _ := simtypes.RandomAcc(r, accs) + + // Get the current admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(operator.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "invalid admin address"), nil, nil + } + + signer, found := simtesting.GetSimAccount(adminAddress, accs) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "admin account not found"), nil, nil + } + + // Create the message + msg = types.NewMsgTransferOperatorOwnership(operator.ID, newAdminAccount.Address.String(), operator.Admin) + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateDeleteOperator(ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var msg = &types.MsgDeleteOperator{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random operator from the one existing + operator, found := GetRandomExistingOperator(r, ctx, k, func(operator types.Operator) bool { + return operator.Status == types.OPERATOR_STATUS_INACTIVE + }) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get operator"), nil, nil + } + + // Get the admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(operator.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "invalid admin address"), nil, nil + } + + signer, found := simtesting.GetSimAccount(adminAddress, accs) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "admin account not found"), nil, nil + } + + // Create the message + msg = types.NewMsgDeleteOperator(operator.ID, operator.Admin) + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateSetOperatorParams(ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var msg = &types.MsgSetOperatorParams{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random operator from the one existing + operator, found := GetRandomExistingOperator(r, ctx, k, nil) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get operator"), nil, nil + } + + // Generate a new data + newParams := RandomOperatorParams(r) + + // Get the admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(operator.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "invalid admin address"), nil, nil + } + + signer, found := simtesting.GetSimAccount(adminAddress, accs) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "admin account not found"), nil, nil + } + + // Create the message + msg = types.NewMsgSetOperatorParams(operator.ID, newParams, operator.Admin) + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} diff --git a/x/operators/simulation/proposals.go b/x/operators/simulation/proposals.go new file mode 100644 index 000000000..f9dfec529 --- /dev/null +++ b/x/operators/simulation/proposals.go @@ -0,0 +1,50 @@ +package simulation + +import ( + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + + "github.com/milkyway-labs/milkyway/x/operators/types" +) + +const ( + DefaultWeightMsgUpdateParams = 30 + + OperationWeightMsgUpdateParams = "op_weight_msg_update_params" +) + +func ProposalMsgs(stakingKeeper *stakingkeeper.Keeper) []simtypes.WeightedProposalMsg { + return []simtypes.WeightedProposalMsg{ + simulation.NewWeightedProposalMsg( + OperationWeightMsgUpdateParams, + DefaultWeightMsgUpdateParams, + SimulateMsgUpdateParams(stakingKeeper), + ), + } +} + +// SimulateMsgUpdateParams returns a random MsgUpdateParams +func SimulateMsgUpdateParams(stakingKeeper *stakingkeeper.Keeper) func(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { + return func(r *rand.Rand, ctx sdk.Context, _ []simtypes.Account) sdk.Msg { + // Use the default gov module account address as authority + var authority sdk.AccAddress = address.Module(govtypes.ModuleName) + + // Get the stake denom + bondDenom, err := stakingKeeper.BondDenom(ctx) + if err != nil { + panic("failed to get bond denom") + } + + // Generate the new random params + params := RandomParams(r, bondDenom) + + // Return the message + return types.NewMsgUpdateParams(params, authority.String()) + } +} diff --git a/x/operators/simulation/utils.go b/x/operators/simulation/utils.go new file mode 100644 index 000000000..4449a6c2c --- /dev/null +++ b/x/operators/simulation/utils.go @@ -0,0 +1,73 @@ +package simulation + +import ( + "context" + "math/rand" + "time" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/milkyway-labs/milkyway/testutils/simtesting" + "github.com/milkyway-labs/milkyway/utils" + "github.com/milkyway-labs/milkyway/x/operators/keeper" + "github.com/milkyway-labs/milkyway/x/operators/types" +) + +// RandomOperator returns a random operator +func RandomOperator(r *rand.Rand, accounts []simtypes.Account) types.Operator { + adminAccount, _ := simtypes.RandomAcc(r, accounts) + + return types.NewOperator( + r.Uint32(), + randomOperatorStatus(r), + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 20), + simtypes.RandStringOfLength(r, 20), + adminAccount.Address.String(), + ) +} + +// randomOperatorStatus returns a random operator status +func randomOperatorStatus(r *rand.Rand) types.OperatorStatus { + statusesSize := len(types.OperatorStatus_name) + return types.OperatorStatus(r.Intn(statusesSize-1) + 1) +} + +// RandomOperatorParams returns random operator params +func RandomOperatorParams(r *rand.Rand) types.OperatorParams { + return types.NewOperatorParams( + sdkmath.LegacyNewDecWithPrec(int64(r.Intn(100)), 2), + ) +} + +// RandomParams returns random params +func RandomParams(r *rand.Rand, stakeDenom string) types.Params { + return types.NewParams( + sdk.NewCoins(simtesting.RandomCoin(r, stakeDenom, 10)), + simtesting.RandomDuration(r, 5*time.Minute, 3*24*time.Hour), + ) +} + +// GetRandomExistingOperator returns a random existing operator +func GetRandomExistingOperator(r *rand.Rand, ctx context.Context, k *keeper.Keeper, filter func(operator types.Operator) bool) (types.Operator, bool) { + operators, err := k.GetOperators(ctx) + if err != nil { + panic(err) + } + + if len(operators) == 0 { + return types.Operator{}, false + } + + if filter != nil { + operators = utils.Filter(operators, filter) + if len(operators) == 0 { + return types.Operator{}, false + } + } + + randomOperatorIndex := r.Intn(len(operators)) + return operators[randomOperatorIndex], true +} diff --git a/x/operators/types/genesis.go b/x/operators/types/genesis.go index 25add88e3..e93d45d90 100644 --- a/x/operators/types/genesis.go +++ b/x/operators/types/genesis.go @@ -66,10 +66,23 @@ func (data *GenesisState) Validate() error { } // Validate unbonding operators - for _, operator := range data.UnbondingOperators { - err := operator.Validate() + for _, unbondingOperator := range data.UnbondingOperators { + err := unbondingOperator.Validate() if err != nil { - return fmt.Errorf("invalid unbonding operator with id %d: %s", operator.OperatorID, err) + return fmt.Errorf("invalid unbonding operator with id %d: %s", unbondingOperator.OperatorID, err) + } + + // Make sure the operator status is inactivating + operator, found := utils.Find(data.Operators, func(operator Operator) bool { + return operator.ID == unbondingOperator.OperatorID + }) + + if !found { + return fmt.Errorf("unbonding operator with id %d not found", unbondingOperator.OperatorID) + } + + if operator.Status != OPERATOR_STATUS_INACTIVATING { + return fmt.Errorf("operator with id %d is not inactivating", unbondingOperator.OperatorID) } } diff --git a/x/operators/types/genesis_test.go b/x/operators/types/genesis_test.go index 010e0125d..a0bc40fbc 100644 --- a/x/operators/types/genesis_test.go +++ b/x/operators/types/genesis_test.go @@ -100,6 +100,42 @@ func TestGenesisState_Validate(t *testing.T) { }, shouldErr: true, }, + { + name: "not found unbonding operator returns error", + genesis: &types.GenesisState{ + NextOperatorID: 1, + UnbondingOperators: []types.UnbondingOperator{ + types.NewUnbondingOperator( + 1, + time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), + ), + }, + }, + shouldErr: true, + }, + { + name: "unbonding operator with status non inactivating returns error", + genesis: &types.GenesisState{ + NextOperatorID: 1, + Operators: []types.Operator{ + types.NewOperator( + 1, + types.OPERATOR_STATUS_ACTIVE, + "MilkyWay Operator", + "https://milkyway.com", + "https://milkyway.com/picture", + "cosmos167x6ehhple8gwz5ezy9x0464jltvdpzl6qfdt4", + ), + }, + UnbondingOperators: []types.UnbondingOperator{ + types.NewUnbondingOperator( + 1, + time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), + ), + }, + }, + shouldErr: true, + }, { name: "invalid params returns error", genesis: &types.GenesisState{ @@ -122,7 +158,7 @@ func TestGenesisState_Validate(t *testing.T) { Operators: []types.Operator{ types.NewOperator( 1, - types.OPERATOR_STATUS_ACTIVE, + types.OPERATOR_STATUS_INACTIVATING, "MilkyWay Operator", "https://milkyway.com", "https://milkyway.com/picture", diff --git a/x/operators/types/messages.go b/x/operators/types/messages.go index 22ad2487f..02bf9a1fa 100644 --- a/x/operators/types/messages.go +++ b/x/operators/types/messages.go @@ -251,11 +251,11 @@ func (msg *MsgUpdateParams) GetSigners() []sdk.AccAddress { // -------------------------------------------------------------------------------------------------------------------- -func NewMsgSetOperatorParams(sender string, operatorID uint32, operatorParams OperatorParams) *MsgSetOperatorParams { +func NewMsgSetOperatorParams(operatorID uint32, operatorParams OperatorParams, sender string) *MsgSetOperatorParams { return &MsgSetOperatorParams{ - Sender: sender, OperatorID: operatorID, Params: operatorParams, + Sender: sender, } }