diff --git a/app/sim_test.go b/app/sim_test.go new file mode 100644 index 0000000..a3506d1 --- /dev/null +++ b/app/sim_test.go @@ -0,0 +1,385 @@ +package app + +import ( + "encoding/json" + "flag" + "fmt" + "math/rand" + "os" + "runtime/debug" + "strings" + "testing" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + abci "github.com/cometbft/cometbft/abci/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + + dbm "github.com/cosmos/cosmos-db" + + "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" + "cosmossdk.io/x/feegrant" + + "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" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + "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 TestFullAppSimulation(t *testing.T) { + config := simcli.NewConfigFromFlags() + config.ChainID = SimAppChainID + + 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 := NewApp(logger, db, nil, true, appOptions, []wasmkeeper.Option{}, baseapp.SetChainID(SimAppChainID)) + require.Equal(t, "tokenfactory", 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 = SimAppChainID + + 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 := NewApp(logger, db, nil, true, appOptions, []wasmkeeper.Option{}, baseapp.SetChainID(SimAppChainID)) + require.Equal(t, "tokenfactory", 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 := NewApp(log.NewNopLogger(), newDB, nil, true, appOptions, []wasmkeeper.Option{}, baseapp.SetChainID(SimAppChainID)) + require.Equal(t, "tokenfactory", newApp.Name()) + + var genesisState GenesisState + err = json.Unmarshal(exported.AppState, &genesisState) + require.NoError(t, err) + + ctxA := app.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()}) + ctxB := newApp.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()}) + _, err = newApp.ModuleManager.InitGenesis(ctxB, app.AppCodec(), genesisState) + + if err != nil { + if strings.Contains(err.Error(), "validator set is empty after InitGenesis") { + logger.Info("Skipping simulation as all validators have been unbonded") + logger.Info("err", err, "stacktrace", string(debug.Stack())) + return + } + } + + require.NoError(t, err) + err = newApp.StoreConsensusParams(ctxB, exported.ConsensusParams) + require.NoError(t, err) + fmt.Printf("comparing stores...\n") + + // 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, + }, + authzkeeper.StoreKey: {authzkeeper.GrantQueuePrefix}, + feegrant.StoreKey: {feegrant.FeeAllowanceQueueKeyPrefix}, + slashingtypes.StoreKey: {slashingtypes.ValidatorMissedBlockBitmapKeyPrefix}, + } + + storeKeys := app.GetStoreKeys() + require.NotEmpty(t, 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(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare %s", keyName) + + fmt.Printf("compared %d different key/value pairs between %s and %s\n", len(failedKVAs), appKeyA, appKeyB) + + require.Equal(t, 0, len(failedKVAs), simtestutil.GetSimulationLog(keyName, app.SimulationManager().StoreDecoders, failedKVAs, failedKVBs)) + } +} + +func TestAppSimulationAfterImport(t *testing.T) { + config := simcli.NewConfigFromFlags() + config.ChainID = SimAppChainID + + 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 := NewApp(logger, db, nil, true, appOptions, []wasmkeeper.Option{}, baseapp.SetChainID(SimAppChainID)) + require.Equal(t, "tokenfactory", 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 := NewApp(log.NewNopLogger(), newDB, nil, true, appOptions, []wasmkeeper.Option{}, baseapp.SetChainID(SimAppChainID)) + require.Equal(t, "tokenfactory", newApp.Name()) + + _, err = newApp.InitChain(&abci.RequestInitChain{ + AppStateBytes: exported.AppState, + ChainId: SimAppChainID, + }) + require.NoError(t, err) + + _, _, 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) +} + +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 = SimAppChainID + + numSeeds := 3 + numTimesToRunPerSeed := 3 // This used to be set to 5, but we've temporarily reduced it to 3 for the sake of faster CI. + appHashList := make([]json.RawMessage, numTimesToRunPerSeed) + + // 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 + } + + appOptions := viper.New() + if FlagEnableStreamingValue { + m := make(map[string]interface{}) + m["streaming.abci.keys"] = []string{"*"} + m["streaming.abci.plugin"] = "abci_v1" + m["streaming.abci.stop-node-on-err"] = true + for key, value := range m { + appOptions.SetDefault(key, value) + } + } + appOptions.SetDefault(flags.FlagHome, DefaultNodeHome) + appOptions.SetDefault(server.FlagInvCheckPeriod, simcli.FlagPeriodValue) + if simcli.FlagVerboseValue { + appOptions.SetDefault(flags.FlagLogLevel, "debug") + } + + for i := 0; i < numSeeds; i++ { + if config.Seed == simcli.DefaultSeedValue { + config.Seed = rand.Int63() + } + + fmt.Println("config.Seed: ", config.Seed) + + 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 := NewApp(logger, db, nil, true, appOptions, []wasmkeeper.Option{}, baseapp.SetChainID(SimAppChainID)) + + 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/tokenfactory/keeper/keeper.go b/x/tokenfactory/keeper/keeper.go index c6ea0ff..8ff361f 100644 --- a/x/tokenfactory/keeper/keeper.go +++ b/x/tokenfactory/keeper/keeper.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/types" "cosmossdk.io/log" @@ -13,6 +12,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) type ( diff --git a/x/tokenfactory/simulation/operations.go b/x/tokenfactory/simulation/operations.go index 3bcb060..b6b115a 100644 --- a/x/tokenfactory/simulation/operations.go +++ b/x/tokenfactory/simulation/operations.go @@ -22,12 +22,12 @@ import ( // //nolint:gosec const ( - OpWeightMsgCreateDenom = "op_weight_msg_create_denom" - OpWeightMsgMint = "op_weight_msg_mint" - OpWeightMsgBurn = "op_weight_msg_burn" - OpWeightMsgChangeAdmin = "op_weight_msg_change_admin" - OpWeightMsgSetDenomMetadata = "op_weight_msg_set_denom_metadata" - OpWeightMsgForceTransfer = "op_weight_msg_force_transfer" + OpWeightMsgCreateDenom = "op_weight_msg_tf_create_denom" + OpWeightMsgMint = "op_weight_msg_tf_mint" + OpWeightMsgBurn = "op_weight_msg_tf_burn" + OpWeightMsgChangeAdmin = "op_weight_msg_tf_change_admin" + OpWeightMsgSetDenomMetadata = "op_weight_msg_tf_set_denom_metadata" + OpWeightMsgForceTransfer = "op_weight_msg_tf_force_transfer" DefaultWeightMsgCreateDenom int = 100 DefaultWeightMsgMint int = 100 @@ -170,23 +170,25 @@ func SimulateMsgSetDenomMetadata( accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + msgType := sdk.MsgTypeURL(&types.MsgSetDenomMetadata{}) + // Get create denom account createdDenomAccount, _ := simtypes.RandomAcc(r, accs) // Get demon denom, hasDenom := denomSelector(r, ctx, tfKeeper, createdDenomAccount.Address.String()) if !hasDenom { - return simtypes.NoOpMsg(types.ModuleName, types.MsgSetDenomMetadata{}.Type(), "sim account have no denom created"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "sim account have no denom created"), nil, nil } // Get admin of the denom authData, err := tfKeeper.GetAuthorityMetadata(ctx, denom) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, types.MsgSetDenomMetadata{}.Type(), "err authority metadata"), nil, err + return simtypes.NoOpMsg(types.ModuleName, msgType, "err authority metadata"), nil, err } adminAccount, found := simtypes.FindAccount(accs, sdk.MustAccAddressFromBech32(authData.Admin)) if !found { - return simtypes.NoOpMsg(types.ModuleName, types.MsgSetDenomMetadata{}.Type(), "admin account not found"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "admin account not found"), nil, nil } metadata := banktypes.Metadata{ @@ -224,29 +226,30 @@ func SimulateMsgChangeAdmin( accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + msgType := sdk.MsgTypeURL(&types.MsgChangeAdmin{}) // Get create denom account createdDenomAccount, _ := simtypes.RandomAcc(r, accs) // Get demon denom, hasDenom := denomSelector(r, ctx, tfKeeper, createdDenomAccount.Address.String()) if !hasDenom { - return simtypes.NoOpMsg(types.ModuleName, types.MsgChangeAdmin{}.Type(), "sim account have no denom created"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "sim account have no denom created"), nil, nil } // Get admin of the denom authData, err := tfKeeper.GetAuthorityMetadata(ctx, denom) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, types.MsgChangeAdmin{}.Type(), "err authority metadata"), nil, err + return simtypes.NoOpMsg(types.ModuleName, msgType, "err authority metadata"), nil, err } curAdminAccount, found := simtypes.FindAccount(accs, sdk.MustAccAddressFromBech32(authData.Admin)) if !found { - return simtypes.NoOpMsg(types.ModuleName, types.MsgChangeAdmin{}.Type(), "admin account not found"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "admin account not found"), nil, nil } // Rand new admin account newAdmin, _ := simtypes.RandomAcc(r, accs) if newAdmin.Address.String() == curAdminAccount.Address.String() { - return simtypes.NoOpMsg(types.ModuleName, types.MsgChangeAdmin{}.Type(), "new admin cannot be the same as current admin"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "new admin cannot be the same as current admin"), nil, nil } // Create msg @@ -274,29 +277,31 @@ func SimulateMsgBurn( accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + msgType := sdk.MsgTypeURL(&types.MsgBurn{}) + // Get create denom account createdDenomAccount, _ := simtypes.RandomAcc(r, accs) // Get demon denom, hasDenom := denomSelector(r, ctx, tfKeeper, createdDenomAccount.Address.String()) if !hasDenom { - return simtypes.NoOpMsg(types.ModuleName, types.MsgBurn{}.Type(), "sim account have no denom created"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "sim account have no denom created"), nil, nil } // Get admin of the denom authData, err := tfKeeper.GetAuthorityMetadata(ctx, denom) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, types.MsgBurn{}.Type(), "err authority metadata"), nil, err + return simtypes.NoOpMsg(types.ModuleName, msgType, "err authority metadata"), nil, err } adminAccount, found := simtypes.FindAccount(accs, sdk.MustAccAddressFromBech32(authData.Admin)) if !found { - return simtypes.NoOpMsg(types.ModuleName, types.MsgBurn{}.Type(), "admin account not found"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "admin account not found"), nil, nil } // Check if admin account balance = 0 accountBalance := bk.GetBalance(ctx, adminAccount.Address, denom) if accountBalance.Amount.LTE(sdkmath.ZeroInt()) { - return simtypes.NoOpMsg(types.ModuleName, types.MsgBurn{}.Type(), "sim account have no balance"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "sim account have no balance"), nil, nil } // Rand burn amount @@ -328,23 +333,25 @@ func SimulateMsgMint( accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + msgType := sdk.MsgTypeURL(&types.MsgMint{}) + // Get create denom account createdDenomAccount, _ := simtypes.RandomAcc(r, accs) // Get demon denom, hasDenom := denomSelector(r, ctx, tfKeeper, createdDenomAccount.Address.String()) if !hasDenom { - return simtypes.NoOpMsg(types.ModuleName, types.MsgMint{}.Type(), "sim account have no denom created"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "sim account have no denom created"), nil, nil } // Get admin of the denom authData, err := tfKeeper.GetAuthorityMetadata(ctx, denom) if err != nil { - return simtypes.NoOpMsg(types.ModuleName, types.MsgMint{}.Type(), "err authority metadata"), nil, err + return simtypes.NoOpMsg(types.ModuleName, msgType, "err authority metadata"), nil, err } adminAccount, found := simtypes.FindAccount(accs, sdk.MustAccAddressFromBech32(authData.Admin)) if !found { - return simtypes.NoOpMsg(types.ModuleName, types.MsgMint{}.Type(), "admin account not found"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "admin account not found"), nil, nil } // Rand mint amount @@ -370,6 +377,7 @@ func SimulateMsgCreateDenom(tfKeeper TokenfactoryKeeper, ak types.AccountKeeper, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + msgType := sdk.MsgTypeURL(&types.MsgCreateDenom{}) // Get sims account simAccount, _ := simtypes.RandomAcc(r, accs) @@ -378,7 +386,7 @@ func SimulateMsgCreateDenom(tfKeeper TokenfactoryKeeper, ak types.AccountKeeper, balances := bk.GetAllBalances(ctx, simAccount.Address) _, hasNeg := balances.SafeSub(createFee[0]) if hasNeg { - return simtypes.NoOpMsg(types.ModuleName, types.MsgCreateDenom{}.Type(), "Creator not enough creation fee"), nil, nil + return simtypes.NoOpMsg(types.ModuleName, msgType, "Creator not enough creation fee"), nil, nil } // Create msg create denom