//go:build sims package simapp import ( "encoding/binary" "encoding/json" "flag" "io" "math/rand" "strings" "sync" "testing" abci "github.com/cometbft/cometbft/abci/types" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" dbm "github.com/cosmos/cosmos-db" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "cosmossdk.io/log" "cosmossdk.io/store" storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/baseapp" servertypes "github.com/cosmos/cosmos-sdk/server/types" "github.com/cosmos/cosmos-sdk/telemetry" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sims "github.com/cosmos/cosmos-sdk/testutil/simsx" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" "github.com/cosmos/cosmos-sdk/x/feegrant" "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" ) var FlagEnableStreamingValue bool // Get flags every time the simulator is run func init() { simcli.GetSimulatorFlags() flag.BoolVar(&FlagEnableStreamingValue, "EnableStreaming", false, "Enable streaming service") } func TestMain(m *testing.M) { telemetry.TestingMain(m, nil) } // 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) { sims.Run(t, NewSimApp, setupStateFactory) } func setupStateFactory(app *SimApp) sims.SimStateFactory { return sims.SimStateFactory{ Codec: app.AppCodec(), AppStateFn: simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), BlockedAddr: BlockedAddresses(), AccountSource: app.AccountKeeper, BalanceSource: app.BankKeeper, } } var ( exportAllModules = []string{} exportWithValidatorSet = []string{} ) func TestAppImportExport(t *testing.T) { sims.Run(t, NewSimApp, setupStateFactory, func(tb testing.TB, ti sims.TestInstance[*SimApp], accs []simtypes.Account) { tb.Helper() app := ti.App tb.Log("exporting genesis...\n") exported, err := app.ExportAppStateAndValidators(false, exportWithValidatorSet, exportAllModules) require.NoError(tb, err) tb.Log("importing genesis...\n") newTestInstance := sims.NewSimulationAppInstance(tb, ti.Cfg, NewSimApp) newApp := newTestInstance.App var genesisState GenesisState require.NoError(tb, json.Unmarshal(exported.AppState, &genesisState)) ctxB := newApp.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()}) _, err = newApp.ModuleManager.InitGenesis(ctxB, newApp.appCodec, genesisState) if IsEmptyValidatorSetErr(err) { tb.Skip("Skipping simulation as all validators have been unbonded") return } require.NoError(tb, err) err = newApp.StoreConsensusParams(ctxB, exported.ConsensusParams) require.NoError(tb, err) tb.Log("comparing stores...") // skip certain prefixes skipPrefixes := map[string][][]byte{ stakingtypes.StoreKey: { stakingtypes.UnbondingQueueKey, stakingtypes.RedelegationQueueKey, stakingtypes.ValidatorQueueKey, stakingtypes.HistoricalInfoKey, stakingtypes.UnbondingIDKey, stakingtypes.UnbondingIndexKey, stakingtypes.UnbondingTypeKey, stakingtypes.ValidatorUpdatesKey, // todo (Alex): double check why there is a diff with test-sim-import-export }, authzkeeper.StoreKey: {authzkeeper.GrantQueuePrefix}, feegrant.StoreKey: {feegrant.FeeAllowanceQueueKeyPrefix}, slashingtypes.StoreKey: {slashingtypes.ValidatorMissedBlockBitmapKeyPrefix}, } AssertEqualStores(tb, app, newApp, app.SimulationManager().StoreDecoders, skipPrefixes) }) } // Scenario: // // Start a fresh node and run n blocks, export state // set up a new node instance, Init chain from exported genesis // run new instance for n blocks func TestAppSimulationAfterImport(t *testing.T) { sims.Run(t, NewSimApp, setupStateFactory, func(tb testing.TB, ti sims.TestInstance[*SimApp], accs []simtypes.Account) { tb.Helper() app := ti.App tb.Log("exporting genesis...\n") exported, err := app.ExportAppStateAndValidators(false, exportWithValidatorSet, exportAllModules) require.NoError(tb, err) tb.Log("importing genesis...\n") newTestInstance := sims.NewSimulationAppInstance(tb, ti.Cfg, NewSimApp) newApp := newTestInstance.App _, err = newApp.InitChain(&abci.RequestInitChain{ AppStateBytes: exported.AppState, ChainId: sims.SimAppChainID, }) if IsEmptyValidatorSetErr(err) { tb.Skip("Skipping simulation as all validators have been unbonded") return } require.NoError(tb, err) newStateFactory := setupStateFactory(newApp) _, _, err = simulation.SimulateFromSeedX( tb, newTestInstance.AppLogger, sims.WriteToDebugLog(newTestInstance.AppLogger), newApp.BaseApp, newStateFactory.AppStateFn, simtypes.RandomAccounts, simtestutil.BuildSimulationOperations(newApp, newApp.AppCodec(), newTestInstance.Cfg, newApp.TxConfig()), newStateFactory.BlockedAddr, newTestInstance.Cfg, newStateFactory.Codec, ti.ExecLogWriter, ) require.NoError(tb, err) }) } func IsEmptyValidatorSetErr(err error) bool { return err != nil && strings.Contains(err.Error(), "validator set is empty after InitGenesis") } func TestAppStateDeterminism(t *testing.T) { const numTimesToRunPerSeed = 3 var seeds []int64 if s := simcli.NewConfigFromFlags().Seed; s != simcli.DefaultSeedValue { // We will be overriding the random seed and just run a single simulation on the provided seed value for j := 0; j < numTimesToRunPerSeed; j++ { // multiple rounds seeds = append(seeds, s) } } else { // setup with 3 random seeds for i := 0; i < 3; i++ { seed := rand.Int63() for j := 0; j < numTimesToRunPerSeed; j++ { // multiple rounds seeds = append(seeds, seed) } } } // overwrite default app config interBlockCachingAppFactory := func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) *SimApp { if FlagEnableStreamingValue { m := map[string]any{ "streaming.abci.keys": []string{"*"}, "streaming.abci.plugin": "abci_v1", "streaming.abci.stop-node-on-err": true, } others := appOpts appOpts = appOptionsFn(func(k string) any { if v, ok := m[k]; ok { return v } return others.Get(k) }) } return NewSimApp(logger, db, nil, true, appOpts, append(baseAppOptions, interBlockCacheOpt())...) } var mx sync.Mutex appHashResults := make(map[int64][][]byte) appSimLogger := make(map[int64][]simulation.LogWriter) captureAndCheckHash := func(tb testing.TB, ti sims.TestInstance[*SimApp], _ []simtypes.Account) { tb.Helper() seed, appHash := ti.Cfg.Seed, ti.App.LastCommitID().Hash mx.Lock() otherHashes, execWriters := appHashResults[seed], appSimLogger[seed] if len(otherHashes) < numTimesToRunPerSeed-1 { appHashResults[seed], appSimLogger[seed] = append(otherHashes, appHash), append(execWriters, ti.ExecLogWriter) } else { // cleanup delete(appHashResults, seed) delete(appSimLogger, seed) } mx.Unlock() var failNow bool // and check that all app hashes per seed are equal for each iteration for i := 0; i < len(otherHashes); i++ { if !assert.Equal(tb, otherHashes[i], appHash) { execWriters[i].PrintLogs() failNow = true } } if failNow { ti.ExecLogWriter.PrintLogs() tb.Fatalf("non-determinism in seed %d", seed) } } // run simulations sims.RunWithSeeds(t, interBlockCachingAppFactory, setupStateFactory, seeds, []byte{}, captureAndCheckHash) } type ComparableStoreApp interface { LastBlockHeight() int64 NewContextLegacy(isCheckTx bool, header cmtproto.Header) sdk.Context GetKey(storeKey string) *storetypes.KVStoreKey GetStoreKeys() []storetypes.StoreKey } func AssertEqualStores( tb testing.TB, app, newApp ComparableStoreApp, storeDecoders simtypes.StoreDecoderRegistry, skipPrefixes map[string][][]byte, ) { tb.Helper() ctxA := app.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()}) ctxB := newApp.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()}) storeKeys := app.GetStoreKeys() require.NotEmpty(tb, storeKeys) for _, appKeyA := range storeKeys { // only compare kvstores if _, ok := appKeyA.(*storetypes.KVStoreKey); !ok { continue } keyName := appKeyA.Name() appKeyB := newApp.GetKey(keyName) storeA := ctxA.KVStore(appKeyA) storeB := ctxB.KVStore(appKeyB) failedKVAs, failedKVBs := simtestutil.DiffKVStores(storeA, storeB, skipPrefixes[keyName]) require.Equal(tb, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare %s, key stores %s and %s", keyName, appKeyA, appKeyB) tb.Logf("compared %d different key/value pairs between %s and %s\n", len(failedKVAs), appKeyA, appKeyB) if !assert.Equal(tb, 0, len(failedKVAs), simtestutil.GetSimulationLog(keyName, storeDecoders, failedKVAs, failedKVBs)) { for _, v := range failedKVAs { tb.Logf("store mismatch: %q\n", v) } tb.FailNow() } } } // appOptionsFn is an adapter to the single method AppOptions interface type appOptionsFn func(string) any func (f appOptionsFn) Get(k string) any { return f(k) } // 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 FuzzFullAppSimulation(f *testing.F) { f.Fuzz(func(t *testing.T, rawSeed []byte) { if len(rawSeed) < 8 { t.Skip() return } sims.RunWithSeeds( t, NewSimApp, setupStateFactory, []int64{int64(binary.BigEndian.Uint64(rawSeed))}, rawSeed[8:], ) }) }