cosmos-sdk/simapp/sim_test.go
Aaron Craelius f2d4a98039
feat: OpenTelemetry configuration and BaseApp instrumentation (#25516)
Co-authored-by: Tyler <48813565+technicallyty@users.noreply.github.com>
Co-authored-by: Alex | Cosmos Labs <alex@cosmoslabs.io>
2025-12-10 23:15:18 +00:00

305 lines
9.8 KiB
Go

//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:],
)
})
}