diff --git a/app/app.go b/app/app.go index 34bfac3e2..0445d5f55 100644 --- a/app/app.go +++ b/app/app.go @@ -12,6 +12,9 @@ import ( autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1" runtimeservices "github.com/cosmos/cosmos-sdk/runtime/services" + "github.com/cosmos/cosmos-sdk/x/auth" + authsims "github.com/cosmos/cosmos-sdk/x/auth/simulation" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" simappparams "cosmossdk.io/simapp/params" "github.com/cosmos/cosmos-sdk/runtime" @@ -113,6 +116,8 @@ type App struct { mm *module.Manager // module configurator configurator module.Configurator + // simulation manager + sm *module.SimulationManager } // New returns a reference to an initialized blockchain app @@ -195,6 +200,13 @@ func New( app.configurator = module.NewConfigurator(app.appCodec, app.MsgServiceRouter(), app.GRPCQueryRouter()) app.mm.RegisterServices(app.configurator) + /**** Simulations ****/ + overrideModules := map[string]module.AppModuleSimulation{ + authtypes.ModuleName: auth.NewAppModule(app.appCodec, app.AccountKeeper, authsims.RandomGenesisAccounts, app.GetSubspace(authtypes.ModuleName)), + } + app.sm = module.NewSimulationManagerFromAppModules(app.mm.Modules, overrideModules) + app.sm.RegisterStoreDecoders() + // initialize stores app.MountKVStores(keepers.KVStoreKeys) app.MountTransientStores(app.GetTransientStoreKey()) @@ -358,7 +370,7 @@ func RegisterSwaggerAPI(_ client.Context, rtr *mux.Router) { // SimulationManager implements the SimulationApp interface func (app *App) SimulationManager() *module.SimulationManager { - return nil + return app.sm } // GetTxConfig implements ibctesting.TestingApp diff --git a/app/keepers/modules.go b/app/keepers/modules.go index f24f4f4a3..09382b8d8 100644 --- a/app/keepers/modules.go +++ b/app/keepers/modules.go @@ -207,7 +207,7 @@ func (a *AppKeepers) SetupModules( iro.NewAppModule(appCodec, *a.IROKeeper), sequencermodule.NewAppModule(appCodec, a.SequencerKeeper), - sponsorship.NewAppModule(a.SponsorshipKeeper), + sponsorship.NewAppModule(a.SponsorshipKeeper, a.AccountKeeper, a.BankKeeper, a.IncentivesKeeper, a.StakingKeeper), streamermodule.NewAppModule(a.StreamerKeeper, a.AccountKeeper, a.BankKeeper, a.EpochsKeeper), delayedackmodule.NewAppModule(appCodec, a.DelayedAckKeeper, a.delayedAckMiddleware), denommetadatamodule.NewAppModule(a.DenomMetadataKeeper, *a.EvmKeeper, a.BankKeeper), diff --git a/ibctesting/genesis_bridge_test.go b/ibctesting/genesis_bridge_test.go index 5736ffad9..a850b91f7 100644 --- a/ibctesting/genesis_bridge_test.go +++ b/ibctesting/genesis_bridge_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/suite" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -13,7 +15,6 @@ import ( clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" ibctesting "github.com/cosmos/ibc-go/v7/testing" - "github.com/stretchr/testify/suite" "github.com/dymensionxyz/dymension/v3/app/apptesting" appparams "github.com/dymensionxyz/dymension/v3/app/params" diff --git a/internal/collcompat/simulation.go b/internal/collcompat/simulation.go new file mode 100644 index 000000000..d1d49aea9 --- /dev/null +++ b/internal/collcompat/simulation.go @@ -0,0 +1,52 @@ +package collcompat + +import ( + "bytes" + "fmt" + + "cosmossdk.io/collections" + collcodec "cosmossdk.io/collections/codec" + + "github.com/cosmos/cosmos-sdk/types/kv" +) + +func NewStoreDecoderFuncFromCollectionsSchema(schema collections.Schema) func(kvA, kvB kv.Pair) string { + colls := schema.ListCollections() + prefixes := make([][]byte, len(colls)) + valueCodecs := make([]collcodec.UntypedValueCodec, len(colls)) + for i, coll := range colls { + prefixes[i] = coll.GetPrefix() + valueCodecs[i] = coll.ValueCodec() + } + + return func(kvA, kvB kv.Pair) string { + for i, prefix := range prefixes { + if bytes.HasPrefix(kvA.Key, prefix) { + if !bytes.HasPrefix(kvB.Key, prefix) { + panic(fmt.Sprintf("prefix mismatch, keyA has prefix %x (%s), but keyB does not %x (%s)", prefix, prefix, kvB.Key, kvB.Key)) + } + vc := valueCodecs[i] + // unmarshal kvA.Value to the corresponding type + vA, err := vc.Decode(kvA.Value) + if err != nil { + panic(err) + } + // unmarshal kvB.Value to the corresponding type + vB, err := vc.Decode(kvB.Value) + if err != nil { + panic(err) + } + vAString, err := vc.Stringify(vA) + if err != nil { + panic(err) + } + vBString, err := vc.Stringify(vB) + if err != nil { + panic(err) + } + return vAString + "\n" + vBString + } + } + panic(fmt.Errorf("unexpected key %X (%s)", kvA.Key, kvA.Key)) + } +} diff --git a/simulation/genesis_test.go b/simulation/genesis_test.go new file mode 100644 index 000000000..36e060acf --- /dev/null +++ b/simulation/genesis_test.go @@ -0,0 +1,204 @@ +package simulation_test + +import ( + "fmt" + "time" + + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/codec" + auth "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" + govtypes2 "github.com/cosmos/cosmos-sdk/x/gov/types" + govtypes1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + slashing "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" + epochstypes "github.com/osmosis-labs/osmosis/v15/x/epochs/types" + gammtypes "github.com/osmosis-labs/osmosis/v15/x/gamm/types" + txfeestypes "github.com/osmosis-labs/osmosis/v15/x/txfees/types" + + "github.com/dymensionxyz/dymension/v3/app" + dymnstypes "github.com/dymensionxyz/dymension/v3/x/dymns/types" + incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types" + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + sequencertypes "github.com/dymensionxyz/dymension/v3/x/sequencer/types" +) + +func prepareGenesis(cdc codec.JSONCodec) (app.GenesisState, error) { + genesis := app.NewDefaultGenesisState(cdc) + + // Modify gov params + govGenesis := govtypes1.DefaultGenesisState() + govGenesis.Params.MinDeposit[0].Amount = math.NewInt(10000000000) + govGenesis.Params.MinDeposit[0].Denom = "adym" + govVotingPeriod := time.Minute + govGenesis.Params.VotingPeriod = &govVotingPeriod + govRawGenesis, err := cdc.MarshalJSON(govGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal gov genesis state: %w", err) + } + genesis[govtypes2.ModuleName] = govRawGenesis + + // Modify rollapp params + rollappGenesis := rollapptypes.DefaultGenesis() + rollappGenesis.Params.DisputePeriodInBlocks = 50 + rollappRawGenesis, err := cdc.MarshalJSON(rollappGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal rollapp genesis state: %w", err) + } + genesis[rollapptypes.ModuleName] = rollappRawGenesis + + // Modify sequencer params + sequencerGenesis := sequencertypes.DefaultGenesis() + sequencerGenesis.Params.NoticePeriod = time.Minute + sequencerRawGenesis, err := cdc.MarshalJSON(sequencerGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal sequencer genesis state: %w", err) + } + genesis[sequencertypes.ModuleName] = sequencerRawGenesis + + // Modify auth params + authGenesis := auth.DefaultGenesisState() + authGenesis.Params.TxSizeCostPerByte = 100 + authRawGenesis, err := cdc.MarshalJSON(authGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal auth genesis state: %w", err) + } + genesis[auth.ModuleName] = authRawGenesis + + // Modify slashing params + slashingGenesis := slashing.DefaultGenesisState() + slashingGenesis.Params.SignedBlocksWindow = 10000 + slashingGenesis.Params.MinSignedPerWindow = math.LegacyMustNewDecFromStr("0.800000000000000000") + slashingGenesis.Params.DowntimeJailDuration = 2 * time.Minute + slashingGenesis.Params.SlashFractionDowntime = math.LegacyZeroDec() + slashingRawGenesis, err := cdc.MarshalJSON(slashingGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal slashing genesis state: %w", err) + } + genesis[slashing.ModuleName] = slashingRawGenesis + + // Modify staking params + stakingGenesis := stakingtypes.DefaultGenesisState() + stakingGenesis.Params.BondDenom = "adym" + stakingRawGenesis, err := cdc.MarshalJSON(stakingGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal staking genesis state: %w", err) + } + genesis[stakingtypes.ModuleName] = stakingRawGenesis + + // Modify mint params + mintGenesis := minttypes.DefaultGenesisState() + mintGenesis.Params.MintDenom = "adym" + mintRawGenesis, err := cdc.MarshalJSON(mintGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal mint genesis state: %w", err) + } + genesis[minttypes.ModuleName] = mintRawGenesis + + // Modify evm params + evmGenesis := evmtypes.DefaultGenesisState() + evmGenesis.Params.EvmDenom = "adym" + evmGenesis.Params.EnableCreate = false + evmRawGenesis, err := cdc.MarshalJSON(evmGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal evm genesis state: %w", err) + } + genesis[evmtypes.ModuleName] = evmRawGenesis + + // Modify feemarket params + feemarketGenesis := feemarkettypes.DefaultGenesisState() + feemarketGenesis.Params.NoBaseFee = true + feemarketRawGenesis, err := cdc.MarshalJSON(feemarketGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal feemarket genesis state: %w", err) + } + genesis[feemarkettypes.ModuleName] = feemarketRawGenesis + + // Modify dymns params + dymnsGenesis := dymnstypes.DefaultGenesis() + dymnsGenesis.Params.Misc.SellOrderDuration = 2 * time.Minute + dymnsRawGenesis, err := cdc.MarshalJSON(dymnsGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal dymns genesis state: %w", err) + } + genesis[dymnstypes.ModuleName] = dymnsRawGenesis + + // Modify bank denom metadata + bankGenesis := banktypes.DefaultGenesisState() + bankGenesis.DenomMetadata = []banktypes.Metadata{ + { + Base: "adym", + DenomUnits: []*banktypes.DenomUnit{ + {Denom: "adym", Exponent: 0}, + {Denom: "DYM", Exponent: 18}, + }, + Description: "Denom metadata for DYM (adym)", + Display: "DYM", + Name: "DYM", + Symbol: "DYM", + }, + } + bankRawGenesis, err := cdc.MarshalJSON(bankGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal bank genesis state: %w", err) + } + genesis[banktypes.ModuleName] = bankRawGenesis + + // Modify misc params + crisisGenesis := crisistypes.DefaultGenesisState() + crisisGenesis.ConstantFee.Denom = "adym" + crisisRawGenesis, err := cdc.MarshalJSON(crisisGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal crisis genesis state: %w", err) + } + genesis[crisistypes.ModuleName] = crisisRawGenesis + + txfeesGenesis := txfeestypes.DefaultGenesis() + txfeesGenesis.Basedenom = "adym" + txfeesGenesis.Params.EpochIdentifier = "minute" + txfeesRawGenesis, err := cdc.MarshalJSON(txfeesGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal txfees genesis state: %w", err) + } + genesis[txfeestypes.ModuleName] = txfeesRawGenesis + + gammGenesis := gammtypes.DefaultGenesis() + gammGenesis.Params.PoolCreationFee[0].Denom = "adym" + gammGenesis.Params.EnableGlobalPoolFees = true + gammRawGenesis, err := cdc.MarshalJSON(gammGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal gamm genesis state: %w", err) + } + genesis[gammtypes.ModuleName] = gammRawGenesis + + // Modify incentives params + incentivesGenesis := incentivestypes.DefaultGenesis() + incentivesGenesis.Params.DistrEpochIdentifier = "minute" + incentivesGenesis.LockableDurations = []time.Duration{time.Minute} + incentivesRawGenesis, err := cdc.MarshalJSON(incentivesGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal incentives genesis state: %w", err) + } + genesis[incentivestypes.ModuleName] = incentivesRawGenesis + + // Modify epochs params + epochsGenesis := epochstypes.DefaultGenesis() + epochsGenesis.Epochs = append(epochsGenesis.Epochs, epochstypes.EpochInfo{ + Identifier: "minute", + StartTime: time.Time{}, + Duration: time.Minute, + CurrentEpoch: 0, + CurrentEpochStartHeight: 0, + }) + epochsRawGenesis, err := cdc.MarshalJSON(epochsGenesis) + if err != nil { + return app.GenesisState{}, fmt.Errorf("failed to marshal epochs genesis state: %w", err) + } + genesis[epochstypes.ModuleName] = epochsRawGenesis + + return genesis, nil +} diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go new file mode 100644 index 000000000..cdb4f3704 --- /dev/null +++ b/simulation/simulation_test.go @@ -0,0 +1,248 @@ +package simulation_test + +import ( + "encoding/base64" + "fmt" + "math/rand" + "os" + "testing" + + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/server" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/cosmos-sdk/types" + simulationtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" + "github.com/cosmos/cosmos-sdk/x/simulation" + simcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" + "github.com/stretchr/testify/require" + + "github.com/dymensionxyz/dymension/v3/app" + appParams "github.com/dymensionxyz/dymension/v3/app/params" +) + +func init() { + simcli.GetSimulatorFlags() +} + +const SimulationAppChainID = "dymension_100-1" + +/* +To execute a completely pseudo-random simulation (from the root of the repository): + + go test ./simulation \ + -run=TestFullAppSimulation \ + -Enabled=true \ + -NumBlocks=100 \ + -BlockSize=200 \ + -Commit=true \ + -Seed=99 \ + -Period=1 \ + -PrintAllInvariants=true \ + -v -timeout 24h + +To export the simulation params to a file at a given block height: + + go test ./simulation \ + -run=TestFullAppSimulation \ + -Enabled=true \ + -NumBlocks=100 \ + -BlockSize=200 \ + -Commit=true \ + -Seed=99 \ + -Period=1 \ + -PrintAllInvariants=true \ + -ExportParamsPath=/path/to/params.json \ + -ExportParamsHeight=50 \ + -v -timeout 24h + +To export the simulation app state (i.e genesis) to a file: + + go test ./simulation \ + -run=TestFullAppSimulation \ + -Enabled=true \ + -NumBlocks=100 \ + -BlockSize=200 \ + -Commit=true \ + -Seed=99 \ + -Period=1 \ + -PrintAllInvariants=true \ + -ExportStatePath=/path/to/genesis.json \ + -v -timeout 24h +*/ +func TestFullAppSimulation(t *testing.T) { + config := simcli.NewConfigFromFlags() + config.ChainID = SimulationAppChainID + + db, dir, logger, skip, err := simtestutil.SetupSimulation(config, "leveldb-app-sim", "Simulation", simcli.FlagVerboseValue, simcli.FlagEnabledValue) + if skip { + t.Skip("skipping application simulation") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + require.NoError(t, db.Close()) + require.NoError(t, os.RemoveAll(dir)) + }() + + appOptions := make(simtestutil.AppOptionsMap, 0) + appOptions[flags.FlagHome] = app.DefaultNodeHome + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue // period at which invariants are checked + appOptions[cli.FlagDefaultBondDenom] = "adym" + types.DefaultBondDenom = "adym" + types.DefaultPowerReduction = math.NewIntFromUint64(1000000) // overwrite evm module's default power reduction + + encoding := app.MakeEncodingConfig() + + appParams.SetAddressPrefixes() + + dymdApp := app.New( + logger, + db, + nil, + true, + map[int64]bool{}, + app.DefaultNodeHome, + 0, + encoding, + appOptions, + baseapp.SetChainID(SimulationAppChainID), + ) + require.Equal(t, "dymension", dymdApp.Name()) + + genesis, err := prepareGenesis(dymdApp.AppCodec()) + require.NoError(t, err) + + // run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + dymdApp.BaseApp, + simtestutil.AppStateFn(dymdApp.AppCodec(), dymdApp.SimulationManager(), genesis), + simulationtypes.RandomAccounts, + simtestutil.SimulationOperations(dymdApp, dymdApp.AppCodec(), config), + dymdApp.ModuleAccountAddrs(), + config, + dymdApp.AppCodec(), + ) + + // export state and simParams before the simulation error is checked + err = simtestutil.CheckExportSimulation(dymdApp, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simtestutil.PrintStats(db) + } + + appHash := base64.StdEncoding.EncodeToString(dymdApp.LastCommitID().Hash) + fmt.Println("App hash:", appHash) +} + +/* +TestAppStateDeterminism runs a simulation to ensure that the application is deterministic. +It generates a random seed and runs the simulation multiple times with the same seed to ensure +that the resulting app hash is the same each time. You may manually specify a seed by using +the -Seed flag. The test may take a few minutes to run. + + go test ./simulation \ + -run=TestAppStateDeterminism \ + -Enabled=true \ + -NumBlocks=50 \ + -BlockSize=300 \ + -Commit=true \ + -Period=0 \ + -v -timeout 24h +*/ +func TestAppStateDeterminism(t *testing.T) { + if !simcli.FlagEnabledValue { + t.Skip("skipping application simulation") + } + + config := simcli.NewConfigFromFlags() + config.InitialBlockHeight = 1 + config.ExportParamsPath = "" + config.OnOperation = false + config.AllInvariants = false + config.ChainID = SimulationAppChainID + + numSeeds := 1 + numTimesToRunPerSeed := 5 + + appOptions := make(simtestutil.AppOptionsMap, 0) + appOptions[flags.FlagHome] = app.DefaultNodeHome + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue // period at which invariants are checked + appOptions[cli.FlagDefaultBondDenom] = "adym" + types.DefaultBondDenom = "adym" + types.DefaultPowerReduction = math.NewIntFromUint64(1000000) // overwrite evm module's default power reduction + + encoding := app.MakeEncodingConfig() + + for i := 0; i < numSeeds; i++ { + if config.Seed == simcli.DefaultSeedValue { + // overwrite default seed + config.Seed = rand.Int63() //nolint:gosec + } + + fmt.Println("config.Seed: ", config.Seed) + + appHashList := make([]string, numTimesToRunPerSeed) + + for j := 0; j < numTimesToRunPerSeed; j++ { + db, _, logger, _, err := simtestutil.SetupSimulation(config, "leveldb-app-sim", "Simulation", simcli.FlagVerboseValue, simcli.FlagEnabledValue) + require.NoError(t, err, "simulation setup failed") + + dymdApp := app.New( + logger, + db, + nil, + true, + map[int64]bool{}, + app.DefaultNodeHome, + 0, + encoding, + appOptions, + baseapp.SetChainID(SimulationAppChainID), + ) + require.Equal(t, "dymension", dymdApp.Name()) + + fmt.Printf( + "running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n", + config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, + ) + + genesis, err := prepareGenesis(dymdApp.AppCodec()) + require.NoError(t, err) + + _, _, err = simulation.SimulateFromSeed( + t, + os.Stdout, + dymdApp.BaseApp, + simtestutil.AppStateFn(dymdApp.AppCodec(), dymdApp.SimulationManager(), genesis), + simulationtypes.RandomAccounts, + simtestutil.SimulationOperations(dymdApp, dymdApp.AppCodec(), config), + dymdApp.ModuleAccountAddrs(), + config, + dymdApp.AppCodec(), + ) + require.NoError(t, err) + + if config.Commit { + simtestutil.PrintStats(db) + } + + appHash := base64.StdEncoding.EncodeToString(dymdApp.LastCommitID().Hash) + fmt.Printf("Seed: %d, appempt: %d/%d, app hash: %s\n", config.Seed, j+1, numTimesToRunPerSeed, appHash) + appHashList[j] = appHash + + if j != 0 { + require.Equal( + t, appHashList[0], appHashList[j], + "non-determinism in seed %d: %d/%d, attempt: %d/%d\n", config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, + ) + } + } + } +} diff --git a/simulation/types/expected_keepers.go b/simulation/types/expected_keepers.go new file mode 100644 index 000000000..f2361bfbb --- /dev/null +++ b/simulation/types/expected_keepers.go @@ -0,0 +1,30 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types" +) + +// AccountKeeper defines the expected account keeper used for simulations (noalias) +type AccountKeeper interface { + GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI +} + +// BankKeeper defines the expected interface needed to retrieve account balances. +type BankKeeper interface { + SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error +} + +type IncentivesKeeper interface { + GetGauges(ctx sdk.Context) []incentivestypes.Gauge +} + +type StakingKeeper interface { + GetAllValidators(ctx sdk.Context) (validators []stakingtypes.Validator) + GetValidatorDelegations(ctx sdk.Context, valAddr sdk.ValAddress) []stakingtypes.Delegation +} diff --git a/simulation/types/helpers.go b/simulation/types/helpers.go new file mode 100644 index 000000000..6e9064d6c --- /dev/null +++ b/simulation/types/helpers.go @@ -0,0 +1,30 @@ +package types + +import ( + "errors" + "math/rand" + + "cosmossdk.io/math" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" +) + +// RandIntBetween returns a random integer in the range [min; max). +func RandIntBetween(r *rand.Rand, min, max math.Int) (math.Int, error) { + if min.GT(max) { + return math.Int{}, errors.New("min cannot be greater than max") + } + + // Calculate the range size: + // rangeSize = max - min + rangeSize := max.Sub(min) + + // Get a random number in the range (0; rangeSize] + randInRange, err := simtypes.RandPositiveInt(r, rangeSize) + if err != nil { + return math.Int{}, err + } + + // Adjust the random number to be in the range [min; max): + // (0; rangeSize] + min - 1 = (min-1; max-1] = [min; max) + return min.Add(randInRange).Sub(math.OneInt()), nil +} diff --git a/simulation/types/incentives.go b/simulation/types/incentives.go new file mode 100644 index 000000000..07e3a67ef --- /dev/null +++ b/simulation/types/incentives.go @@ -0,0 +1,37 @@ +package types + +import ( + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" + + incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types" +) + +// RandomGauge takes a context, then returns a random existing gauge. +func RandomGauge(ctx sdk.Context, r *rand.Rand, k IncentivesKeeper) *incentivestypes.Gauge { + gauges := k.GetGauges(ctx) + if len(gauges) == 0 { + return nil + } + return &gauges[r.Intn(len(gauges))] +} + +// RandomGaugeSubset takes a context, a random number generator, and an IncentivesKeeper, +// then returns a random subset of gauges. Gauges are non-duplicated. +func RandomGaugeSubset(ctx sdk.Context, r *rand.Rand, k IncentivesKeeper) []incentivestypes.Gauge { + allGauges := k.GetGauges(ctx) + if len(allGauges) == 0 { + return nil + } + + numGauges := r.Intn(len(allGauges)) + 1 + + // Shuffle the list of all gauges + r.Shuffle(len(allGauges), func(i, j int) { + allGauges[i], allGauges[j] = allGauges[j], allGauges[i] + }) + + // Select the first numGauges elements from the shuffled list + return allGauges[:numGauges] +} diff --git a/simulation/types/staking.go b/simulation/types/staking.go new file mode 100644 index 000000000..dd9dcc9de --- /dev/null +++ b/simulation/types/staking.go @@ -0,0 +1,26 @@ +package types + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// RandomDelegation returns a random delegation from a random validator. +func RandomDelegation(ctx sdk.Context, r *rand.Rand, k StakingKeeper) *stakingtypes.Delegation { + allVals := k.GetAllValidators(ctx) + srcVal, ok := testutil.RandSliceElem(r, allVals) + if !ok { + return nil + } + + srcAddr := srcVal.GetOperator() + delegations := k.GetValidatorDelegations(ctx, srcAddr) + if delegations == nil { + return nil + } + + return &delegations[r.Intn(len(delegations))] +} diff --git a/x/dymns/keeper/hooks.go b/x/dymns/keeper/hooks.go index 1f23ef165..9fb8acc19 100644 --- a/x/dymns/keeper/hooks.go +++ b/x/dymns/keeper/hooks.go @@ -12,6 +12,7 @@ import ( "github.com/osmosis-labs/osmosis/v15/osmoutils" sdk "github.com/cosmos/cosmos-sdk/types" + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" ) @@ -58,7 +59,12 @@ func (h rollappHooks) RollappCreated(ctx sdk.Context, rollappID, alias string, c ), ) - return h.Keeper.registerAliasForRollApp(ctx, rollappID, creatorAddr, alias, aliasCost) + err := h.Keeper.registerAliasForRollApp(ctx, rollappID, creatorAddr, alias, aliasCost) + if err != nil { + return errorsmod.Wrap(errors.Join(gerrc.ErrInternal, err), "register alias for RollApp") + } + + return nil } func (h rollappHooks) BeforeUpdateState(_ sdk.Context, _ string, _ string, _ bool) error { diff --git a/x/incentives/module.go b/x/incentives/module.go index 6bb985073..a48c650d0 100644 --- a/x/incentives/module.go +++ b/x/incentives/module.go @@ -24,8 +24,8 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + dymsimtypes "github.com/dymensionxyz/dymension/v3/simulation/types" "github.com/dymensionxyz/dymension/v3/x/incentives/client/cli" "github.com/dymensionxyz/dymension/v3/x/incentives/keeper" "github.com/dymensionxyz/dymension/v3/x/incentives/types" @@ -111,14 +111,17 @@ type AppModule struct { keeper keeper.Keeper - accountKeeper stakingtypes.AccountKeeper - bankKeeper stakingtypes.BankKeeper + // simulation keepers + accountKeeper dymsimtypes.AccountKeeper + bankKeeper dymsimtypes.BankKeeper epochKeeper types.EpochKeeper } // NewAppModule creates a new AppModule struct. -func NewAppModule(keeper keeper.Keeper, - accountKeeper stakingtypes.AccountKeeper, bankKeeper stakingtypes.BankKeeper, +func NewAppModule( + keeper keeper.Keeper, + accountKeeper dymsimtypes.AccountKeeper, + bankKeeper dymsimtypes.BankKeeper, epochKeeper types.EpochKeeper, ) AppModule { return AppModule{ diff --git a/x/incentives/module_simulation.go b/x/incentives/module_simulation.go new file mode 100644 index 000000000..c5a4288a8 --- /dev/null +++ b/x/incentives/module_simulation.go @@ -0,0 +1,26 @@ +package incentives + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/dymensionxyz/dymension/v3/x/incentives/simulation" +) + +// ---------------------------------------------------------------------------- +// AppModuleSimulation +// ---------------------------------------------------------------------------- + +// GenerateGenesisState creates a randomized GenState of x/incentives. +func (AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// RegisterStoreDecoder registers a decoder for supply module's types. +func (AppModule) RegisterStoreDecoder(sdk.StoreDecoderRegistry) {} + +// WeightedOperations returns the all the module's operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.bankKeeper, am.epochKeeper, am.keeper) +} diff --git a/x/incentives/simulation/genesis.go b/x/incentives/simulation/genesis.go new file mode 100644 index 000000000..f8151e5d2 --- /dev/null +++ b/x/incentives/simulation/genesis.go @@ -0,0 +1,47 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + "time" + + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/types/simulation" + + commontypes "github.com/dymensionxyz/dymension/v3/x/common/types" + "github.com/dymensionxyz/dymension/v3/x/incentives/types" +) + +func getFee(r *rand.Rand) math.Int { + // use comparatively small numbers as the initial account balance is always bounded with max Int64 in simulation + w, _ := simulation.RandPositiveInt(r, commontypes.ADYM.MulRaw(100_000)) + return w +} + +// RandomizedGenState generates a random GenesisState for x/incentives. +func RandomizedGenState(simState *module.SimulationState) { + genesis := types.GenesisState{ + Params: types.Params{ + DistrEpochIdentifier: "day", + CreateGaugeBaseFee: getFee(simState.Rand), + AddToGaugeBaseFee: getFee(simState.Rand), + AddDenomFee: getFee(simState.Rand), + }, + LockableDurations: []time.Duration{ + time.Second, + time.Hour, + time.Hour * 3, + time.Hour * 7, + }, + } + + bz, err := json.MarshalIndent(genesis.Params, "", " ") + if err != nil { + panic(err) + } + fmt.Printf("Selected randomly generated incentives parameters:\n%s\n", bz) + + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&genesis) +} diff --git a/x/incentives/simulation/operations.go b/x/incentives/simulation/operations.go new file mode 100644 index 000000000..e4d692955 --- /dev/null +++ b/x/incentives/simulation/operations.go @@ -0,0 +1,218 @@ +package simulation + +import ( + "math/rand" + "time" + + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + dymsimtypes "github.com/dymensionxyz/dymension/v3/simulation/types" + "github.com/dymensionxyz/dymension/v3/x/incentives/keeper" + "github.com/dymensionxyz/dymension/v3/x/incentives/types" + lockuptypes "github.com/dymensionxyz/dymension/v3/x/lockup/types" +) + +// Simulation operation weights constants. +const ( + DefaultWeightMsgCreateGauge int = 100 + DefaultWeightMsgAddToGauge int = 100 + OpWeightMsgCreateGauge = "op_weight_msg_create_gauge" //nolint:gosec + OpWeightMsgAddToGauge = "op_weight_msg_add_to_gauge" //nolint:gosec +) + +// WeightedOperations returns all the operations from the module with their respective weights. +func WeightedOperations( + appParams simtypes.AppParams, + cdc codec.JSONCodec, + ak dymsimtypes.AccountKeeper, + bk dymsimtypes.BankKeeper, + ek types.EpochKeeper, + k keeper.Keeper, +) simulation.WeightedOperations { + var ( + weightMsgCreateGauge int + weightMsgAddToGauge int + ) + + interfaceRegistry := codectypes.NewInterfaceRegistry() + protoCdc := codec.NewProtoCodec(interfaceRegistry) + + appParams.GetOrGenerate( + cdc, OpWeightMsgCreateGauge, &weightMsgCreateGauge, nil, + func(*rand.Rand) { weightMsgCreateGauge = DefaultWeightMsgCreateGauge }, + ) + + appParams.GetOrGenerate( + cdc, OpWeightMsgAddToGauge, &weightMsgAddToGauge, nil, + func(*rand.Rand) { weightMsgAddToGauge = DefaultWeightMsgAddToGauge }, + ) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgCreateGauge, + SimulateMsgCreateGauge(protoCdc, ak, bk, ek, k), + ), + simulation.NewWeightedOperation( + weightMsgAddToGauge, + SimulateMsgAddToGauge(protoCdc, ak, bk, k), + ), + } +} + +// genRewardCoins generates a random number of coin denoms with a respective random value for each coin. +func genRewardCoins(r *rand.Rand, coins sdk.Coins, fee math.Int) (res sdk.Coins) { + numCoins := 1 + r.Intn(min(coins.Len(), 1)) + denomIndices := r.Perm(numCoins) + for i := 0; i < numCoins; i++ { + var ( + amt math.Int + err error + ) + denom := coins[denomIndices[i]].Denom + if denom == sdk.DefaultBondDenom { + amt, err = simtypes.RandPositiveInt(r, coins[i].Amount.Sub(fee)) + if err != nil { + panic(err) + } + } else { + amt, err = simtypes.RandPositiveInt(r, coins[i].Amount) + if err != nil { + panic(err) + } + } + res = append(res, sdk.Coin{Denom: denom, Amount: amt}) + } + return +} + +// genQueryCondition returns a single lockup QueryCondition, which is generated from a single coin randomly selected from the provided coin array +func genQueryCondition(r *rand.Rand, blocktime time.Time, coins sdk.Coins, durations []time.Duration) lockuptypes.QueryCondition { + lockQueryType := 0 + denom := coins[r.Intn(len(coins))].Denom + durationIndex := r.Intn(len(durations)) + duration := durations[durationIndex] + timestampSecs := r.Intn(1 * 60 * 60 * 24 * 7) // range of 1 week + timestamp := blocktime.Add(time.Duration(timestampSecs) * time.Second) + + return lockuptypes.QueryCondition{ + LockQueryType: lockuptypes.LockQueryType(lockQueryType), + Denom: denom, + Duration: duration, + Timestamp: timestamp, + } +} + +// SimulateMsgCreateGauge generates and executes a MsgCreateGauge with random parameters +func SimulateMsgCreateGauge( + cdc *codec.ProtoCodec, + ak dymsimtypes.AccountKeeper, + bk dymsimtypes.BankKeeper, + ek types.EpochKeeper, + 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) { + params := k.GetParams(ctx) + // we always expect that we add no more than 1 denom to the gauge in simulation + fee := params.CreateGaugeBaseFee.Add(params.AddDenomFee.MulRaw(1)) + feeCoin := sdk.Coin{Denom: sdk.DefaultBondDenom, Amount: fee} + + simAccount, _ := simtypes.RandomAcc(r, accs) + simCoins := bk.SpendableCoins(ctx, simAccount.Address) + if simCoins.AmountOf(sdk.DefaultBondDenom).LT(fee) { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgCreateGauge, "Account have no coin"), nil, nil + } + + distributeTo := genQueryCondition(r, ctx.BlockTime(), simCoins, types.DefaultGenesis().LockableDurations) + rewards := genRewardCoins(r, simCoins, fee) + startTimeSecs := r.Intn(1 * 60 * 60 * 24 * 7) // range of 1 week + startTime := ctx.BlockTime().Add(time.Duration(startTimeSecs) * time.Second) + numEpochsPaidOver := uint64(1) // == 1 since we only support perpetual gauges + + msg := &types.MsgCreateGauge{ + Owner: simAccount.Address.String(), + IsPerpetual: true, // all gauges are perpetual + DistributeTo: distributeTo, + Coins: rewards, + StartTime: startTime, + NumEpochsPaidOver: numEpochsPaidOver, + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: cdc, + Msg: msg, + MsgType: msg.Type(), + CoinsSpentInMsg: rewards.Add(feeCoin), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateMsgAddToGauge generates and executes a MsgAddToGauge with random parameters +func SimulateMsgAddToGauge( + cdc *codec.ProtoCodec, + ak dymsimtypes.AccountKeeper, + bk dymsimtypes.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + gauge := dymsimtypes.RandomGauge(ctx, r, k) + if gauge == nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddToGauge, "No gauge exists"), nil, nil + } else if gauge.IsFinishedGauge(ctx.BlockTime()) { + // TODO: Ideally we'd still run this but expect failure. + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddToGauge, "Selected a gauge that is finished"), nil, nil + } + + params := k.GetParams(ctx) + // we always expect that we add no more than 1 denom to the gauge in simulation + fee := params.AddToGaugeBaseFee.Add(params.AddDenomFee.MulRaw(int64(1 + len(gauge.Coins)))) + feeCoin := sdk.Coin{Denom: sdk.DefaultBondDenom, Amount: fee} + + simAccount, _ := simtypes.RandomAcc(r, accs) + simCoins := bk.SpendableCoins(ctx, simAccount.Address) + if simCoins.AmountOf(sdk.DefaultBondDenom).LT(fee) { + return simtypes.NoOpMsg( + types.ModuleName, types.TypeMsgAddToGauge, "Account have no coin"), nil, nil + } + + rewards := genRewardCoins(r, simCoins, fee) + msg := &types.MsgAddToGauge{ + Owner: simAccount.Address.String(), + GaugeId: gauge.Id, + Rewards: rewards, + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: cdc, + Msg: msg, + MsgType: msg.Type(), + CoinsSpentInMsg: rewards.Add(feeCoin), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/sponsorship/keeper/invariants.go b/x/sponsorship/keeper/invariants.go index b23ce11cd..631630af7 100644 --- a/x/sponsorship/keeper/invariants.go +++ b/x/sponsorship/keeper/invariants.go @@ -7,6 +7,7 @@ import ( "cosmossdk.io/collections" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/dymension/v3/utils/uinv" "github.com/dymensionxyz/dymension/v3/x/sponsorship/types" ) diff --git a/x/sponsorship/keeper/keeper.go b/x/sponsorship/keeper/keeper.go index a71f0d5d5..e5ffdf007 100644 --- a/x/sponsorship/keeper/keeper.go +++ b/x/sponsorship/keeper/keeper.go @@ -16,6 +16,7 @@ import ( type Keeper struct { authority string // authority is the x/gov module account + schema collections.Schema params collections.Item[types.Params] delegatorValidatorPower collections.Map[collections.Pair[sdk.AccAddress, sdk.ValAddress], math.Int] distribution collections.Item[types.Distribution] @@ -47,8 +48,9 @@ func NewKeeper( sb := collections.NewSchemaBuilder(collcompat.NewKVStoreService(storeKey)) - return Keeper{ + k := Keeper{ authority: authority, + schema: collections.Schema{}, // set later params: collections.NewItem( sb, types.ParamsPrefix(), @@ -82,4 +84,18 @@ func NewKeeper( incentivesKeeper: ik, sequencerKeeper: sqk, } + + // SchemaBuilder CANNOT be used after Build is called, + // so we build it after all collections are initialized + schema, err := sb.Build() + if err != nil { + panic(err) + } + k.schema = schema + + return k +} + +func (k Keeper) Schema() collections.Schema { + return k.schema } diff --git a/x/sponsorship/module.go b/x/sponsorship/module.go index 5bd8bf79f..8453fdd54 100644 --- a/x/sponsorship/module.go +++ b/x/sponsorship/module.go @@ -16,6 +16,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + dymsimtypes "github.com/dymensionxyz/dymension/v3/simulation/types" "github.com/dymensionxyz/dymension/v3/x/sponsorship/client/cli" "github.com/dymensionxyz/dymension/v3/x/sponsorship/keeper" "github.com/dymensionxyz/dymension/v3/x/sponsorship/types" @@ -102,13 +103,29 @@ type AppModule struct { AppModuleBasic keeper keeper.Keeper + + // simulation keepers + accountKeeper dymsimtypes.AccountKeeper + bankKeeper dymsimtypes.BankKeeper + incentivesKeeper dymsimtypes.IncentivesKeeper + stakingKeeper dymsimtypes.StakingKeeper } // NewAppModule creates a new AppModule struct. -func NewAppModule(keeper keeper.Keeper) AppModule { +func NewAppModule( + keeper keeper.Keeper, + accountKeeper dymsimtypes.AccountKeeper, + bankKeeper dymsimtypes.BankKeeper, + incentivesKeeper dymsimtypes.IncentivesKeeper, + stakingKeeper dymsimtypes.StakingKeeper, +) AppModule { return AppModule{ - AppModuleBasic: NewAppModuleBasic(), - keeper: keeper, + AppModuleBasic: NewAppModuleBasic(), + keeper: keeper, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + incentivesKeeper: incentivesKeeper, + stakingKeeper: stakingKeeper, } } diff --git a/x/sponsorship/module_simulation.go b/x/sponsorship/module_simulation.go new file mode 100644 index 000000000..6a4315aea --- /dev/null +++ b/x/sponsorship/module_simulation.go @@ -0,0 +1,30 @@ +package sponsorship + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/dymensionxyz/dymension/v3/internal/collcompat" + "github.com/dymensionxyz/dymension/v3/x/sponsorship/simulation" + "github.com/dymensionxyz/dymension/v3/x/sponsorship/types" +) + +// ---------------------------------------------------------------------------- +// AppModuleSimulation +// ---------------------------------------------------------------------------- + +// GenerateGenesisState creates a randomized GenState of x/incentives. +func (AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// RegisterStoreDecoder registers a decoder for supply module's types. +func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[types.ModuleName] = collcompat.NewStoreDecoderFuncFromCollectionsSchema(am.keeper.Schema()) +} + +// WeightedOperations returns the all the module's operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.bankKeeper, am.incentivesKeeper, am.stakingKeeper, am.keeper) +} diff --git a/x/sponsorship/simulation/genesis.go b/x/sponsorship/simulation/genesis.go new file mode 100644 index 000000000..ebb805ecc --- /dev/null +++ b/x/sponsorship/simulation/genesis.go @@ -0,0 +1,44 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/types/simulation" + + commontypes "github.com/dymensionxyz/dymension/v3/x/common/types" + "github.com/dymensionxyz/dymension/v3/x/sponsorship/types" +) + +func getMinAllocationWeight(r *rand.Rand) math.Int { + w, _ := simulation.RandPositiveInt(r, types.DefaultMinAllocationWeight) + return w +} + +func getMinVotingPower(r *rand.Rand) math.Int { + // use comparatively small numbers as the initial account balance is always bounded with max Int64 in simulation + w, _ := simulation.RandPositiveInt(r, commontypes.ADYM.MulRaw(100_000)) + return w +} + +// RandomizedGenState generates a random GenesisState for staking +func RandomizedGenState(simState *module.SimulationState) { + genesis := &types.GenesisState{ + Params: types.Params{ + MinAllocationWeight: getMinAllocationWeight(simState.Rand), + MinVotingPower: getMinVotingPower(simState.Rand), + }, + VoterInfos: nil, + } + + bz, err := json.MarshalIndent(genesis.Params, "", " ") + if err != nil { + panic(err) + } + fmt.Printf("Selected randomly generated sponsorship parameters:\n%s\n", bz) + + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) +} diff --git a/x/sponsorship/simulation/operations.go b/x/sponsorship/simulation/operations.go new file mode 100644 index 000000000..bf31c86b5 --- /dev/null +++ b/x/sponsorship/simulation/operations.go @@ -0,0 +1,153 @@ +package simulation + +import ( + "math/rand" + + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + dymsimtypes "github.com/dymensionxyz/dymension/v3/simulation/types" + "github.com/dymensionxyz/dymension/v3/x/sponsorship/keeper" + "github.com/dymensionxyz/dymension/v3/x/sponsorship/types" +) + +// Simulation operation weights constants. +const ( + DefaultWeightMsgVote int = 100 + DefaultWeightMsgRevokeVote int = 100 + OpWeightMsgVote = "op_weight_msg_vote" //nolint:gosec + OpWeightMsgRevokeVote = "op_weight_msg_revoke_vote" //nolint:gosec +) + +// WeightedOperations returns all the operations from the module with their respective weights. +func WeightedOperations( + appParams simtypes.AppParams, + cdc codec.JSONCodec, + ak dymsimtypes.AccountKeeper, + bk dymsimtypes.BankKeeper, + ik dymsimtypes.IncentivesKeeper, + sk dymsimtypes.StakingKeeper, + s keeper.Keeper, +) simulation.WeightedOperations { + var weightMsgVote int + + appParams.GetOrGenerate( + cdc, OpWeightMsgVote, &weightMsgVote, nil, + func(*rand.Rand) { weightMsgVote = DefaultWeightMsgVote }, + ) + + protoCdc := codec.NewProtoCodec(codectypes.NewInterfaceRegistry()) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgVote, + SimulateMsgVote(protoCdc, ak, bk, ik, sk, s), + ), + } +} + +// getAllocationWeight returns a random allocation weight in range [minAllocationWeight; MaxAllocationWeight]. +func getAllocationWeight(r *rand.Rand, minAllocationWeight math.Int) math.Int { + w, _ := dymsimtypes.RandIntBetween(r, minAllocationWeight, types.MaxAllocationWeight.AddRaw(1)) + return w +} + +// SimulateMsgVote generates and executes a MsgVote with random parameters +func SimulateMsgVote( + cdc *codec.ProtoCodec, + ak dymsimtypes.AccountKeeper, + bk dymsimtypes.BankKeeper, + ik dymsimtypes.IncentivesKeeper, + sk dymsimtypes.StakingKeeper, + 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) { + params, _ := k.GetParams(ctx) + + delegation := dymsimtypes.RandomDelegation(ctx, r, sk) + if delegation == nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgVote, "No delegation available"), nil, nil + } + + b, err := k.GetValidatorBreakdown(ctx, delegation.GetDelegatorAddr()) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgVote, "Failed to get validator breakdown"), nil, err + } + + if b.TotalPower.LT(params.MinVotingPower) { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgVote, "Address does not have enough staking power to vote"), nil, nil + } + + // Get a random subset of gauges + selectedGauges := dymsimtypes.RandomGaugeSubset(ctx, r, ik) + if len(selectedGauges) == 0 { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgVote, "No gauges available"), nil, nil + } + + // Generate random weights for the selected gauges. + // The sum of the weights should be less than or equal to 100 DYM (100%). + totalWeight := math.ZeroInt() + var gaugeWeights []types.GaugeWeight + for _, gauge := range selectedGauges { + weight := getAllocationWeight(r, params.MinAllocationWeight) + if totalWeight.Add(weight).GT(types.MaxAllocationWeight) { + weight = types.MaxAllocationWeight.Sub(totalWeight) + } + + if weight.LT(params.MinAllocationWeight) { + // We don't have any more weight to distribute. + // The remaining weight is abstained. + break + } + + gaugeWeights = append(gaugeWeights, types.GaugeWeight{ + GaugeId: gauge.Id, + Weight: weight, + }) + + totalWeight = totalWeight.Add(weight) + } + + msg := &types.MsgVote{ + Voter: delegation.GetDelegatorAddr().String(), + Weights: gaugeWeights, + } + + // Need to retrieve the simulation account associated with delegation to retrieve PrivKey + var simAccount simtypes.Account + + for _, simAcc := range accs { + if simAcc.Address.Equals(delegation.GetDelegatorAddr()) { + simAccount = simAcc + break + } + } + // If simaccount.PrivKey == nil, delegation address does not exist in accs. However, since smart contracts and module accounts can stake, we can ignore the error. + if simAccount.PrivKey == nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "Voter account private key is nil"), nil, nil + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: cdc, + Msg: msg, + MsgType: msg.Type(), + CoinsSpentInMsg: nil, + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/streamer/keeper/hooks.go b/x/streamer/keeper/hooks.go index 0787fe974..ee4f3118b 100644 --- a/x/streamer/keeper/hooks.go +++ b/x/streamer/keeper/hooks.go @@ -161,7 +161,7 @@ func (h Hooks) RollappCreated(ctx sdk.Context, rollappID, _ string, _ sdk.AccAdd err := h.k.CreateRollappGauge(ctx, rollappID) if err != nil { ctx.Logger().Error("Failed to create rollapp gauge", "error", err) - return err + return fmt.Errorf("create rollapp gauge: %w", err) } return nil }