diff --git a/app/modules.go b/app/modules.go index d2b0d27a..50282356 100644 --- a/app/modules.go +++ b/app/modules.go @@ -152,7 +152,7 @@ func appModules( services.NewAppModule(appCodec, app.ServicesKeeper, app.AccountKeeper, app.BankKeeper), operators.NewAppModule(appCodec, app.OperatorsKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper), pools.NewAppModule(appCodec, app.PoolsKeeper), - restaking.NewAppModule(appCodec, app.RestakingKeeper), + restaking.NewAppModule(appCodec, app.RestakingKeeper, app.AccountKeeper, app.BankKeeper, app.PoolsKeeper, app.OperatorsKeeper, app.ServicesKeeper), assets.NewAppModule(appCodec, app.AssetsKeeper), rewards.NewAppModule(appCodec, app.RewardsKeeper), liquidvesting.NewAppModule(appCodec, app.LiquidVestingKeeper), @@ -205,6 +205,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), + restaking.NewAppModule(appCodec, app.RestakingKeeper, app.AccountKeeper, app.BankKeeper, app.PoolsKeeper, app.OperatorsKeeper, app.ServicesKeeper), } } diff --git a/testutils/simtesting/utils.go b/testutils/simtesting/utils.go index 778ee823..f31c7bd9 100644 --- a/testutils/simtesting/utils.go +++ b/testutils/simtesting/utils.go @@ -87,3 +87,33 @@ func RandomCoin(r *rand.Rand, denom string, maxAmount int) sdk.Coin { sdkmath.NewInt(int64(r.Intn(maxAmount*1e6))), ) } + +// RandomSubSlice returns a random subset of the given slice +func RandomSubSlice[T any](r *rand.Rand, items []T) []T { + // Empty slice, we can't pick random elements + if len(items) == 0 { + return nil + } + + // We store here the selected index, this allows T to not be comparable. + pickedIndexes := make(map[int]bool) + + var elements []T + // Randomly select how many items to pick + count := r.Intn(len(items) + 1) + for len(pickedIndexes) < count { + // Get a random index + index := r.Intn(len(items)) + _, found := pickedIndexes[index] + + // Check if we have already picked this element + if !found { + // Element not picked, add it + elements = append(elements, items[index]) + // Signal that we have picked the element at index + pickedIndexes[index] = true + } + } + + return elements +} diff --git a/x/operators/simulation/genesis.go b/x/operators/simulation/genesis.go index a8ea6d46..da2e178d 100644 --- a/x/operators/simulation/genesis.go +++ b/x/operators/simulation/genesis.go @@ -58,3 +58,16 @@ func RandomizedGenState(simState *module.SimulationState) { genesis := types.NewGenesisState(nextOperatorID, operators, operatorParams, unbondingOperators, params) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) } + +// GetGenesisState returns the operators genesis state from the SimulationState +func GetGenesisState(simState *module.SimulationState) types.GenesisState { + operatorsGenesisJSON, found := simState.GenState[types.ModuleName] + var operatorsGenesis types.GenesisState + if found { + simState.Cdc.MustUnmarshalJSON(operatorsGenesisJSON, &operatorsGenesis) + } else { + operatorsGenesis = *types.DefaultGenesis() + } + + return operatorsGenesis +} diff --git a/x/pools/simulation/genesis.go b/x/pools/simulation/genesis.go new file mode 100644 index 00000000..db4f7216 --- /dev/null +++ b/x/pools/simulation/genesis.go @@ -0,0 +1,20 @@ +package simulation + +import ( + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/milkyway-labs/milkyway/v3/x/pools/types" +) + +// GetGenesisState returns the pools genesis state from the SimulationState +func GetGenesisState(simState *module.SimulationState) types.GenesisState { + operatorsGenesisJSON, found := simState.GenState[types.ModuleName] + var operatorsGenesis types.GenesisState + if found { + simState.Cdc.MustUnmarshalJSON(operatorsGenesisJSON, &operatorsGenesis) + } else { + operatorsGenesis = *types.DefaultGenesis() + } + + return operatorsGenesis +} diff --git a/x/pools/simulation/utils.go b/x/pools/simulation/utils.go new file mode 100644 index 00000000..da92acb5 --- /dev/null +++ b/x/pools/simulation/utils.go @@ -0,0 +1,30 @@ +package simulation + +import ( + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/milkyway-labs/milkyway/v3/utils" + "github.com/milkyway-labs/milkyway/v3/x/pools/keeper" + "github.com/milkyway-labs/milkyway/v3/x/pools/types" +) + +// GetRandomExistingPool returns a random existing pool +func GetRandomExistingPool(r *rand.Rand, ctx sdk.Context, k *keeper.Keeper, filter func(s types.Pool) bool) (types.Pool, bool) { + pools, err := k.GetPools(ctx) + if err != nil { + panic(err) + } + + if filter != nil { + pools = utils.Filter(pools, filter) + } + + if len(pools) == 0 { + return types.Pool{}, false + } + + randomServiceIndex := r.Intn(len(pools)) + return pools[randomServiceIndex], true +} diff --git a/x/restaking/keeper/invariants.go b/x/restaking/keeper/invariants.go index 8637bbc2..3f53d753 100644 --- a/x/restaking/keeper/invariants.go +++ b/x/restaking/keeper/invariants.go @@ -373,7 +373,7 @@ func AllowedOperatorsExistInvariant(k *Keeper) sdk.Invariant { // Iterate over all the services joined by operators var notFoundOperatorsIDs []uint32 err := k.IterateAllServicesAllowedOperators(ctx, func(serviceID uint32, operatorID uint32) (stop bool, err error) { - _, err = k.operatorsKeeper.GetOperator(ctx, serviceID) + _, err = k.operatorsKeeper.GetOperator(ctx, operatorID) if err != nil { if errors.Is(err, collections.ErrNotFound) { notFoundOperatorsIDs = append(notFoundOperatorsIDs, operatorID) diff --git a/x/restaking/keeper/keeper.go b/x/restaking/keeper/keeper.go index a79dbe5e..8d3f17e3 100644 --- a/x/restaking/keeper/keeper.go +++ b/x/restaking/keeper/keeper.go @@ -26,7 +26,7 @@ type Keeper struct { servicesKeeper types.ServicesKeeper // Keeper data - schema collections.Schema + Schema collections.Schema // Here we use a IndexMap with NoValue instead of a KeySet because the cosmos-sdk don't // provide a KeySet with indexes that we need in order to get the list of operators @@ -103,7 +103,7 @@ func NewKeeper( if err != nil { panic(err) } - k.schema = schema + k.Schema = schema return k } diff --git a/x/restaking/module.go b/x/restaking/module.go index 2b83c6f1..92769856 100644 --- a/x/restaking/module.go +++ b/x/restaking/module.go @@ -6,20 +6,25 @@ import ( "fmt" "cosmossdk.io/core/appmodule" - "github.com/grpc-ecosystem/grpc-gateway/runtime" - "github.com/spf13/cobra" - abci "github.com/cometbft/cometbft/abci/types" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + 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/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + operatorskeeper "github.com/milkyway-labs/milkyway/v3/x/operators/keeper" + poolskeeper "github.com/milkyway-labs/milkyway/v3/x/pools/keeper" "github.com/milkyway-labs/milkyway/v3/x/restaking/client/cli" "github.com/milkyway-labs/milkyway/v3/x/restaking/keeper" + "github.com/milkyway-labs/milkyway/v3/x/restaking/simulation" "github.com/milkyway-labs/milkyway/v3/x/restaking/types" + serviceskeeper "github.com/milkyway-labs/milkyway/v3/x/services/keeper" ) const ( @@ -100,12 +105,31 @@ type AppModule struct { // To ensure setting hooks properly, keeper must be a reference keeper *keeper.Keeper -} -func NewAppModule(cdc codec.Codec, keeper *keeper.Keeper) AppModule { + ak authkeeper.AccountKeeper + bk bankkeeper.Keeper + poolsKeeper *poolskeeper.Keeper + operatorsKeeper *operatorskeeper.Keeper + servicesKeeper *serviceskeeper.Keeper +} + +func NewAppModule( + cdc codec.Codec, + keeper *keeper.Keeper, + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + poolsKeeper *poolskeeper.Keeper, + operatorsKeeper *operatorskeeper.Keeper, + servicesKeeper *serviceskeeper.Keeper, +) AppModule { return AppModule{ - AppModuleBasic: NewAppModuleBasic(cdc), - keeper: keeper, + AppModuleBasic: NewAppModuleBasic(cdc), + keeper: keeper, + ak: ak, + bk: bk, + poolsKeeper: poolsKeeper, + operatorsKeeper: operatorsKeeper, + servicesKeeper: servicesKeeper, } } @@ -156,3 +180,33 @@ func (am AppModule) EndBlock(ctx context.Context) error { func (am AppModule) IsOnePerModuleType() {} func (am AppModule) IsAppModule() {} + +// AppModuleSimulation functions + +// GenerateGenesisState creates a randomized GenState of the restaking 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.keeper) +} + +// RegisterStoreDecoder registers a decoder for restaking module's types +func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc, am.keeper) +} + +// WeightedOperations returns all the restaking module operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations( + simState.AppParams, + am.ak, + am.bk, + am.poolsKeeper, + am.operatorsKeeper, + am.servicesKeeper, + am.keeper, + ) +} diff --git a/x/restaking/simulation/decoder.go b/x/restaking/simulation/decoder.go new file mode 100644 index 00000000..f839ca18 --- /dev/null +++ b/x/restaking/simulation/decoder.go @@ -0,0 +1,84 @@ +package simulation + +import ( + "bytes" + "encoding/binary" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/kv" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/milkyway-labs/milkyway/v3/x/restaking/keeper" + "github.com/milkyway-labs/milkyway/v3/x/restaking/types" +) + +// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's +// Value to the corresponding restaking type. +func NewDecodeStore(cdc codec.BinaryCodec, 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.UnbondingIDKey): + idA := binary.BigEndian.Uint64(kvA.Value) + idB := binary.BigEndian.Uint64(kvB.Value) + return fmt.Sprintf("%d\n%d", idA, idB) + + case bytes.Equal(kvA.Key[:1], types.UnbondingIndexKey): + return fmt.Sprintf("%v\n%v", kvA.Value, kvB.Value) + + case bytes.Equal(kvA.Key[:1], types.UnbondingTypeKey): + typeA := binary.BigEndian.Uint32(kvA.Value) + typeB := binary.BigEndian.Uint32(kvB.Value) + return fmt.Sprintf("%d\n%d", typeA, typeB) + + case bytes.Equal(kvA.Key[:1], types.PoolDelegationPrefix): + delegationA := types.MustUnmarshalDelegation(cdc, kvA.Value) + delegationB := types.MustUnmarshalDelegation(cdc, kvB.Value) + return fmt.Sprintf("%v\n%v", delegationA, delegationB) + + case bytes.Equal(kvA.Key[:1], types.PoolUnbondingDelegationPrefix): + undelegationA := types.MustUnmarshalUnbondingDelegation(cdc, kvA.Value) + undelegationB := types.MustUnmarshalUnbondingDelegation(cdc, kvB.Value) + return fmt.Sprintf("%v\n%v", undelegationA, undelegationB) + + case bytes.Equal(kvA.Key[:1], types.OperatorDelegationPrefix): + delegationA := types.MustUnmarshalDelegation(cdc, kvA.Value) + delegationB := types.MustUnmarshalDelegation(cdc, kvB.Value) + return fmt.Sprintf("%v\n%v", delegationA, delegationB) + + case bytes.Equal(kvA.Key[:1], types.OperatorUnbondingDelegationPrefix): + undelegationA := types.MustUnmarshalUnbondingDelegation(cdc, kvA.Value) + undelegationB := types.MustUnmarshalUnbondingDelegation(cdc, kvB.Value) + return fmt.Sprintf("%v\n%v", undelegationA, undelegationB) + + case bytes.Equal(kvA.Key[:1], types.ServiceDelegationPrefix): + delegationA := types.MustUnmarshalDelegation(cdc, kvA.Value) + delegationB := types.MustUnmarshalDelegation(cdc, kvB.Value) + return fmt.Sprintf("%v\n%v", delegationA, delegationB) + + case bytes.Equal(kvA.Key[:1], types.ServiceUnbondingDelegationPrefix): + undelegationA := types.MustUnmarshalUnbondingDelegation(cdc, kvA.Value) + undelegationB := types.MustUnmarshalUnbondingDelegation(cdc, kvB.Value) + return fmt.Sprintf("%v\n%v", undelegationA, undelegationB) + + case bytes.Equal(kvA.Key[:1], types.UnbondingQueueKey): + var listA, listB types.DTDataList + cdc.MustUnmarshal(kvA.Value, &listA) + cdc.MustUnmarshal(kvB.Value, &listB) + return fmt.Sprintf("%v\n%v", listA, listB) + + // Collections + case bytes.Equal(kvA.Key[:1], types.OperatorJoinedServicesPrefix), + bytes.Equal(kvA.Key[:1], types.ServiceOperatorsAllowListPrefix), + bytes.Equal(kvA.Key[:1], types.ServiceSecuringPoolsPrefix), + bytes.Equal(kvA.Key[:1], types.UserPreferencesPrefix): + return collectionsDecoder(kvA, kvB) + + default: + panic(fmt.Sprintf("invalid restaking key prefix %X", kvA.Key[:1])) + } + } +} diff --git a/x/restaking/simulation/genesis.go b/x/restaking/simulation/genesis.go new file mode 100644 index 00000000..06b703c5 --- /dev/null +++ b/x/restaking/simulation/genesis.go @@ -0,0 +1,30 @@ +package simulation + +import ( + "github.com/cosmos/cosmos-sdk/types/module" + + operatorssimulation "github.com/milkyway-labs/milkyway/v3/x/operators/simulation" + poolssimulation "github.com/milkyway-labs/milkyway/v3/x/pools/simulation" + "github.com/milkyway-labs/milkyway/v3/x/restaking/types" + servicessimulation "github.com/milkyway-labs/milkyway/v3/x/services/simulation" +) + +// RandomizedGenState generates a random GenesisState for the restaking module +func RandomizedGenState(simState *module.SimulationState) { + poolsGenesis := poolssimulation.GetGenesisState(simState) + operatorsGenesis := operatorssimulation.GetGenesisState(simState) + servicesGenesis := servicessimulation.GetGenesisState(simState) + + genesis := types.NewGenesis( + RandomOperatorJoinedServices(simState.Rand, operatorsGenesis.Operators, servicesGenesis.Services), + RandomServiceAllowedOperators(simState.Rand, servicesGenesis.Services, operatorsGenesis.Operators), + RandomServiceSecuringPools(simState.Rand, poolsGenesis.Pools, servicesGenesis.Services), + // empty delegations and undelegations since we need to also perform side effects to other + // modules to keep the shares consistent. + nil, + nil, + RandomUserPreferencesEntries(simState.Rand, servicesGenesis.Services), + RandomParams(simState.Rand), + ) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) +} diff --git a/x/restaking/simulation/msg_factory.go b/x/restaking/simulation/msg_factory.go new file mode 100644 index 00000000..10069a67 --- /dev/null +++ b/x/restaking/simulation/msg_factory.go @@ -0,0 +1,637 @@ +package simulation + +import ( + "math/rand" + "slices" + + "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/v3/testutils/simtesting" + operatorskeeper "github.com/milkyway-labs/milkyway/v3/x/operators/keeper" + operatorssimulation "github.com/milkyway-labs/milkyway/v3/x/operators/simulation" + operatorstypes "github.com/milkyway-labs/milkyway/v3/x/operators/types" + poolskeeper "github.com/milkyway-labs/milkyway/v3/x/pools/keeper" + poolssimulation "github.com/milkyway-labs/milkyway/v3/x/pools/simulation" + "github.com/milkyway-labs/milkyway/v3/x/restaking/keeper" + "github.com/milkyway-labs/milkyway/v3/x/restaking/types" + serviceskeeper "github.com/milkyway-labs/milkyway/v3/x/services/keeper" + servicessimulation "github.com/milkyway-labs/milkyway/v3/x/services/simulation" + servicestypes "github.com/milkyway-labs/milkyway/v3/x/services/types" +) + +// Simulation operation weights constants +const ( + DefaultWeightMsgJoinService int = 80 + DefaultWeightMsgLeaveService int = 30 + DefaultWeightMsgAddOperatorToAllowList int = 50 + DefaultWeightMsgRemoveOperatorFromAlloList int = 50 + DefaultWeightMsgBorrowPoolSecurity int = 80 + DefaultWeightMsgCeasePoolSecurityBorrow int = 30 + DefaultWeightMsgDelegatePool int = 80 + DefaultWeightMsgDelegateOperator int = 80 + DefaultWeightMsgDelegateService int = 80 + DefaultWeightMsgSetUserPreferences int = 20 + + OperationWeightMsgJoinService = "op_weight_msg_join_service" + OperationWeightMsgLeaveService = "op_weight_msg_leave_service" + OperationWeightMsgAddOperatorToAlloList = "op_weight_msg_add_operator_to_allow_list" + OperationWeightMsgRemoveOperatorFromAllowList = "op_weight_msg_remove_operator_from_allow_list" + OperationWeightMsgBorrowPoolSecurity = "op_weight_msg_borrow_pool_security" + OperationWeightMsgCeasePoolSecurityBorrow = "op_weight_msg_cease_pool_security_borrow" + OperationWeightMsgDelegatePool = "Op_weight_msg_delegate_pool" + OperationWeightMsgDelegateOperator = "op_weight_msg_delegate_operator" + OperationWeightMsgDelegateService = "op_weight_msg_delegate_service" + OperationWeightMsgSetUserPreferences = "op_weight_msg_set_user_preferences" +) + +// WeightedOperations returns all the operations from the module with their respective weights +func WeightedOperations( + appParams simtypes.AppParams, + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + pk *poolskeeper.Keeper, + opk *operatorskeeper.Keeper, + sk *serviceskeeper.Keeper, + k *keeper.Keeper, +) simulation.WeightedOperations { + var ( + weightMsgJoinService int + weightMsgLeaveService int + weightMsgAddOperatorToAllowList int + weightMsgRemoveOperatorhFromAllowList int + weightMsgBorrowPoolSecurity int + weightMsgCeasePoolSecurityBorrow int + weightMsgDelegatePool int + weightMsgDelegateOperator int + weightMsgDelegateService int + weightMsgSetUserPreferences int + ) + + // Generate the weights + appParams.GetOrGenerate(OperationWeightMsgJoinService, &weightMsgJoinService, nil, func(_ *rand.Rand) { + weightMsgJoinService = DefaultWeightMsgJoinService + }) + + appParams.GetOrGenerate(OperationWeightMsgLeaveService, &weightMsgLeaveService, nil, func(_ *rand.Rand) { + weightMsgLeaveService = DefaultWeightMsgLeaveService + }) + + appParams.GetOrGenerate(OperationWeightMsgAddOperatorToAlloList, &weightMsgAddOperatorToAllowList, nil, func(_ *rand.Rand) { + weightMsgAddOperatorToAllowList = DefaultWeightMsgAddOperatorToAllowList + }) + + appParams.GetOrGenerate(OperationWeightMsgRemoveOperatorFromAllowList, &weightMsgRemoveOperatorhFromAllowList, nil, func(_ *rand.Rand) { + weightMsgRemoveOperatorhFromAllowList = DefaultWeightMsgRemoveOperatorFromAlloList + }) + + appParams.GetOrGenerate(OperationWeightMsgBorrowPoolSecurity, &weightMsgBorrowPoolSecurity, nil, func(_ *rand.Rand) { + weightMsgBorrowPoolSecurity = DefaultWeightMsgBorrowPoolSecurity + }) + + appParams.GetOrGenerate(OperationWeightMsgCeasePoolSecurityBorrow, &weightMsgCeasePoolSecurityBorrow, nil, func(_ *rand.Rand) { + weightMsgCeasePoolSecurityBorrow = DefaultWeightMsgCeasePoolSecurityBorrow + }) + + appParams.GetOrGenerate(OperationWeightMsgDelegatePool, &weightMsgDelegatePool, nil, func(_ *rand.Rand) { + weightMsgDelegatePool = DefaultWeightMsgDelegatePool + }) + + appParams.GetOrGenerate(OperationWeightMsgDelegateOperator, &weightMsgDelegateOperator, nil, func(_ *rand.Rand) { + weightMsgDelegateOperator = DefaultWeightMsgDelegateOperator + }) + + appParams.GetOrGenerate(OperationWeightMsgDelegateService, &weightMsgDelegateService, nil, func(_ *rand.Rand) { + weightMsgDelegateService = DefaultWeightMsgDelegateService + }) + + appParams.GetOrGenerate(OperationWeightMsgSetUserPreferences, &weightMsgSetUserPreferences, nil, func(_ *rand.Rand) { + weightMsgSetUserPreferences = DefaultWeightMsgSetUserPreferences + }) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation(weightMsgJoinService, SimulateMsgJoinService(ak, bk, opk, sk)), + simulation.NewWeightedOperation(weightMsgLeaveService, SimulateMsgLeaveService(ak, bk, opk, sk, k)), + simulation.NewWeightedOperation(weightMsgAddOperatorToAllowList, SimulateMsgAddOperatorToAllowList(ak, bk, opk, sk, k)), + simulation.NewWeightedOperation(weightMsgRemoveOperatorhFromAllowList, SimulateMsgRemoveOperatorFromAllowlist(ak, bk, opk, sk, k)), + simulation.NewWeightedOperation(weightMsgBorrowPoolSecurity, SimulateMsgBorrowPoolSecurity(ak, bk, pk, sk, k)), + simulation.NewWeightedOperation(weightMsgCeasePoolSecurityBorrow, SimulateMsgCeasePoolSecurityBorrow(ak, bk, pk, sk, k)), + simulation.NewWeightedOperation(weightMsgDelegatePool, SimulateMsgDelegatePool(ak, bk, k)), + simulation.NewWeightedOperation(weightMsgDelegateOperator, SimulateMsgDelegateOperator(ak, bk, opk, k)), + simulation.NewWeightedOperation(weightMsgDelegateService, SimulateMsgDelegateService(ak, bk, sk, k)), + simulation.NewWeightedOperation(weightMsgSetUserPreferences, SimulateMsgSetUserPreferences(ak, bk, sk, k)), + } +} + +func SimulateMsgJoinService( + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + opk *operatorskeeper.Keeper, + sk *serviceskeeper.Keeper, +) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + msg := &types.MsgJoinService{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random operator + operator, found := operatorssimulation.GetRandomExistingOperator(r, ctx, opk, func(o operatorstypes.Operator) bool { + return o.IsActive() + }) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get operator"), nil, nil + } + + // Get a random service + service, found := servicessimulation.GetRandomExistingService(r, ctx, sk, func(s servicestypes.Service) bool { + return s.IsActive() + }) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get service"), nil, nil + } + + msg = types.NewMsgJoinService(operator.ID, service.ID, operator.Admin) + + // 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), "error while parsing 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 + } + + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateMsgLeaveService( + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + opk *operatorskeeper.Keeper, + sk *serviceskeeper.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) { + msg := &types.MsgLeaveService{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get all the services + services, err := sk.GetServices(ctx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get services"), nil, nil + } + + // Get a operator that has joined a service + var service servicestypes.Service + operator, found := operatorssimulation.GetRandomExistingOperator(r, ctx, opk, func(o operatorstypes.Operator) bool { + // Search a service that the operator has joined + for _, s := range services { + hasJoined, _ := k.HasOperatorJoinedService(ctx, o.ID, s.ID) + if hasJoined { + service = s + return true + } + } + return false + }) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get operator"), nil, nil + } + + msg = types.NewMsgLeaveService(operator.ID, service.ID, operator.Admin) + + // 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), "error while parsing 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 + } + + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateMsgAddOperatorToAllowList( + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + opk *operatorskeeper.Keeper, + sk *serviceskeeper.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) { + msg := &types.MsgAddOperatorToAllowList{} + + // Get a random operator + operator, found := operatorssimulation.GetRandomExistingOperator(r, ctx, opk, nil) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get operators"), nil, nil + } + + // Get a random service + service, found := servicessimulation.GetRandomExistingService(r, ctx, sk, nil) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get service"), nil, nil + } + + // Ensure that the operator is not in the service allow list + isAllowed, err := k.IsOperatorInServiceAllowList(ctx, service.ID, operator.ID) + if err != nil { + panic(err) + } + if isAllowed { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "operator is already in the service allow list"), nil, nil + } + + msg = types.NewMsgAddOperatorToAllowList(service.ID, operator.ID, service.Admin) + + // Get the admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(service.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "error while parsing 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 + } + + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateMsgRemoveOperatorFromAllowlist( + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + opk *operatorskeeper.Keeper, + sk *serviceskeeper.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) { + msg := &types.MsgRemoveOperatorFromAllowlist{} + + // Get a random operator + operator, found := operatorssimulation.GetRandomExistingOperator(r, ctx, opk, nil) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get operators"), nil, nil + } + + // Get a random service + service, found := servicessimulation.GetRandomExistingService(r, ctx, sk, nil) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get service"), nil, nil + } + + // Ensure that the operator is in the service allow list + isAllowed, err := k.IsOperatorInServiceAllowList(ctx, service.ID, operator.ID) + if err != nil { + panic(err) + } + if !isAllowed { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "operator is not in the service allow list"), nil, nil + } + + msg = types.NewMsgRemoveOperatorFromAllowList(service.ID, operator.ID, service.Admin) + + // Get the admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(service.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "error while parsing 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 + } + + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateMsgBorrowPoolSecurity( + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + pk *poolskeeper.Keeper, + sk *serviceskeeper.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) { + msg := &types.MsgBorrowPoolSecurity{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random pool + pool, found := poolssimulation.GetRandomExistingPool(r, ctx, pk, nil) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "error while getting pools"), nil, nil + } + + // Get a random service + service, found := servicessimulation.GetRandomExistingService(r, ctx, sk, func(s servicestypes.Service) bool { + return s.IsActive() + }) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get service"), nil, nil + } + + isServiceSecured, err := k.IsPoolInServiceSecuringPools(ctx, service.ID, pool.ID) + if err != nil { + panic(err) + } + if isServiceSecured { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "service already secured by pool"), nil, nil + } + + msg = types.NewMsgBorrowPoolSecurity(service.ID, pool.ID, service.Admin) + + // Get the admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(service.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "error while parsing 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 + } + + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateMsgCeasePoolSecurityBorrow( + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + pk *poolskeeper.Keeper, + sk *serviceskeeper.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) { + msg := &types.MsgCeasePoolSecurityBorrow{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random pool + pool, found := poolssimulation.GetRandomExistingPool(r, ctx, pk, nil) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "error while getting pools"), nil, nil + } + + // Get a random service + service, found := servicessimulation.GetRandomExistingService(r, ctx, sk, nil) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get service"), nil, nil + } + + isServiceSecured, err := k.IsPoolInServiceSecuringPools(ctx, service.ID, pool.ID) + if err != nil { + panic(err) + } + if !isServiceSecured { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "service not secured by pool"), nil, nil + } + + msg = types.NewMsgCeasePoolSecurityBorrow(service.ID, pool.ID, service.Admin) + + // Get the admin account that should sign the transaction + adminAddress, err := sdk.AccAddressFromBech32(service.Admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "error while parsing 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 + } + + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, signer) + } +} + +func SimulateMsgDelegatePool( + 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) { + msg := &types.MsgDelegatePool{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random delegator with a random amount + delegator, coins, skip := randomDelegatorAndAmount(r, ctx, accs, k, bk, ak) + + // If coins slice is empty, we can not create valid msg + if len(coins) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "empty coins slice"), nil, nil + } + + if skip { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "skip delegate"), nil, nil + } + + msg = types.NewMsgDelegatePool(coins[0], delegator.Address.String()) + + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, delegator) + } +} + +func SimulateMsgDelegateOperator( + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + opk *operatorskeeper.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) { + msg := &types.MsgDelegateOperator{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random operator + operator, found := operatorssimulation.GetRandomExistingOperator(r, ctx, opk, func(o operatorstypes.Operator) bool { + return o.IsActive() + }) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get operator"), nil, nil + } + + // Get a random delegator with a random amount + delegator, coins, skip := randomDelegatorAndAmount(r, ctx, accs, k, bk, ak) + + // If coins slice is empty, we can not create valid msg + if len(coins) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "empty coins slice"), nil, nil + } + + if skip { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "skip delegate"), nil, nil + } + + msg = types.NewMsgDelegateOperator(operator.ID, coins, delegator.Address.String()) + + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, delegator) + } +} + +func SimulateMsgDelegateService( + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + sk *serviceskeeper.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) { + msg := &types.MsgDelegateService{} + + // No account skipping + if len(accs) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + // Get a random service + service, found := servicessimulation.GetRandomExistingService(r, ctx, sk, func(s servicestypes.Service) bool { + return s.IsActive() + }) + if !found { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "could not get service"), nil, nil + } + + // Get a random delegator with a random amount + delegator, coins, skip := randomDelegatorAndAmount(r, ctx, accs, k, bk, ak) + // Filter the coins to only those that can be restaked toward the service + coins = filterServiceRestakableCoins(sk, ctx, service.ID, coins) + + // If coins slice is empty, we can not create valid msg + if len(coins) == 0 { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "empty coins slice"), nil, nil + } + + if skip { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "skip delegate"), nil, nil + } + + msg = types.NewMsgDelegateService(service.ID, coins, delegator.Address.String()) + + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, delegator) + } +} + +func SimulateMsgSetUserPreferences( + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + sk *serviceskeeper.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) { + msg := &types.MsgSetUserPreferences{} + + delegator, _ := simtypes.RandomAcc(r, accs) + acc := ak.GetAccount(ctx, delegator.Address) + if acc == nil { + return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "no accounts"), nil, nil + } + + services, err := sk.GetServices(ctx) + if err != nil { + panic(err) + } + msg = types.NewMsgSetUserPreferences(RandomUserPreferences(r, services), delegator.Address.String()) + + return simtesting.SendMsg(r, types.ModuleName, app, ak, bk, msg, ctx, delegator) + } +} + +// Get a random account with an amount that can be delegated from the provided +// account. +func randomDelegatorAndAmount( + r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, k *keeper.Keeper, bk bankkeeper.Keeper, ak authkeeper.AccountKeeper, +) (simtypes.Account, sdk.Coins, bool) { + delegator, _ := simtypes.RandomAcc(r, accs) + + acc := ak.GetAccount(ctx, delegator.Address) + if acc == nil { + return delegator, nil, true + } + + spendable := bk.SpendableCoins(ctx, acc.GetAddress()) + // Filter the spendable coins to only include the restakable ones + restakableCoins := sdk.NewCoins() + for _, coin := range spendable { + isRestakable, err := k.IsDenomRestakable(ctx, coin.Denom) + if err != nil { + panic(err) + } + if isRestakable { + restakableCoins = restakableCoins.Add(coin) + } + } + + coins := simtypes.RandSubsetCoins(r, restakableCoins) + if coins.Empty() { + return delegator, nil, true + } + + return delegator, coins, false +} + +func filterServiceRestakableCoins(sk *serviceskeeper.Keeper, ctx sdk.Context, serviceID uint32, coins sdk.Coins) sdk.Coins { + serviceParams, err := sk.GetServiceParams(ctx, serviceID) + if err != nil { + panic(err) + } + // If empty allows all denoms + if len(serviceParams.AllowedDenoms) == 0 { + return coins + } + + filteredCoins := sdk.NewCoins() + for _, coin := range coins { + if slices.Contains(serviceParams.AllowedDenoms, coin.Denom) { + filteredCoins = filteredCoins.Add(coin) + } + } + + return filteredCoins +} diff --git a/x/restaking/simulation/proposals.go b/x/restaking/simulation/proposals.go new file mode 100644 index 00000000..ee1099f3 --- /dev/null +++ b/x/restaking/simulation/proposals.go @@ -0,0 +1,47 @@ +package simulation + +import ( + "math/rand" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/milkyway-labs/milkyway/v3/x/restaking/keeper" + "github.com/milkyway-labs/milkyway/v3/x/restaking/types" +) + +// Simulation operation weights constants +const ( + DefaultWeightMsgUpdateParams int = 50 + + OperationWeightMsgUpdateParams = "op_weight_msg_update_params" +) + +// ProposalMsgs defines the module weighted proposals' contents +func ProposalMsgs(keeper *keeper.Keeper) []simtypes.WeightedProposalMsg { + return []simtypes.WeightedProposalMsg{ + simulation.NewWeightedProposalMsg( + OperationWeightMsgUpdateParams, + DefaultWeightMsgUpdateParams, + SimulateMsgUpdateParams, + ), + } +} + +// SimulateMsgUpdateParams returns a random MsgUpdateParams +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") + + params := types.DefaultParams() + unbondingDays := time.Duration(r.Intn(7) + 1) + params.UnbondingTime = time.Hour * 24 * unbondingDays + + return &types.MsgUpdateParams{ + Authority: authority.String(), + Params: params, + } +} diff --git a/x/restaking/simulation/utils.go b/x/restaking/simulation/utils.go new file mode 100644 index 00000000..66fcc922 --- /dev/null +++ b/x/restaking/simulation/utils.go @@ -0,0 +1,167 @@ +package simulation + +import ( + "math/rand" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + simulation "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/milkyway-labs/milkyway/v3/testutils/simtesting" + "github.com/milkyway-labs/milkyway/v3/utils" + operatorstypes "github.com/milkyway-labs/milkyway/v3/x/operators/types" + poolstypes "github.com/milkyway-labs/milkyway/v3/x/pools/types" + "github.com/milkyway-labs/milkyway/v3/x/restaking/keeper" + "github.com/milkyway-labs/milkyway/v3/x/restaking/types" + servicestypes "github.com/milkyway-labs/milkyway/v3/x/services/types" +) + +func RandomOperatorJoinedServices( + r *rand.Rand, + operators []operatorstypes.Operator, + services []servicestypes.Service, +) []types.OperatorJoinedServices { + // Randomly join an operator to a service + var operatorJoinedServices []types.OperatorJoinedServices + if len(operators) == 0 || len(services) == 0 { + return operatorJoinedServices + } + + for _, operator := range simtesting.RandomSubSlice(r, operators) { + serviceIDs := utils.Map(simtesting.RandomSubSlice(r, services), func(s servicestypes.Service) uint32 { + return s.ID + }) + + // Ignore if the joined services list is empty + if len(serviceIDs) == 0 { + continue + } + + operatorJoinedServices = append(operatorJoinedServices, types.NewOperatorJoinedServices(operator.ID, serviceIDs)) + } + + return operatorJoinedServices +} + +func RandomServiceAllowedOperators( + r *rand.Rand, + services []servicestypes.Service, + operators []operatorstypes.Operator, +) []types.ServiceAllowedOperators { + var serviceAllowedOperators []types.ServiceAllowedOperators + if len(operators) == 0 || len(services) == 0 { + return serviceAllowedOperators + } + + for _, service := range simtesting.RandomSubSlice(r, services) { + allowedOperatorIDs := utils.Map(simtesting.RandomSubSlice(r, operators), func(o operatorstypes.Operator) uint32 { + return o.ID + }) + + // Ignore if the allow list is empty + if len(allowedOperatorIDs) == 0 { + continue + } + + serviceAllowedOperators = append( + serviceAllowedOperators, + types.NewServiceAllowedOperators(service.ID, allowedOperatorIDs), + ) + } + + return serviceAllowedOperators +} + +func RandomServiceSecuringPools( + r *rand.Rand, + pools []poolstypes.Pool, + services []servicestypes.Service, +) []types.ServiceSecuringPools { + var serviceSecuringPools []types.ServiceSecuringPools + if len(pools) == 0 || len(services) == 0 { + return serviceSecuringPools + } + + for _, service := range simtesting.RandomSubSlice(r, services) { + allowedPoolIDs := utils.Map(simtesting.RandomSubSlice(r, pools), func(o poolstypes.Pool) uint32 { + return o.ID + }) + // Ignore if the allow list is empty + if len(allowedPoolIDs) == 0 { + continue + } + + serviceSecuringPools = append( + serviceSecuringPools, + types.NewServiceSecuringPools(service.ID, allowedPoolIDs)) + } + + return serviceSecuringPools +} + +func RandomUserPreferencesEntries( + r *rand.Rand, + services []servicestypes.Service, +) []types.UserPreferencesEntry { + var usersPreferences []types.UserPreferencesEntry + if len(services) == 0 { + return usersPreferences + } + + accounts := simulation.RandomAccounts(r, r.Intn(10)) + for _, account := range accounts { + // Create the user preferences + userPreferences := RandomUserPreferences(r, services) + usersPreferences = append( + usersPreferences, + types.NewUserPreferencesEntry(account.Address.String(), userPreferences), + ) + } + + return usersPreferences +} + +func RandomParams(r *rand.Rand) types.Params { + unbondingDays := time.Duration(r.Intn(7) + 1) + return types.NewParams(time.Hour*24*unbondingDays, nil) +} + +func RandomUserPreferences(r *rand.Rand, services []servicestypes.Service) types.UserPreferences { + // Add some services to the user's trusted services + userTrustedServiceIDs := utils.Map(simtesting.RandomSubSlice(r, services), func(s servicestypes.Service) uint32 { + return s.ID + }) + + // Create the user preferences + userPreferences := types.NewUserPreferences( + // 50% of trusting non accredited service + r.Intn(2) == 0, + // 50% of trusting accredited service + r.Intn(2) == 0, + userTrustedServiceIDs, + ) + return userPreferences +} + +func GetRandomExistingDelegation( + r *rand.Rand, + ctx sdk.Context, + k *keeper.Keeper, + filter func(delegation types.Delegation) bool, +) (types.Delegation, bool) { + delegations, err := k.GetAllDelegations(ctx) + if err != nil { + panic(err) + } + + if filter != nil { + delegations = utils.Filter(delegations, filter) + } + + if len(delegations) == 0 { + return types.Delegation{}, false + } + + randomIndex := r.Intn(len(delegations)) + return delegations[randomIndex], true +} diff --git a/x/services/simulation/genesis.go b/x/services/simulation/genesis.go index e7da7fcc..7fe59e99 100644 --- a/x/services/simulation/genesis.go +++ b/x/services/simulation/genesis.go @@ -42,3 +42,16 @@ func RandomizedGenState(simState *module.SimulationState) { servicesGenesis := types.NewGenesisState(nextServiceID, services, servicesParams, params) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(servicesGenesis) } + +// GetGenesisState returns the services genesis state from the SimulationState +func GetGenesisState(simState *module.SimulationState) types.GenesisState { + operatorsGenesisJSON, found := simState.GenState[types.ModuleName] + var operatorsGenesis types.GenesisState + if found { + simState.Cdc.MustUnmarshalJSON(operatorsGenesisJSON, &operatorsGenesis) + } else { + operatorsGenesis = *types.DefaultGenesis() + } + + return operatorsGenesis +}