diff --git a/.github/workflows/sims.yml b/.github/workflows/sims.yml new file mode 100644 index 0000000..6a776f9 --- /dev/null +++ b/.github/workflows/sims.yml @@ -0,0 +1,149 @@ +name: Sims +# Sims workflow runs multiple types of simulations (nondeterminism, import-export, after-import, multi-seed-short) +# This workflow will run on all Pull Requests, if a .go, .mod or .sum file have been changed +on: + pull_request: + push: + branches: + - main + +concurrency: + group: ci-${{ github.ref }}-sims + cancel-in-progress: true + +jobs: + build: + permissions: + contents: read # for actions/checkout to fetch code + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'skip-sims')" + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: "1.21" + check-latest: true + - run: make -C simapp build + - name: Install runsim + run: go install github.com/cosmos/tools/cmd/runsim@v1.0.0 + - uses: actions/cache@v3 + with: + path: ~/go/bin + key: ${{ runner.os }}-go-runsim-binary + + test-sim-nondeterminism: + runs-on: ubuntu-latest + needs: [build] + timeout-minutes: 60 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: "1.21" + check-latest: true + - uses: actions/cache@v3 + with: + path: ~/go/bin + key: ${{ runner.os }}-go-runsim-binary + - name: test-sim-import-export + run: | + make test-sim-nondeterminism + + test-sim-import-export: + runs-on: ubuntu-latest + needs: [build] + timeout-minutes: 60 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: "1.21" + check-latest: true + - uses: actions/cache@v3 + with: + path: ~/go/bin + key: ${{ runner.os }}-go-runsim-binary + - name: test-sim-import-export + run: | + make test-sim-import-export + + test-sim-after-import: + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: "1.21" + check-latest: true + - uses: actions/cache@v3 + with: + path: ~/go/bin + key: ${{ runner.os }}-go-runsim-binary + - name: test-sim-after-import + run: | + make test-sim-after-import + + test-sim-multi-seed-short: + runs-on: ubuntu-latest + needs: [build] + timeout-minutes: 60 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: "1.21" + check-latest: true + - uses: actions/cache@v3 + with: + path: ~/go/bin + key: ${{ runner.os }}-go-runsim-binary + - name: test-sim-multi-seed-short + run: | + make test-sim-multi-seed-short + + sims-notify-success: + needs: + [test-sim-multi-seed-short, test-sim-after-import, test-sim-import-export] + runs-on: ubuntu-latest + if: ${{ success() }} + steps: + - name: Check out repository + uses: actions/checkout@v3 + - name: Get previous workflow status + uses: ./.github/actions/last-workflow-status + id: last_status + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Notify Slack on success + if: ${{ steps.last_status.outputs.last_status == 'failure' }} + uses: rtCamp/action-slack-notify@v2.2.0 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_CHANNEL: sdk-sims + SLACK_USERNAME: Sim Tests + SLACK_ICON_EMOJI: ":white_check_mark:" + SLACK_COLOR: good + SLACK_MESSAGE: Sims are passing + SLACK_FOOTER: "" + + sims-notify-failure: + permissions: + contents: none + needs: + [test-sim-multi-seed-short, test-sim-after-import, test-sim-import-export] + runs-on: ubuntu-latest + if: ${{ failure() }} + steps: + - name: Notify Slack on failure + uses: rtCamp/action-slack-notify@v2.2.0 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_CHANNEL: sdk-sims + SLACK_USERNAME: Sim Tests + SLACK_ICON_EMOJI: ":skull:" + SLACK_COLOR: danger + SLACK_MESSAGE: Sims are failing + SLACK_FOOTER: "" + \ No newline at end of file diff --git a/Makefile b/Makefile index 28871be..a66ba5c 100644 --- a/Makefile +++ b/Makefile @@ -162,33 +162,33 @@ test-sim-nondeterminism: test-sim-custom-genesis-fast: @echo "Running custom genesis simulation..." @echo "By default, ${HOME}/.cada/config/genesis.json will be used." - @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -run TestFullAppSimulation -Genesis=${HOME}/.cada/config/genesis.json \ + @cd ${CURRENT_DIR}/simapp/app && go test -mod=readonly -run TestFullAppSimulation -Genesis=${HOME}/.cada/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..." - @cd ${CURRENT_DIR}/simapp && $(BINDIR)/runsim -Jobs=4 -SimAppPkg=. -ExitOnFail 50 5 TestAppImportExport + @cd ${CURRENT_DIR}/simapp/app && $(BINDIR)/runsim -Jobs=4 -SimAppPkg=. -ExitOnFail 50 5 TestAppImportExport test-sim-after-import: runsim @echo "Running application simulation-after-import. This may take several minutes..." - @cd ${CURRENT_DIR}/simapp && $(BINDIR)/runsim -Jobs=4 -SimAppPkg=. -ExitOnFail 50 5 TestAppSimulationAfterImport + @cd ${CURRENT_DIR}/simapp/app && $(BINDIR)/runsim -Jobs=4 -SimAppPkg=. -ExitOnFail 50 5 TestAppSimulationAfterImport test-sim-custom-genesis-multi-seed: runsim @echo "Running multi-seed custom genesis simulation..." @echo "By default, ${HOME}/.cada/config/genesis.json will be used." - @cd ${CURRENT_DIR}/simapp && $(BINDIR)/runsim -Genesis=${HOME}/.cada/config/genesis.json -SimAppPkg=. -ExitOnFail 400 5 TestFullAppSimulation + @cd ${CURRENT_DIR}/simapp/app && $(BINDIR)/runsim -Genesis=${HOME}/.cada/config/genesis.json -SimAppPkg=. -ExitOnFail 400 5 TestFullAppSimulation test-sim-multi-seed-long: runsim @echo "Running long multi-seed application simulation. This may take awhile!" - @cd ${CURRENT_DIR}/simapp && $(BINDIR)/runsim -Jobs=4 -SimAppPkg=. -ExitOnFail 500 50 TestFullAppSimulation + @cd ${CURRENT_DIR}/simapp/app && $(BINDIR)/runsim -Jobs=4 -SimAppPkg=. -ExitOnFail 500 50 TestFullAppSimulation test-sim-multi-seed-short: runsim @echo "Running short multi-seed application simulation. This may take awhile!" - @cd ${CURRENT_DIR}/simapp && $(BINDIR)/runsim -Jobs=4 -SimAppPkg=. -ExitOnFail 50 10 TestFullAppSimulation + @cd ${CURRENT_DIR}/simapp/app && $(BINDIR)/runsim -Jobs=4 -SimAppPkg=. -ExitOnFail 50 10 TestFullAppSimulation test-sim-benchmark-invariants: @echo "Running simulation invariant benchmarks..." - cd ${CURRENT_DIR}/simapp && @go test -mod=readonly -benchmem -bench=BenchmarkInvariants -run=^$ \ + cd ${CURRENT_DIR}/simapp/app && @go test -mod=readonly -benchmem -bench=BenchmarkInvariants -run=^$ \ -Enabled=true -NumBlocks=1000 -BlockSize=200 \ -Period=1 -Commit=true -Seed=57 -v -timeout 24h diff --git a/simapp/app/sim_bench_test.go b/simapp/app/sim_bench_test.go new file mode 100644 index 0000000..66033db --- /dev/null +++ b/simapp/app/sim_bench_test.go @@ -0,0 +1,150 @@ +package app + +import ( + "fmt" + "os" + "testing" + + "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" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + simcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" +) + +// Profile with: +// /usr/local/go/bin/go test -benchmem -run=^$ cosmossdk.io/simapp -bench ^BenchmarkFullAppSimulation$ -Commit=true -cpuprofile cpu.out +func BenchmarkFullAppSimulation(b *testing.B) { + b.ReportAllocs() + config := simcli.NewConfigFromFlags() + config.ChainID = AppChainID + + db, dir, logger, skip, err := simtestutil.SetupSimulation(config, "goleveldb-app-sim", "Simulation", + simcli.FlagVerboseValue, simcli.FlagEnabledValue) + if err != nil { + b.Fatalf("simulation setup failed: %s", err.Error()) + } + + if skip { + b.Skip("skipping benchmark application simulation") + } + + defer func() { + db.Close() + err = os.RemoveAll(dir) + if err != nil { + b.Fatal(err) + } + }() + + appOptions := make(simtestutil.AppOptionsMap, 0) + appOptions[flags.FlagHome] = DefaultNodeHome + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + + app := NewChainApp(logger, db, nil, true, appOptions, interBlockCacheOpt(), baseapp.SetChainID(AppChainID)) + + // run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + b, + os.Stdout, + app.BaseApp, + simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simtestutil.SimulationOperations(app, app.AppCodec(), config), + BlockedAddresses(), + config, + app.AppCodec(), + ) + + // export state and simParams before the simulation error is checked + if err = simtestutil.CheckExportSimulation(app, config, simParams); err != nil { + b.Fatal(err) + } + + if simErr != nil { + b.Fatal(simErr) + } + + if config.Commit { + simtestutil.PrintStats(db) + } +} + +func BenchmarkInvariants(b *testing.B) { + b.ReportAllocs() + + config := simcli.NewConfigFromFlags() + config.ChainID = AppChainID + + db, dir, logger, skip, err := simtestutil.SetupSimulation(config, "leveldb-app-invariant-bench", "Simulation", + simcli.FlagVerboseValue, simcli.FlagEnabledValue) + if err != nil { + b.Fatalf("simulation setup failed: %s", err.Error()) + } + + if skip { + b.Skip("skipping benchmark application simulation") + } + + config.AllInvariants = false + + defer func() { + db.Close() + err = os.RemoveAll(dir) + if err != nil { + b.Fatal(err) + } + }() + + appOptions := make(simtestutil.AppOptionsMap, 0) + appOptions[flags.FlagHome] = DefaultNodeHome + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + + app := NewChainApp(logger, db, nil, true, appOptions, interBlockCacheOpt(), baseapp.SetChainID(AppChainID)) + + // run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + b, + os.Stdout, + app.BaseApp, + simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simtestutil.SimulationOperations(app, app.AppCodec(), config), + BlockedAddresses(), + config, + app.AppCodec(), + ) + + // export state and simParams before the simulation error is checked + if err = simtestutil.CheckExportSimulation(app, config, simParams); err != nil { + b.Fatal(err) + } + + if simErr != nil { + b.Fatal(simErr) + } + + if config.Commit { + simtestutil.PrintStats(db) + } + + ctx := app.NewContext(true) + + // 3. Benchmark each invariant separately + // + // NOTE: We use the crisis keeper as it has all the invariants registered with + // their respective metadata which makes it useful for testing/benchmarking. + for _, cr := range app.CrisisKeeper.Routes() { + cr := cr + b.Run(fmt.Sprintf("%s/%s", cr.ModuleName, cr.Route), func(b *testing.B) { + if res, stop := cr.Invar(ctx); stop { + b.Fatalf( + "broken invariant at block %d of %d\n%s", + ctx.BlockHeight()-1, config.NumBlocks, res, + ) + } + }) + } +} diff --git a/simapp/app/sim_test.go b/simapp/app/sim_test.go new file mode 100644 index 0000000..f718de8 --- /dev/null +++ b/simapp/app/sim_test.go @@ -0,0 +1,408 @@ +package app + +import ( + "encoding/json" + "fmt" + "os" + "runtime/debug" + "strings" + "testing" + + "cosmossdk.io/log" + "cosmossdk.io/store" + storetypes "cosmossdk.io/store/types" + evidencetypes "cosmossdk.io/x/evidence/types" + abci "github.com/cometbft/cometbft/abci/types" + dbm "github.com/cosmos/cosmos-db" + "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" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + simcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" + "github.com/stretchr/testify/require" + cadatypes "github.com/vitwit/avail-da-module/x/cada/types" + "golang.org/x/exp/rand" +) + +// AppChainID hardcoded chainID for simulation +const AppChainID = "demo" + +// Get flags every time the simulator is run +func init() { + simcli.GetSimulatorFlags() +} + +type StoreKeysPrefixes struct { + A storetypes.StoreKey + B storetypes.StoreKey + Prefixes [][]byte +} + +// fauxMerkleModeOpt returns a BaseApp option to use a dbStoreAdapter instead of +// an IAVLStore for faster simulation speed. +func fauxMerkleModeOpt(bapp *baseapp.BaseApp) { + bapp.SetFauxMerkleMode() +} + +// 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()) +} + +func TestFullAppSimulation(t *testing.T) { + config := simcli.NewConfigFromFlags() + config.ChainID = AppChainID + + 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] = DefaultNodeHome + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + + app := NewChainApp(logger, db, nil, true, appOptions, fauxMerkleModeOpt, baseapp.SetChainID(AppChainID)) + + require.Equal(t, appName, app.Name()) + + // run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simtestutil.SimulationOperations(app, app.AppCodec(), config), + BlockedAddresses(), + 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) + } +} + +func TestAppImportExport(t *testing.T) { + config := simcli.NewConfigFromFlags() + config.ChainID = AppChainID + + db, dir, logger, skip, err := simtestutil.SetupSimulation(config, "leveldb-app-sim", "Simulation", + simcli.FlagVerboseValue, simcli.FlagEnabledValue) + if skip { + t.Skip("skipping application import/export 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] = DefaultNodeHome + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + + app := NewChainApp(logger, db, nil, true, appOptions, fauxMerkleModeOpt, baseapp.SetChainID(AppChainID)) + require.Equal(t, appName, app.Name()) + + // Run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simtestutil.SimulationOperations(app, app.AppCodec(), config), + BlockedAddresses(), + 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) + } + + fmt.Printf("exporting genesis...\n") + + exported, err := app.ExportAppStateAndValidators(false, []string{}, []string{}) + require.NoError(t, err) + + fmt.Printf("importing genesis...\n") + + newDB, newDir, _, _, err := simtestutil.SetupSimulation(config, "leveldb-app-sim-2", "Simulation-2", + simcli.FlagVerboseValue, simcli.FlagEnabledValue) + require.NoError(t, err, "simulation setup failed") + + defer func() { + require.NoError(t, newDB.Close()) + require.NoError(t, os.RemoveAll(newDir)) + }() + + newApp := NewChainApp(log.NewNopLogger(), newDB, nil, true, appOptions, fauxMerkleModeOpt, baseapp.SetChainID(AppChainID)) + require.Equal(t, appName, newApp.Name()) + + var genesisState GenesisState + err = json.Unmarshal(exported.AppState, &genesisState) + require.NoError(t, err) + + defer func() { + if r := recover(); r != nil { + err := fmt.Sprintf("%v", r) + if !strings.Contains(err, "validator set is empty after InitGenesis") && + !strings.Contains(err, "invalid cacheMergeIterator") { + panic(r) + } + logger.Info("Skipping simulation as all validators have been unbonded") + logger.Info("err", err, "stacktrace", string(debug.Stack())) + } + }() + + ctxA := app.NewContext(true) + ctxB := newApp.NewContext(true) + newApp.ModuleManager.InitGenesis(ctxB, app.AppCodec(), genesisState) + newApp.StoreConsensusParams(ctxB, exported.ConsensusParams) + + fmt.Printf("comparing stores...\n") + + storeKeysPrefixes := []StoreKeysPrefixes{ + {app.GetKey(authtypes.StoreKey), newApp.GetKey(authtypes.StoreKey), [][]byte{}}, + { + app.GetKey(stakingtypes.StoreKey), newApp.GetKey(stakingtypes.StoreKey), + [][]byte{ + stakingtypes.UnbondingQueueKey, stakingtypes.RedelegationQueueKey, stakingtypes.ValidatorQueueKey, + stakingtypes.HistoricalInfoKey, stakingtypes.UnbondingIDKey, stakingtypes.UnbondingIndexKey, + stakingtypes.UnbondingTypeKey, stakingtypes.ValidatorUpdatesKey, + }, + }, // ordering may change but it doesn't matter + {app.GetKey(slashingtypes.StoreKey), newApp.GetKey(slashingtypes.StoreKey), [][]byte{}}, + {app.GetKey(minttypes.StoreKey), newApp.GetKey(minttypes.StoreKey), [][]byte{}}, + {app.GetKey(distrtypes.StoreKey), newApp.GetKey(distrtypes.StoreKey), [][]byte{}}, + {app.GetKey(banktypes.StoreKey), newApp.GetKey(banktypes.StoreKey), [][]byte{banktypes.BalancesPrefix}}, + {app.GetKey(paramtypes.StoreKey), newApp.GetKey(paramtypes.StoreKey), [][]byte{}}, + {app.GetKey(govtypes.StoreKey), newApp.GetKey(govtypes.StoreKey), [][]byte{}}, + {app.GetKey(evidencetypes.StoreKey), newApp.GetKey(evidencetypes.StoreKey), [][]byte{}}, + {app.GetKey(capabilitytypes.StoreKey), newApp.GetKey(capabilitytypes.StoreKey), [][]byte{}}, + {app.GetKey(authzkeeper.StoreKey), newApp.GetKey(authzkeeper.StoreKey), [][]byte{authzkeeper.GrantKey, authzkeeper.GrantQueuePrefix}}, + {app.GetKey(cadatypes.StoreKey), newApp.GetKey(cadatypes.StoreKey), [][]byte{}}, + } + + for _, skp := range storeKeysPrefixes { + storeA := ctxA.KVStore(skp.A) + storeB := ctxB.KVStore(skp.B) + + failedKVAs, failedKVBs := simtestutil.DiffKVStores(storeA, storeB, skp.Prefixes) + require.Equal(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare") + + fmt.Printf("compared %d different key/value pairs between %s and %s\n", len(failedKVAs), skp.A, skp.B) + require.Equal(t, len(failedKVAs), 0, simtestutil.GetSimulationLog(skp.A.Name(), app.SimulationManager().StoreDecoders, failedKVAs, failedKVBs)) + } +} + +func TestAppSimulationAfterImport(t *testing.T) { + config := simcli.NewConfigFromFlags() + config.ChainID = AppChainID + + db, dir, logger, skip, err := simtestutil.SetupSimulation(config, "leveldb-app-sim", "Simulation", + simcli.FlagVerboseValue, simcli.FlagEnabledValue) + if skip { + t.Skip("skipping application simulation after import") + } + 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] = DefaultNodeHome + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + + app := NewChainApp(logger, db, nil, true, appOptions, fauxMerkleModeOpt, baseapp.SetChainID(AppChainID)) + require.Equal(t, appName, app.Name()) + + // Run randomized simulation + stopEarly, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simtestutil.SimulationOperations(app, app.AppCodec(), config), + BlockedAddresses(), + 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) + } + + if stopEarly { + fmt.Println("can't export or import a zero-validator genesis, exiting test...") + return + } + + fmt.Printf("exporting genesis...\n") + + exported, err := app.ExportAppStateAndValidators(true, []string{}, []string{}) + require.NoError(t, err) + + fmt.Printf("importing genesis...\n") + + newDB, newDir, _, _, err := simtestutil.SetupSimulation(config, "leveldb-app-sim-2", "Simulation-2", + simcli.FlagVerboseValue, simcli.FlagEnabledValue) + require.NoError(t, err, "simulation setup failed") + + defer func() { + require.NoError(t, newDB.Close()) + require.NoError(t, os.RemoveAll(newDir)) + }() + + newApp := NewChainApp(log.NewNopLogger(), newDB, nil, true, appOptions, fauxMerkleModeOpt, baseapp.SetChainID(AppChainID)) + require.Equal(t, appName, newApp.Name()) + + defer func() { + if r := recover(); r != nil { + err := fmt.Sprintf("%v", r) + if !strings.Contains(err, "validator set is empty after InitGenesis") { + panic(r) + } + logger.Info("Skipping simulation as all validators have been unbonded") + logger.Info("err", err, "stacktrace", string(debug.Stack())) + } + }() + + newApp.InitChain(&abci.RequestInitChain{ + AppStateBytes: exported.AppState, + ChainId: AppChainID, + }) + + _, _, err = simulation.SimulateFromSeed( + t, + os.Stdout, + newApp.BaseApp, + simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simtestutil.SimulationOperations(newApp, newApp.AppCodec(), config), + BlockedAddresses(), + config, + app.AppCodec(), + ) + require.NoError(t, err) +} + +// TODO: Make another test for the fuzzer itself, which just has noOp txs +// and doesn't depend on the application. +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 = AppChainID + + numSeeds := 3 + numTimesToRunPerSeed := 5 + + // We will be overriding the random seed and just run a single simulation on the provided seed value + if config.Seed != simcli.DefaultSeedValue { + numSeeds = 1 + } + + appHashList := make([]json.RawMessage, numTimesToRunPerSeed) + appOptions := make(simtestutil.AppOptionsMap, 0) + appOptions[flags.FlagHome] = DefaultNodeHome + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + + for i := 0; i < numSeeds; i++ { + if config.Seed == simcli.DefaultSeedValue { + config.Seed = rand.Int63() + } + + for j := 0; j < numTimesToRunPerSeed; j++ { + var logger log.Logger + if simcli.FlagVerboseValue { + logger = log.NewTestLogger(t) + } else { + logger = log.NewNopLogger() + } + + db := dbm.NewMemDB() + app := NewChainApp(logger, db, nil, true, appOptions, interBlockCacheOpt(), baseapp.SetChainID(AppChainID)) + + fmt.Printf( + "running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n", + config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, + ) + + _, _, err := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simtestutil.SimulationOperations(app, app.AppCodec(), config), + BlockedAddresses(), + config, + app.AppCodec(), + ) + require.NoError(t, err) + + if config.Commit { + simtestutil.PrintStats(db) + } + + appHash := app.LastCommitID().Hash + appHashList[j] = appHash + + if j != 0 { + require.Equal( + t, string(appHashList[0]), string(appHashList[j]), + "non-determinism in seed %d: %d/%d, attempt: %d/%d\n", config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, + ) + } + } + } +} diff --git a/x/cada/keeper/keeper.go b/x/cada/keeper/keeper.go index 5048ebd..d881e77 100644 --- a/x/cada/keeper/keeper.go +++ b/x/cada/keeper/keeper.go @@ -66,3 +66,7 @@ func (k *Keeper) GetBlobStatus(ctx sdk.Context) uint32 { store := ctx.KVStore(k.storeKey) return GetStatusFromStore(store) } + +func (k Keeper) GetStoreKey() storetypes2.StoreKey { + return k.storeKey +} diff --git a/x/cada/module/module.go b/x/cada/module/module.go index b5b78ea..8180d24 100644 --- a/x/cada/module/module.go +++ b/x/cada/module/module.go @@ -12,12 +12,14 @@ import ( codectypes "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" gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" "github.com/vitwit/avail-da-module/x/cada/client/cli" "github.com/vitwit/avail-da-module/x/cada/keeper" + simulation "github.com/vitwit/avail-da-module/x/cada/simulation" types "github.com/vitwit/avail-da-module/x/cada/types" ) @@ -171,18 +173,18 @@ func (am AppModule) IsOnePerModuleType() {} // IsAppModule implements the appmodule.AppModule interface. func (am AppModule) IsAppModule() {} -// func (AppModule) GenerateGenesisState(simState *module.SimulationState) { -// simulation.RandomizedGenState(simState) -// } - -// // RegisterStoreDecoder registers a decoder for distribution module's types -// func (am AppModule) RegisterStoreDecoder(_ simtypes.StoreDecoderRegistry) { -// } - -// // WeightedOperations returns the all the accounts module operations with their respective weights. -// func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { -// return simulation.WeightedOperations( -// simState.AppParams, simState.Cdc, simState.TxConfig, -// am.authkeeper, am.bankkeeper, *am.keeper, -// ) -// } +func (AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// RegisterStoreDecoder registers a decoder for distribution module's types +func (am AppModule) RegisterStoreDecoder(_ simtypes.StoreDecoderRegistry) { +} + +// WeightedOperations returns the all the accounts module operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations( + simState.AppParams, simState.Cdc, simState.TxConfig, + am.authkeeper, am.bankkeeper, *am.keeper, + ) +} diff --git a/x/cada/simulation/genesis.go b/x/cada/simulation/genesis.go new file mode 100644 index 0000000..5f32926 --- /dev/null +++ b/x/cada/simulation/genesis.go @@ -0,0 +1,16 @@ +package simulation + +import ( + "github.com/cosmos/cosmos-sdk/types/module" + types "github.com/vitwit/avail-da-module/x/cada/types" +) + +// RandomizedGenState creates a randomized GenesisState for testing. +func RandomizedGenState(simState *module.SimulationState) { + // Since your GenesisState is empty, there's not much to randomize. + // We'll just set the GenesisState to its empty struct. + genesis := types.GenesisState{} + + // Here we use simState to set the default genesis + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&genesis) +} diff --git a/x/cada/simulation/operation.go b/x/cada/simulation/operation.go new file mode 100644 index 0000000..2f7e06c --- /dev/null +++ b/x/cada/simulation/operation.go @@ -0,0 +1,103 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + 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/vitwit/avail-da-module/x/cada/keeper" + cadastore "github.com/vitwit/avail-da-module/x/cada/keeper" + availtypes "github.com/vitwit/avail-da-module/x/cada/types" +) + +const ( + OpWeightMsgUpdateBlobStatusRequest = "op_weight_msg_update_blob_status" + + DefaultWeightMsgUpdateStatusRequest = 100 +) + +func WeightedOperations( + appParams simtypes.AppParams, cdc codec.JSONCodec, txConfig client.TxConfig, + ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, k keeper.Keeper, +) simulation.WeightedOperations { + var weightMsgUpdateBlobStatusRequest int + appParams.GetOrGenerate(OpWeightMsgUpdateBlobStatusRequest, &weightMsgUpdateBlobStatusRequest, nil, func(_ *rand.Rand) { + weightMsgUpdateBlobStatusRequest = DefaultWeightMsgUpdateStatusRequest + }) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgUpdateBlobStatusRequest, + SimulateMsgUpdateBlobStatus(ak, bk, k), + ), + } +} + +func SimulateMsgUpdateBlobStatus(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) { + + ctx = ctx.WithBlockHeight(20) + // Randomly select a sender account + sender, _ := simtypes.RandomAcc(r, accs) + + // Ensure the sender has sufficient balance + account := ak.GetAccount(ctx, sender.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + // Generate random fees for the transaction + fees, err := simtypes.RandomFees(r, ctx, spendable) + if err != nil { + return simtypes.NoOpMsg(availtypes.ModuleName, availtypes.TypeMsgUpdateBlobStatus, "unable to generate fees"), nil, err + } + + // Prepare a random blob status update + newStatus := true // You can randomize this value as needed + fromBlock := uint64(5) // Example block range start + toBlock := uint64(20) // Example block range end + availHeight := uint64(120) + + ran := availtypes.Range{ + From: fromBlock, + To: toBlock, + } + + msg := availtypes.NewMsgUpdateBlobStatus( + sender.Address.String(), + ran, + availHeight, + newStatus, + ) + + store := ctx.KVStore(k.GetStoreKey()) + cadastore.UpdateEndHeight(ctx, store, uint64(20)) + + cadastore.UpdateProvenHeight(ctx, store, uint64(4)) + + cadastore.UpdateBlobStatus(ctx, store, uint32(1)) + + // Set up the transaction context + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + Context: ctx, + SimAccount: sender, + AccountKeeper: ak, + ModuleName: availtypes.ModuleName, + } + + // Generate and deliver the transaction + return simulation.GenAndDeliverTx(txCtx, fees) + } +}