# Module Simulation

# Prerequisites

# Synopsis

This document details how to define each module simulation functions to be integrated with the application SimulationManager.

# Simulation package

Every module that implements the Cosmos SDK simulator needs to have a x/<module>/simulation package which contains the primary functions required by the fuzz tests: store decoders, randomized genesis state and parameters, weighted operations and proposal contents.

# Store decoders

Registering the store decoders is required for the AppImportExport. This allows for the key-value pairs from the stores to be decoded (i.e unmarshalled) to their corresponding types. In particular, it matches the key to a concrete type and then unmarshals the value from the KVPair to the type provided.

You can use the example here (opens new window) from the distribution module to implement your store decoders.

# Randomized genesis

The simulator tests different scenarios and values for genesis parameters in order to fully test the edge cases of specific modules. The simulator package from each module must expose a RandomizedGenState function to generate the initial random GenesisState from a given seed.

Once the module genesis parameter are generated randomly (or with the key and values defined in a params file), they are marshaled to JSON format and added to the app genesis JSON to use it on the simulations.

You can check an example on how to create the randomized genesis here (opens new window).

# Randomized parameter changes

The simulator is able to test parameter changes at random. The simulator package from each module must contain a RandomizedParams func that will simulate parameter changes of the module throughout the simulations lifespan.

You can see how an example of what is needed to fully test parameter changes here (opens new window)

# Random weighted operations

Operations are one of the crucial parts of the Cosmos SDK simulation. They are the transactions (Msg) that are simulated with random field values. The sender of the operation is also assigned randomly.

Operations on the simulation are simulated using the full transaction cycle of a ABCI application that exposes the BaseApp.

Shown below is how weights are set:

Copy // Simulation operation weights constants const ( OpWeightMsgCreateValidator = "op_weight_msg_create_validator" OpWeightMsgEditValidator = "op_weight_msg_edit_validator" OpWeightMsgDelegate = "op_weight_msg_delegate" OpWeightMsgUndelegate = "op_weight_msg_undelegate" OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate" OpWeightMsgCancelUnbondingDelegation = "op_weight_msg_cancel_unbonding_delegation" ) // WeightedOperations returns all the operations from the module with their respective weights func WeightedOperations( appParams simtypes.AppParams, cdc codec.JSONCodec, ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, ) simulation.WeightedOperations { var ( weightMsgCreateValidator int weightMsgEditValidator int weightMsgDelegate int weightMsgUndelegate int weightMsgBeginRedelegate int weightMsgCancelUnbondingDelegation int ) appParams.GetOrGenerate(cdc, OpWeightMsgCreateValidator, &weightMsgCreateValidator, nil, func(_ *rand.Rand) { weightMsgCreateValidator = simappparams.DefaultWeightMsgCreateValidator }, ) appParams.GetOrGenerate(cdc, OpWeightMsgEditValidator, &weightMsgEditValidator, nil, func(_ *rand.Rand) { weightMsgEditValidator = simappparams.DefaultWeightMsgEditValidator }, ) appParams.GetOrGenerate(cdc, OpWeightMsgDelegate, &weightMsgDelegate, nil, func(_ *rand.Rand) { weightMsgDelegate = simappparams.DefaultWeightMsgDelegate }, ) appParams.GetOrGenerate(cdc, OpWeightMsgUndelegate, &weightMsgUndelegate, nil, func(_ *rand.Rand) { weightMsgUndelegate = simappparams.DefaultWeightMsgUndelegate }, ) appParams.GetOrGenerate(cdc, OpWeightMsgBeginRedelegate, &weightMsgBeginRedelegate, nil, func(_ *rand.Rand) { weightMsgBeginRedelegate = simappparams.DefaultWeightMsgBeginRedelegate }, ) appParams.GetOrGenerate(cdc, OpWeightMsgCancelUnbondingDelegation, &weightMsgCancelUnbondingDelegation, nil, func(_ *rand.Rand) { weightMsgCancelUnbondingDelegation = simappparams.DefaultWeightMsgCancelUnbondingDelegation }, )

As you can see, the weights are predefined in this case. Options exist to override this behavior with different weights. One option is to use *rand.Rand to define a random weight for the operation, or you can inject your own predefined weights.

Here is how one can override the above package simappparams.

Copy @echo "Running non-determinism test..." @go test -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \ -NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h test-sim-custom-genesis-fast: @echo "Running custom genesis simulation..." @echo "By default, ${HOME}/.gaia/config/genesis.json will be used." @go test -mod=readonly $(SIMAPP) -run TestFullAppSimulation -Genesis=${HOME}/.gaia/config/genesis.json \ -Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Seed=99 -Period=5 -v -timeout 24h test-sim-import-export: runsim @echo "Running application import/export simulation. This may take several minutes..." @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 50 5 TestAppImportExport

For the last test a tool called runsim is used, this is used to parallelize go test instances, provide info to Github and slack integrations to provide information to your team on how the simulations are running.

# Random proposal contents

Randomized governance proposals are also supported on the Cosmos SDK simulator. Each module must define the governance proposal Contents that they expose and register them to be used on the parameters.

# Registering simulation functions

Now that all the required functions are defined, we need to integrate them into the module pattern within the module.go:

