diff --git a/.github/workflows/simulation.yml b/.github/workflows/simulation.yml new file mode 100644 index 0000000000..72c3a70b2c --- /dev/null +++ b/.github/workflows/simulation.yml @@ -0,0 +1,44 @@ +name: Simulation +on: + workflow_call: + pull_request: + merge_group: + push: + branches: + - main + - release/v* + - feat/* + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }}-tests + cancel-in-progress: true + +jobs: + simulation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + check-latest: true + cache: true + cache-dependency-path: go.sum + - uses: technote-space/get-diff-action@v6.1.2 + id: git_diff + with: + PATTERNS: | + **/*.go + go.mod + go.sum + **/go.mod + **/go.sum + **/Makefile + Makefile + - name: simulation test + if: env.GIT_DIFF + run: | + make sim-full \ No newline at end of file diff --git a/Makefile b/Makefile index e68df6fd63..c00d2fcb23 100644 --- a/Makefile +++ b/Makefile @@ -145,6 +145,13 @@ verify-models: ../run_invariants.sh +############################################################################### +### Simulation tests ### + +# Run a full simulation test +sim-full: + cd app/provider;\ + go test -mod=readonly -run=^TestFullAppSimulation$ -Enabled=true -NumBlocks=500 -BlockSize=200 -Commit=true -timeout 24h github.com/cosmos/interchain-security/v5/app/provider -v ############################################################################### ### Linting ### diff --git a/app/provider/app.go b/app/provider/app.go index 405106a188..09e4f279ec 100644 --- a/app/provider/app.go +++ b/app/provider/app.go @@ -66,6 +66,7 @@ import ( authcodec "github.com/cosmos/cosmos-sdk/x/auth/codec" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" "github.com/cosmos/cosmos-sdk/x/auth/posthandler" + authsims "github.com/cosmos/cosmos-sdk/x/auth/simulation" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" txmodule "github.com/cosmos/cosmos-sdk/x/auth/tx/config" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -352,9 +353,7 @@ func New( // Remove the ConsumerRewardsPool from the group of blocked recipient addresses in bank // this is required for the provider chain to be able to receive tokens from // the consumer chain - bankBlockedAddrs := app.ModuleAccountAddrs() - delete(bankBlockedAddrs, authtypes.NewModuleAddress( - providertypes.ConsumerRewardsPool).String()) + bankBlockedAddrs := BankBlockedAddrs(app) app.BankKeeper = bankkeeper.NewBaseKeeper( appCodec, @@ -678,6 +677,15 @@ func New( panic(err) } + // create the simulation manager and define the order of the modules for deterministic 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) + + // register the store decoders for simulation tests + app.sm.RegisterStoreDecoders() + // Note this upgrade handler is just an example and may not be exactly what you need to implement. // See https://docs.cosmos.network/v0.45/building-modules/upgrade.html app.UpgradeKeeper.SetUpgradeHandler( @@ -778,6 +786,13 @@ func New( return app } +func BankBlockedAddrs(app *App) map[string]bool { + bankBlockedAddrs := app.ModuleAccountAddrs() + delete(bankBlockedAddrs, authtypes.NewModuleAddress( + providertypes.ConsumerRewardsPool).String()) + return bankBlockedAddrs +} + // Name returns the name of the App func (app *App) Name() string { return app.BaseApp.Name() } diff --git a/app/provider/sim_test.go b/app/provider/sim_test.go new file mode 100644 index 0000000000..e9d23807c7 --- /dev/null +++ b/app/provider/sim_test.go @@ -0,0 +1,85 @@ +package app_test + +import ( + "os" + "testing" + + "cosmossdk.io/store" + "github.com/stretchr/testify/require" + + "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/x/simulation" + simcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" + + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + providerapp "github.com/cosmos/interchain-security/v5/app/provider" +) + +func init() { + simcli.GetSimulatorFlags() +} + +// interBlockCacheOpt returns a BaseApp option function that sets the persistent +// inter-block write-through cache. +func interBlockCacheOpt() func(*baseapp.BaseApp) { + return baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager()) +} + +// fauxMerkleModeOpt returns a BaseApp option to use a dbStoreAdapter instead of +// an IAVLStore for faster simulation speed. +func fauxMerkleModeOpt(bapp *baseapp.BaseApp) { + bapp.SetFauxMerkleMode() +} + +func TestFullAppSimulation(t *testing.T) { + config := simcli.NewConfigFromFlags() + config.ChainID = "provi" + + 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] = providerapp.DefaultNodeHome + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + + app := providerapp.New(logger, db, nil, true, appOptions, fauxMerkleModeOpt, interBlockCacheOpt(), baseapp.SetChainID("provi")) + require.Equal(t, "interchain-security-p", app.Name()) + + encoding := providerapp.MakeTestEncodingConfig() + + genesisState := providerapp.NewDefaultGenesisState(encoding.Codec) + + // run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simtestutil.AppStateFn(encoding.Codec, app.SimulationManager(), genesisState), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simtestutil.SimulationOperations(app, app.AppCodec(), config), + providerapp.BankBlockedAddrs(app), + config, + app.AppCodec(), + ) + + // export state and simParams before the simulation error is checked + err = simtestutil.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simtestutil.PrintStats(db) + } +} diff --git a/x/ccv/provider/keeper/invariants.go b/x/ccv/provider/keeper/invariants.go new file mode 100644 index 0000000000..ca73ffad25 --- /dev/null +++ b/x/ccv/provider/keeper/invariants.go @@ -0,0 +1,32 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + types "github.com/cosmos/interchain-security/v5/x/ccv/provider/types" +) + +// RegisterInvariants registers all staking invariants +func RegisterInvariants(ir sdk.InvariantRegistry, k *Keeper) { + ir.RegisterRoute(types.ModuleName, "max-provider-validators", + MaxProviderConsensusValidatorsInvariant(k)) +} + +// MaxProviderConsensusValidatorsInvariant checks that the number of provider consensus validators +// is less than or equal to the maximum number of provider consensus validators +func MaxProviderConsensusValidatorsInvariant(k *Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + params := k.GetParams(ctx) + maxProviderConsensusValidators := params.MaxProviderConsensusValidators + + consensusValidators := k.GetLastProviderConsensusValSet(ctx) + if int64(len(consensusValidators)) > maxProviderConsensusValidators { + return sdk.FormatInvariant(types.ModuleName, "max-provider-validators", + fmt.Sprintf("number of provider consensus validators: %d, exceeds max: %d", + len(consensusValidators), maxProviderConsensusValidators)), true + } + + return "", false + } +} diff --git a/x/ccv/provider/module.go b/x/ccv/provider/module.go index e6de7c8ab9..66a6ee28f6 100644 --- a/x/ccv/provider/module.go +++ b/x/ccv/provider/module.go @@ -23,6 +23,7 @@ import ( "github.com/cosmos/interchain-security/v5/x/ccv/provider/client/cli" "github.com/cosmos/interchain-security/v5/x/ccv/provider/keeper" "github.com/cosmos/interchain-security/v5/x/ccv/provider/migrations" + "github.com/cosmos/interchain-security/v5/x/ccv/provider/simulation" providertypes "github.com/cosmos/interchain-security/v5/x/ccv/provider/types" ) @@ -115,8 +116,8 @@ func NewAppModule(k *keeper.Keeper, paramSpace paramtypes.Subspace) AppModule { } // RegisterInvariants implements the AppModule interface -func (AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { - // TODO +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { + keeper.RegisterInvariants(ir, am.keeper) } // RegisterServices registers module services. @@ -194,6 +195,7 @@ func (am AppModule) EndBlock(ctx context.Context) ([]abci.ValidatorUpdate, error // GenerateGenesisState creates a randomized GenState of the transfer module. func (AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) } // RegisterStoreDecoder registers a decoder for provider module's types diff --git a/x/ccv/provider/simulation/genesis.go b/x/ccv/provider/simulation/genesis.go new file mode 100644 index 0000000000..f27573cc7f --- /dev/null +++ b/x/ccv/provider/simulation/genesis.go @@ -0,0 +1,44 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/interchain-security/v5/x/ccv/provider/types" +) + +// Simulation parameter constants +const ( + // only includes params that make sense even with a single + maxProviderConsensusValidators = "max_provider_consensus_validators" +) + +// genMaxProviderConsensusValidators returns randomized maxProviderConsensusValidators +func genMaxProviderConsensusValidators(r *rand.Rand) int64 { + return int64(r.Intn(250) + 1) +} + +// RandomizedGenState generates a random GenesisState for staking +func RandomizedGenState(simState *module.SimulationState) { + // params + var ( + maxProviderConsensusVals int64 + ) + + simState.AppParams.GetOrGenerate(maxProviderConsensusValidators, &maxProviderConsensusVals, simState.Rand, func(r *rand.Rand) { maxProviderConsensusVals = genMaxProviderConsensusValidators(r) }) + + providerParams := types.DefaultParams() + providerParams.MaxProviderConsensusValidators = maxProviderConsensusVals + + providerGenesis := types.DefaultGenesisState() + providerGenesis.Params = providerParams + + bz, err := json.MarshalIndent(&providerGenesis.Params, "", " ") + if err != nil { + panic(err) + } + fmt.Printf("Selected randomly generated provider parameters:\n%s\n", bz) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(providerGenesis) +}