Copy package distribution import ( "context" "encoding/json" "fmt" "math/rand" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" abci "github.com/tendermint/tendermint/abci/types" sdkclient "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" "github.com/cosmos/cosmos-sdk/x/distribution/client/cli" "github.com/cosmos/cosmos-sdk/x/distribution/keeper" "github.com/cosmos/cosmos-sdk/x/distribution/simulation" "github.com/cosmos/cosmos-sdk/x/distribution/types" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" ) var ( _ module.AppModule = AppModule{} _ module.AppModuleBasic = AppModuleBasic{} _ module.AppModuleSimulation = AppModule{} ) // AppModuleBasic defines the basic application module used by the distribution module. type AppModuleBasic struct { cdc codec.Codec } // Name returns the distribution module's name. func (AppModuleBasic) Name() string { return types.ModuleName } // RegisterLegacyAminoCodec registers the distribution module's types for the given codec. func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { types.RegisterLegacyAminoCodec(cdc) } // DefaultGenesis returns default genesis state as raw bytes for the distribution // module. func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { return cdc.MustMarshalJSON(types.DefaultGenesisState()) } // ValidateGenesis performs genesis state validation for the distribution module. func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config sdkclient.TxEncodingConfig, bz json.RawMessage) error { var data types.GenesisState if err := cdc.UnmarshalJSON(bz, &data); err != nil { return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) } return types.ValidateGenesis(&data) } // RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the distribution module. func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx sdkclient.Context, mux *runtime.ServeMux) { if err := types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)); err != nil { panic(err) } } // GetTxCmd returns the root tx command for the distribution module. func (AppModuleBasic) GetTxCmd() *cobra.Command { return cli.NewTxCmd() } // GetQueryCmd returns the root query command for the distribution module. func (AppModuleBasic) GetQueryCmd() *cobra.Command { return cli.GetQueryCmd() } // RegisterInterfaces implements InterfaceModule func (b AppModuleBasic) RegisterInterfaces(registry cdctypes.InterfaceRegistry) { types.RegisterInterfaces(registry) } // AppModule implements an application module for the distribution module. type AppModule struct { AppModuleBasic keeper keeper.Keeper accountKeeper types.AccountKeeper bankKeeper types.BankKeeper stakingKeeper stakingkeeper.Keeper } // NewAppModule creates a new AppModule object func NewAppModule( cdc codec.Codec, keeper keeper.Keeper, accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper, stakingKeeper stakingkeeper.Keeper, ) AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{cdc: cdc}, keeper: keeper, accountKeeper: accountKeeper, bankKeeper: bankKeeper, stakingKeeper: stakingKeeper, } } // Name returns the distribution module's name. func (AppModule) Name() string { return types.ModuleName } // RegisterInvariants registers the distribution module invariants. func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { keeper.RegisterInvariants(ir, am.keeper) } // Deprecated: Route returns the message routing key for the distribution module. func (am AppModule) Route() sdk.Route { return sdk.Route{} } // QuerierRoute returns the distribution module's querier route name. func (AppModule) QuerierRoute() string { return types.QuerierRoute } // LegacyQuerierHandler returns the distribution module sdk.Querier. func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sdk.Querier { return keeper.NewQuerier(am.keeper, legacyQuerierCdc) } // RegisterServices registers module services. func (am AppModule) RegisterServices(cfg module.Configurator) { types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) types.RegisterQueryServer(cfg.QueryServer(), am.keeper) m := keeper.NewMigrator(am.keeper) cfg.RegisterMigration(types.ModuleName, 1, m.Migrate1to2) } // InitGenesis performs genesis initialization for the distribution module. It returns // no validator updates. func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { var genesisState types.GenesisState cdc.MustUnmarshalJSON(data, &genesisState) am.keeper.InitGenesis(ctx, genesisState) return []abci.ValidatorUpdate{} } // ExportGenesis returns the exported genesis state as raw bytes for the distribution // module. func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { gs := am.keeper.ExportGenesis(ctx) return cdc.MustMarshalJSON(gs) } // ConsensusVersion implements AppModule/ConsensusVersion. func (AppModule) ConsensusVersion() uint64 { return 2 } // BeginBlock returns the begin blocker for the distribution module. func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { BeginBlocker(ctx, req, am.keeper) } // EndBlock returns the end blocker for the distribution module. It returns no validator // updates. func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { return []abci.ValidatorUpdate{} } // AppModuleSimulation functions // GenerateGenesisState creates a randomized GenState of the distribution module. func (AppModule) GenerateGenesisState(simState *module.SimulationState) { simulation.RandomizedGenState(simState) } // ProposalContents returns all the distribution content functions used to // simulate governance proposals. func (am AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent { return simulation.ProposalContents(am.keeper) } // RandomizedParams creates randomized distribution param changes for the simulator. func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange { return simulation.ParamChanges(r) } // RegisterStoreDecoder registers a decoder for distribution module's types func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) } // WeightedOperations returns the all the gov module 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.keeper, am.stakingKeeper, ) }

# App Simulator manager

The following step is setting up the SimulatorManager at the app level. This is required for the simulation test files on the next step.

Copy type CustomApp struct { ... sm *module.SimulationManager }

Then at the instantiation of the application, we create the SimulationManager instance in the same way we create the ModuleManager but this time we only pass the modules that implement the simulation functions from the AppModuleSimulation interface described above.

Copy func NewCustomApp(...) { // create the simulation manager and define the order of the modules for deterministic simulations app.sm = module.NewSimulationManager( auth.NewAppModule(app.accountKeeper), bank.NewAppModule(app.bankKeeper, app.accountKeeper), supply.NewAppModule(app.supplyKeeper, app.accountKeeper), ov.NewAppModule(app.govKeeper, app.accountKeeper, app.supplyKeeper), mint.NewAppModule(app.mintKeeper), distr.NewAppModule(app.distrKeeper, app.accountKeeper, app.supplyKeeper, app.stakingKeeper), staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper), slashing.NewAppModule(app.slashingKeeper, app.accountKeeper, app.stakingKeeper), ) // register the store decoders for simulation tests app.sm.RegisterStoreDecoders() ... }