diff --git a/.github/workflows/sims.yml b/.github/workflows/sims.yml index 59dc62b0c6..99fabfa256 100644 --- a/.github/workflows/sims.yml +++ b/.github/workflows/sims.yml @@ -63,9 +63,9 @@ jobs: with: go-version: "1.23" check-latest: true - - name: test-sim-nondeterminism + - name: test-sim-nondeterminism-streaming run: | - make test-sim-nondeterminism + make test-sim-nondeterminism-streaming test-sim-multi-seed-short: runs-on: depot-ubuntu-22.04-16 diff --git a/.golangci.yml b/.golangci.yml index 29c3e352b6..71eafc7787 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,6 +6,7 @@ run: - e2e - ledger - test_ledger_mock + - sims linters: disable-all: true diff --git a/CHANGELOG.md b/CHANGELOG.md index e8d95c5d48..32aa6e30b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (perf)[#24045](https://github.com/cosmos/cosmos-sdk/pull/24045) Sims: Replace runsim command with Go stdlib testing. CLI: `Commit` default true, `Lean`, `SimulateEveryOperation`, `PrintAllInvariants`, `DBBackend` params removed * (crypto/keyring) [#24040](https://github.com/cosmos/cosmos-sdk/pull/24040) Expose the db keyring used in the keystore. * (types) [#23919](https://github.com/cosmos/cosmos-sdk/pull/23919) Add MustValAddressFromBech32 function. * (all) [#23708](https://github.com/cosmos/cosmos-sdk/pull/23708) Add unordered transaction support. diff --git a/Makefile b/Makefile index 78f453b0e6..fff5efbf5a 100644 --- a/Makefile +++ b/Makefile @@ -108,9 +108,6 @@ endif all: tools build lint test vulncheck -# The below include contains the tools and runsim targets. -include contrib/devtools/Makefile - ############################################################################### ### Build ### ############################################################################### @@ -267,8 +264,8 @@ endif test-sim-nondeterminism: @echo "Running non-determinism test..." - @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -run TestAppStateDeterminism -Enabled=true \ - -NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h + @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -timeout=30m -tags='sims' -run TestAppStateDeterminism \ + -NumBlocks=100 -BlockSize=200 -Period=0 # Requires an exported plugin. See store/streaming/README.md for documentation. # @@ -281,41 +278,40 @@ test-sim-nondeterminism: # make test-sim-nondeterminism-streaming test-sim-nondeterminism-streaming: @echo "Running non-determinism-streaming test..." - @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -run TestAppStateDeterminism -Enabled=true \ - -NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h -EnableStreaming=true + @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -timeout=30m -tags='sims' -run TestAppStateDeterminism \ + -NumBlocks=100 -BlockSize=200 -Period=0 -EnableStreaming=true test-sim-custom-genesis-fast: @echo "Running custom genesis simulation..." @echo "By default, ${HOME}/.simapp/config/genesis.json will be used." - @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -run TestFullAppSimulation -Genesis=${HOME}/.simapp/config/genesis.json \ - -Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Seed=99 -Period=5 -SigverifyTx=false -v -timeout 24h + @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -timeout=30m -tags='sims' -run TestFullAppSimulation -Genesis=${HOME}/.simapp/config/genesis.json \ + -NumBlocks=100 -BlockSize=200 -Seed=99 -Period=5 -SigverifyTx=false -test-sim-import-export: runsim +test-sim-import-export: @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 && go test -mod=readonly -timeout 20m -tags='sims' -run TestAppImportExport \ + -NumBlocks=50 -Period=5 -test-sim-after-import: runsim +test-sim-after-import: @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 && go test -mod=readonly -timeout 30m -tags='sims' -run TestAppSimulationAfterImport \ + -NumBlocks=50 -Period=5 -test-sim-custom-genesis-multi-seed: runsim +test-sim-custom-genesis-multi-seed: @echo "Running multi-seed custom genesis simulation..." @echo "By default, ${HOME}/.simapp/config/genesis.json will be used." - @cd ${CURRENT_DIR}/simapp && $(BINDIR)/runsim -Genesis=${HOME}/.simapp/config/genesis.json -SigverifyTx=false -SimAppPkg=. -ExitOnFail 400 5 TestFullAppSimulation + @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -timeout 30m -tags='sims' -run TestFullAppSimulation -Genesis=${HOME}/.simapp/config/genesis.json \ + -NumBlocks=400 -Period=5 -test-sim-multi-seed-long: runsim +test-sim-multi-seed-long: @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 && go test -mod=readonly -timeout=1h -tags='sims' -run TestFullAppSimulation \ + -NumBlocks=500 -Period=50 -test-sim-multi-seed-short: runsim +test-sim-multi-seed-short: @echo "Running short multi-seed application simulation. This may take awhile!" - @cd ${CURRENT_DIR}/simapp && $(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=^$ \ - -Enabled=true -NumBlocks=1000 -BlockSize=200 \ - -Period=1 -Commit=true -Seed=57 -v -timeout 24h + @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -timeout 30m -tags='sims' -run TestFullAppSimulation \ + -NumBlocks=50 -Period=10 .PHONY: \ test-sim-nondeterminism \ @@ -325,17 +321,23 @@ test-sim-import-export \ test-sim-after-import \ test-sim-custom-genesis-multi-seed \ test-sim-multi-seed-short \ -test-sim-multi-seed-long \ -test-sim-benchmark-invariants +test-sim-multi-seed-long SIM_NUM_BLOCKS ?= 500 SIM_BLOCK_SIZE ?= 200 SIM_COMMIT ?= true +#? test-sim-fuzz: Run fuzz test for simapp +test-sim-fuzz: + @echo "Running application fuzz for numBlocks=2, blockSize=20. This may take awhile!" +#ld flags are a quick fix to make it work on current osx + @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -json -tags='sims' -ldflags="-extldflags=-Wl,-ld_classic" -timeout=60m -fuzztime=60m -run=^$$ -fuzz=FuzzFullAppSimulation -GenesisTime=1714720615 -NumBlocks=2 -BlockSize=20 + +#? test-sim-benchmark: Run benchmark test for simapp test-sim-benchmark: @echo "Running application benchmark for numBlocks=$(SIM_NUM_BLOCKS), blockSize=$(SIM_BLOCK_SIZE). This may take awhile!" - @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -run=^$$ $(.) -bench ^BenchmarkFullAppSimulation$$ \ - -Enabled=true -NumBlocks=$(SIM_NUM_BLOCKS) -BlockSize=$(SIM_BLOCK_SIZE) -Commit=$(SIM_COMMIT) -timeout 24h + @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -tags='sims' -run=^$$ $(.) -bench ^BenchmarkFullAppSimulation$$ \ + -NumBlocks=$(SIM_NUM_BLOCKS) -BlockSize=$(SIM_BLOCK_SIZE) -Commit=$(SIM_COMMIT) -Seed=57 -timeout 30m # Requires an exported plugin. See store/streaming/README.md for documentation. # @@ -349,12 +351,12 @@ test-sim-benchmark: test-sim-benchmark-streaming: @echo "Running application benchmark for numBlocks=$(SIM_NUM_BLOCKS), blockSize=$(SIM_BLOCK_SIZE). This may take awhile!" @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -run=^$$ $(.) -bench ^BenchmarkFullAppSimulation$$ \ - -Enabled=true -NumBlocks=$(SIM_NUM_BLOCKS) -BlockSize=$(SIM_BLOCK_SIZE) -Commit=$(SIM_COMMIT) -timeout 24h -EnableStreaming=true + -NumBlocks=$(SIM_NUM_BLOCKS) -BlockSize=$(SIM_BLOCK_SIZE) -Commit=$(SIM_COMMIT) -timeout 24h -EnableStreaming=true test-sim-profile: @echo "Running application benchmark for numBlocks=$(SIM_NUM_BLOCKS), blockSize=$(SIM_BLOCK_SIZE). This may take awhile!" @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -benchmem -run=^$$ $(.) -bench ^BenchmarkFullAppSimulation$$ \ - -Enabled=true -NumBlocks=$(SIM_NUM_BLOCKS) -BlockSize=$(SIM_BLOCK_SIZE) -Commit=$(SIM_COMMIT) -timeout 24h -cpuprofile cpu.out -memprofile mem.out + -NumBlocks=$(SIM_NUM_BLOCKS) -BlockSize=$(SIM_BLOCK_SIZE) -Commit=$(SIM_COMMIT) -timeout 24h -cpuprofile cpu.out -memprofile mem.out # Requires an exported plugin. See store/streaming/README.md for documentation. # @@ -368,9 +370,9 @@ test-sim-profile: test-sim-profile-streaming: @echo "Running application benchmark for numBlocks=$(SIM_NUM_BLOCKS), blockSize=$(SIM_BLOCK_SIZE). This may take awhile!" @cd ${CURRENT_DIR}/simapp && go test -mod=readonly -benchmem -run=^$$ $(.) -bench ^BenchmarkFullAppSimulation$$ \ - -Enabled=true -NumBlocks=$(SIM_NUM_BLOCKS) -BlockSize=$(SIM_BLOCK_SIZE) -Commit=$(SIM_COMMIT) -timeout 24h -cpuprofile cpu.out -memprofile mem.out -EnableStreaming=true + -NumBlocks=$(SIM_NUM_BLOCKS) -BlockSize=$(SIM_BLOCK_SIZE) -Commit=$(SIM_COMMIT) -timeout 24h -cpuprofile cpu.out -memprofile mem.out -EnableStreaming=true -.PHONY: test-sim-profile test-sim-benchmark +.PHONY: test-sim-profile test-sim-benchmark test-sim-fuzz benchmark: @go test -mod=readonly -bench=. $(PACKAGES_NOSIMULATION) diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 02f55de411..e18172d60b 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -1175,3 +1175,8 @@ func (app *BaseApp) Close() error { return errors.Join(errs...) } + +// GetBaseApp returns the pointer to itself. +func (app *BaseApp) GetBaseApp() *BaseApp { + return app +} diff --git a/contrib/devtools/Makefile b/contrib/devtools/Makefile deleted file mode 100644 index e302695bdb..0000000000 --- a/contrib/devtools/Makefile +++ /dev/null @@ -1,69 +0,0 @@ -### -# Find OS and Go environment -# GO contains the Go binary -# FS contains the OS file separator -### -ifeq ($(OS),Windows_NT) - GO := $(shell where go.exe 2> NUL) - FS := "\\" -else - GO := $(shell command -v go 2> /dev/null) - FS := "/" -endif - -ifeq ($(GO),) - $(error could not find go. Is it in PATH? $(GO)) -endif - -############################################################################### -### Functions ### -############################################################################### - -go_get = $(if $(findstring Windows_NT,$(OS)),\ -IF NOT EXIST $(GITHUBDIR)$(FS)$(1)$(FS) ( mkdir $(GITHUBDIR)$(FS)$(1) ) else (cd .) &\ -IF NOT EXIST $(GITHUBDIR)$(FS)$(1)$(FS)$(2)$(FS) ( cd $(GITHUBDIR)$(FS)$(1) && git clone https://github.com/$(1)/$(2) ) else (cd .) &\ -,\ -mkdir -p $(GITHUBDIR)$(FS)$(1) &&\ -(test ! -d $(GITHUBDIR)$(FS)$(1)$(FS)$(2) && cd $(GITHUBDIR)$(FS)$(1) && git clone https://github.com/$(1)/$(2)) || true &&\ -)\ -cd $(GITHUBDIR)$(FS)$(1)$(FS)$(2) && git fetch origin && git checkout -q $(3) - -mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) -mkfile_dir := $(shell cd $(shell dirname $(mkfile_path)); pwd) - - -############################################################################### -### Tools ### -############################################################################### - -PREFIX ?= /usr/local -BIN ?= $(PREFIX)/bin -UNAME_S ?= $(shell uname -s) -UNAME_M ?= $(shell uname -m) - -GOPATH ?= $(shell $(GO) env GOPATH) -GITHUBDIR := $(GOPATH)$(FS)src$(FS)github.com - -BUF_VERSION ?= 0.11.0 - -TOOLS_DESTDIR ?= $(GOPATH)/bin -RUNSIM = $(TOOLS_DESTDIR)/runsim - -tools: tools-stamp -tools-stamp: runsim - # Create dummy file to satisfy dependency and avoid - # rebuilding when this Makefile target is hit twice - # in a row. - touch $@ - -# Install the runsim binary -runsim: $(RUNSIM) -$(RUNSIM): - @echo "Installing runsim..." - @go install github.com/cosmos/tools/cmd/runsim@v1.0.0 - -tools-clean: - rm -f $(GOLANGCI_LINT) $(RUNSIM) - rm -f tools-stamp - -.PHONY: tools-clean runsim \ No newline at end of file diff --git a/scripts/go-lint-all.bash b/scripts/go-lint-all.bash index fcac08335d..44775ada72 100755 --- a/scripts/go-lint-all.bash +++ b/scripts/go-lint-all.bash @@ -2,7 +2,7 @@ set -e -o pipefail -LINT_TAGS="e2e,ledger,test_ledger_mock,system_test" +LINT_TAGS="e2e,ledger,test_ledger_mock,system_test,sims" export LINT_TAGS REPO_ROOT="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd )" diff --git a/scripts/go-lint-changes.bash b/scripts/go-lint-changes.bash index 75ee723d58..4e8a8ecb82 100755 --- a/scripts/go-lint-changes.bash +++ b/scripts/go-lint-changes.bash @@ -3,7 +3,7 @@ set -e -o pipefail -LINT_TAGS="e2e,ledger,test_ledger_mock,system_test" +LINT_TAGS="e2e,ledger,test_ledger_mock,system_test,sims" export LINT_TAGS REPO_ROOT="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd )" diff --git a/simapp/sim_bench_test.go b/simapp/sim_bench_test.go index a0bf7f2fee..d568b52b4e 100644 --- a/simapp/sim_bench_test.go +++ b/simapp/sim_bench_test.go @@ -1,38 +1,35 @@ +//go:build sims + package simapp import ( "os" "testing" - flag "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/stretchr/testify/require" + "cosmossdk.io/log" + "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/testutils/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" ) -var FlagEnableBenchStreamingValue bool - -// Get flags every time the simulator is run -func init() { - flag.BoolVar(&FlagEnableBenchStreamingValue, "EnableStreaming", false, "Enable streaming service") -} - // 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 = SimAppChainID + config.ChainID = sims.SimAppChainID - db, dir, logger, skip, err := simtestutil.SetupSimulation(config, "goleveldb-app-sim", "Simulation", simcli.FlagVerboseValue, simcli.FlagEnabledValue) + db, dir, logger, skip, err := simtestutil.SetupSimulation(config, "goleveldb-app-sim", "Simulation", simcli.FlagVerboseValue, true) if err != nil { b.Fatalf("simulation setup failed: %s", err.Error()) } @@ -47,86 +44,24 @@ func BenchmarkFullAppSimulation(b *testing.B) { }() 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) - app := NewSimApp(logger, db, nil, true, appOptions, interBlockCacheOpt(), baseapp.SetChainID(SimAppChainID)) + app := NewSimApp(logger, db, nil, true, appOptions, interBlockCacheOpt(), baseapp.SetChainID(sims.SimAppChainID)) // run randomized simulation - _, simParams, simErr := simulation.SimulateFromSeed( + simParams, simErr := simulation.SimulateFromSeedX( b, + log.NewNopLogger(), 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 = SimAppChainID - - 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() { - require.NoError(b, db.Close()) - require.NoError(b, os.RemoveAll(dir)) - }() - - appOptions := make(simtestutil.AppOptionsMap, 0) - appOptions[flags.FlagHome] = DefaultNodeHome - appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue - - app := NewSimApp(logger, db, nil, true, appOptions, interBlockCacheOpt(), baseapp.SetChainID(SimAppChainID)) - - // 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), + simtypes.RandomAccounts, + simtestutil.BuildSimulationOperations(app, app.AppCodec(), config, app.txConfig), BlockedAddresses(), config, app.AppCodec(), + &simulation.DummyLogWriter{}, ) // export state and simParams before the simulation error is checked diff --git a/simapp/sim_test.go b/simapp/sim_test.go index bacee9cde7..3a08fb16e7 100644 --- a/simapp/sim_test.go +++ b/simapp/sim_test.go @@ -1,19 +1,21 @@ +//go:build sims + package simapp import ( + "encoding/binary" "encoding/json" "flag" - "fmt" + "io" "math/rand" - "os" - "runtime/debug" "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/spf13/viper" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "cosmossdk.io/log" @@ -22,9 +24,10 @@ import ( "cosmossdk.io/x/feegrant" "github.com/cosmos/cosmos-sdk/baseapp" - "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/server" + servertypes "github.com/cosmos/cosmos-sdk/server/types" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/cosmos-sdk/testutils/sims" + 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/simulation" @@ -33,9 +36,6 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) -// SimAppChainID hardcoded chainID for simulation -const SimAppChainID = "simulation-app" - var FlagEnableStreamingValue bool // Get flags every time the simulator is run @@ -44,12 +44,6 @@ func init() { flag.BoolVar(&FlagEnableStreamingValue, "EnableStreaming", false, "Enable streaming service") } -// 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) { @@ -57,150 +51,188 @@ func interBlockCacheOpt() func(*baseapp.BaseApp) { } func TestFullAppSimulation(t *testing.T) { - config := simcli.NewConfigFromFlags() - config.ChainID = SimAppChainID + sims.Run(t, NewSimApp, setupStateFactory) +} - 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 := NewSimApp(logger, db, nil, true, appOptions, fauxMerkleModeOpt, baseapp.SetChainID(SimAppChainID)) - if !simcli.FlagSigverifyTxValue { - app.SetNotSigverifyTx() - } - require.Equal(t, "SimApp", 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 setupStateFactory(app *SimApp) sims.SimStateFactory { + return sims.SimStateFactory{ + Codec: app.AppCodec(), + AppStateFn: simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + BlockedAddr: BlockedAddresses(), } } +var ( + exportAllModules = []string{} + exportWithValidatorSet = []string{} +) + func TestAppImportExport(t *testing.T) { - config := simcli.NewConfigFromFlags() - config.ChainID = SimAppChainID + sims.Run(t, NewSimApp, setupStateFactory, func(t *testing.T, ti sims.TestInstance[*SimApp]) { + t.Helper() + app := ti.App + t.Log("exporting genesis...\n") + exported, err := app.ExportAppStateAndValidators(false, exportWithValidatorSet, exportAllModules) + require.NoError(t, err) - 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 := NewSimApp(logger, db, nil, true, appOptions, fauxMerkleModeOpt, baseapp.SetChainID(SimAppChainID)) - if !simcli.FlagSigverifyTxValue { - app.SetNotSigverifyTx() - } - require.Equal(t, "SimApp", 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 := NewSimApp(log.NewNopLogger(), newDB, nil, true, appOptions, fauxMerkleModeOpt, baseapp.SetChainID(SimAppChainID)) - require.Equal(t, "SimApp", 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())) + t.Log("importing genesis...\n") + newTestInstance := sims.NewSimulationAppInstance(t, ti.Cfg, NewSimApp) + newApp := newTestInstance.App + var genesisState GenesisState + require.NoError(t, 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) { + t.Skip("Skipping simulation as all validators have been unbonded") return } - } + require.NoError(t, err) + err = newApp.StoreConsensusParams(ctxB, exported.ConsensusParams) + require.NoError(t, err) - require.NoError(t, err) - err = newApp.StoreConsensusParams(ctxB, exported.ConsensusParams) - require.NoError(t, err) - fmt.Printf("comparing stores...\n") + t.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(t, app, newApp, app.SimulationManager().StoreDecoders, skipPrefixes) + }) +} - // 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}, +// 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(t *testing.T, ti sims.TestInstance[*SimApp]) { + t.Helper() + app := ti.App + t.Log("exporting genesis...\n") + exported, err := app.ExportAppStateAndValidators(false, exportWithValidatorSet, exportAllModules) + require.NoError(t, err) + + t.Log("importing genesis...\n") + newTestInstance := sims.NewSimulationAppInstance(t, ti.Cfg, NewSimApp) + newApp := newTestInstance.App + _, err = newApp.InitChain(&abci.RequestInitChain{ + AppStateBytes: exported.AppState, + ChainId: sims.SimAppChainID, + }) + if IsEmptyValidatorSetErr(err) { + t.Skip("Skipping simulation as all validators have been unbonded") + return + } + require.NoError(t, err) + newStateFactory := setupStateFactory(newApp) + _, err = simulation.SimulateFromSeedX( + t, + 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(t, 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 = sims.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(t *testing.T, ti sims.TestInstance[*SimApp]) { + t.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(t, otherHashes[i], appHash) { + execWriters[i].PrintLogs() + failNow = true + } + } + if failNow { + ti.ExecLogWriter.PrintLogs() + t.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(t *testing.T, app, newApp ComparableStoreApp, storeDecoders simtypes.StoreDecoderRegistry, skipPrefixes map[string][][]byte) { + t.Helper() + ctxA := app.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()}) + ctxB := newApp.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()}) storeKeys := app.GetStoreKeys() require.NotEmpty(t, storeKeys) @@ -218,195 +250,30 @@ func TestAppImportExport(t *testing.T) { 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) + require.Equal(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare %s, key stores %s and %s", keyName, appKeyA, appKeyB) - 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)) + t.Logf("compared %d different key/value pairs between %s and %s\n", len(failedKVAs), appKeyA, appKeyB) + if !assert.Equal(t, 0, len(failedKVAs), simtestutil.GetSimulationLog(keyName, storeDecoders, failedKVAs, failedKVBs)) { + for _, v := range failedKVAs { + t.Logf("store mismatch: %q\n", v) + } + t.FailNow() + } } } -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 := NewSimApp(logger, db, nil, true, appOptions, fauxMerkleModeOpt, baseapp.SetChainID(SimAppChainID)) - if !simcli.FlagSigverifyTxValue { - app.SetNotSigverifyTx() - } - require.Equal(t, "SimApp", 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 := NewSimApp(log.NewNopLogger(), newDB, nil, true, appOptions, fauxMerkleModeOpt, baseapp.SetChainID(SimAppChainID)) - require.Equal(t, "SimApp", newApp.Name()) - - _, err = newApp.InitChain(&abci.RequestInitChain{ - AppStateBytes: exported.AppState, - ChainId: SimAppChainID, +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:], + ) }) - 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) -} - -// 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 = 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 := NewSimApp(logger, db, nil, true, appOptions, interBlockCacheOpt(), baseapp.SetChainID(SimAppChainID)) - if !simcli.FlagSigverifyTxValue { - app.SetNotSigverifyTx() - } - - 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/simapp/test_helpers.go b/simapp/test_helpers.go index 7043f2eba3..ff409f9394 100644 --- a/simapp/test_helpers.go +++ b/simapp/test_helpers.go @@ -19,7 +19,6 @@ import ( bam "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" - "github.com/cosmos/cosmos-sdk/server" servertypes "github.com/cosmos/cosmos-sdk/server/types" "github.com/cosmos/cosmos-sdk/testutil/mock" "github.com/cosmos/cosmos-sdk/testutil/network" @@ -43,7 +42,6 @@ func setup(withGenesis bool, invCheckPeriod uint) (*SimApp, GenesisState) { appOptions := make(simtestutil.AppOptionsMap, 0) appOptions[flags.FlagHome] = DefaultNodeHome - appOptions[server.FlagInvCheckPeriod] = invCheckPeriod app := NewSimApp(log.NewNopLogger(), db, nil, true, appOptions) if withGenesis { diff --git a/testutil/sims/simulation_helpers.go b/testutil/sims/simulation_helpers.go index eee7bb74a5..cf432a6c40 100644 --- a/testutil/sims/simulation_helpers.go +++ b/testutil/sims/simulation_helpers.go @@ -12,6 +12,7 @@ import ( "cosmossdk.io/log" storetypes "cosmossdk.io/store/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/runtime" sdk "github.com/cosmos/cosmos-sdk/types" @@ -23,7 +24,7 @@ import ( // SetupSimulation creates the config, db (levelDB), temporary directory and logger for the simulation tests. // If `skip` is false it skips the current test. `skip` should be set using the `FlagEnabledValue` flag. -// Returns error on an invalid db intantiation or temp dir creation. +// Returns error on an invalid db instantiation or temp dir creation. func SetupSimulation(config simtypes.Config, dirPrefix, dbName string, verbose, skip bool) (dbm.DB, string, log.Logger, bool, error) { if !skip { return nil, "", nil, true, nil @@ -31,7 +32,7 @@ func SetupSimulation(config simtypes.Config, dirPrefix, dbName string, verbose, var logger log.Logger if verbose { - logger = log.NewLogger(os.Stdout) // TODO(mr): enable selection of log destination. + logger = log.NewLogger(os.Stdout) } else { logger = log.NewNopLogger() } @@ -51,11 +52,18 @@ func SetupSimulation(config simtypes.Config, dirPrefix, dbName string, verbose, // SimulationOperations retrieves the simulation params from the provided file path // and returns all the modules weighted operations +// Deprecated: use BuildSimulationOperations with TxConfig func SimulationOperations(app runtime.AppI, cdc codec.JSONCodec, config simtypes.Config) []simtypes.WeightedOperation { + return BuildSimulationOperations(app, cdc, config, moduletestutil.MakeTestTxConfig()) +} + +// BuildSimulationOperations retrieves the simulation params from the provided file path +// and returns all the modules weighted operations +func BuildSimulationOperations(app runtime.AppI, cdc codec.JSONCodec, config simtypes.Config, txConfig client.TxConfig) []simtypes.WeightedOperation { simState := module.SimulationState{ AppParams: make(simtypes.AppParams), Cdc: cdc, - TxConfig: moduletestutil.MakeTestTxConfig(), + TxConfig: txConfig, BondDenom: sdk.DefaultBondDenom, } @@ -71,8 +79,7 @@ func SimulationOperations(app runtime.AppI, cdc codec.JSONCodec, config simtypes } } - //nolint:staticcheck // used for legacy testing - simState.LegacyProposalContents = app.SimulationManager().GetProposalContents(simState) + simState.LegacyProposalContents = app.SimulationManager().GetProposalContents(simState) //nolint:staticcheck // we're testing the old way here simState.ProposalMsgs = app.SimulationManager().GetProposalMsgs(simState) return app.SimulationManager().WeightedOperations(simState) } diff --git a/testutil/sims/state_helpers.go b/testutil/sims/state_helpers.go index d1a320e983..6605be4eea 100644 --- a/testutil/sims/state_helpers.go +++ b/testutil/sims/state_helpers.go @@ -17,6 +17,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -35,7 +36,11 @@ const ( // AppStateFn returns the initial application state using a genesis or the simulation parameters. // It calls AppStateFnWithExtendedCb with nil rawStateCb. -func AppStateFn(cdc codec.JSONCodec, simManager *module.SimulationManager, genesisState map[string]json.RawMessage) simtypes.AppStateFn { +func AppStateFn( + cdc codec.JSONCodec, + simManager *module.SimulationManager, + genesisState map[string]json.RawMessage, +) simtypes.AppStateFn { return AppStateFnWithExtendedCb(cdc, simManager, genesisState, nil) } @@ -68,13 +73,9 @@ func AppStateFnWithExtendedCbs( accs []simtypes.Account, config simtypes.Config, ) (appState json.RawMessage, simAccs []simtypes.Account, chainID string, genesisTimestamp time.Time) { - if simcli.FlagGenesisTimeValue == 0 { - genesisTimestamp = simtypes.RandTimestamp(r) - } else { - genesisTimestamp = time.Unix(simcli.FlagGenesisTimeValue, 0) - } - + genesisTimestamp = time.Unix(config.GenesisTime, 0) chainID = config.ChainID + switch { case config.ParamsFile != "" && config.GenesisFile != "": panic("cannot provide both a genesis file and a params file") @@ -138,8 +139,7 @@ func AppStateFnWithExtendedCbs( } notBondedCoins := sdk.NewCoin(stakingState.Params.BondDenom, notBondedTokens) // edit bank state to make it have the not bonded pool tokens - bankStateBz, ok := rawState[banktypes.ModuleName] - // TODO(fdymylja/jonathan): should we panic in this case + bankStateBz, ok := rawState[testutil.BankModuleName] if !ok { panic("bank genesis state is missing") } @@ -166,7 +166,7 @@ func AppStateFnWithExtendedCbs( // change appState back for name, state := range map[string]proto.Message{ stakingtypes.ModuleName: stakingState, - banktypes.ModuleName: bankState, + testutil.BankModuleName: bankState, } { if moduleStateCb != nil { moduleStateCb(name, state) @@ -219,15 +219,6 @@ func AppStateRandomizedFn( numInitiallyBonded = numAccs } - fmt.Printf( - `Selected randomly generated parameters for simulated genesis: -{ - stake_per_account: "%d", - initially_bonded_validators: "%d" -} -`, initialStake.Uint64(), numInitiallyBonded, - ) - simState := &module.SimulationState{ AppParams: appParams, Cdc: cdc, @@ -273,8 +264,8 @@ func AppStateFromGenesisFileFn(r io.Reader, cdc codec.JSONCodec, genesisFile str } var authGenesis authtypes.GenesisState - if appState[authtypes.ModuleName] != nil { - cdc.MustUnmarshalJSON(appState[authtypes.ModuleName], &authGenesis) + if appState[testutil.AuthModuleName] != nil { + cdc.MustUnmarshalJSON(appState[testutil.AuthModuleName], &authGenesis) } newAccs := make([]simtypes.Account, len(authGenesis.Accounts)) diff --git a/testutils/sims/runner.go b/testutils/sims/runner.go new file mode 100644 index 0000000000..b8bf9101f4 --- /dev/null +++ b/testutils/sims/runner.go @@ -0,0 +1,235 @@ +package sims + +import ( + "fmt" + "io" + "path/filepath" + "strings" + "testing" + + dbm "github.com/cosmos/cosmos-db" + "github.com/stretchr/testify/require" + + "cosmossdk.io/log" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/runtime" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" +) + +const SimAppChainID = "simulation-app" + +// this list of seeds was imported from the original simulation runner: https://github.com/cosmos/tools/blob/v1.0.0/cmd/runsim/main.go#L32 +var defaultSeeds = []int64{ + 1, 2, 4, 7, + 32, 123, 124, 582, 1893, 2989, + 3012, 4728, 37827, 981928, 87821, 891823782, + 989182, 89182391, 11, 22, 44, 77, 99, 2020, + 3232, 123123, 124124, 582582, 18931893, + 29892989, 30123012, 47284728, 7601778, 8090485, + 977367484, 491163361, 424254581, 673398983, +} + +type SimStateFactory struct { + Codec codec.Codec + AppStateFn simtypes.AppStateFn + BlockedAddr map[string]bool +} + +// SimulationApp abstract app that is used by sims +type SimulationApp interface { + runtime.AppI + SetNotSigverifyTx() + GetBaseApp() *baseapp.BaseApp + TxConfig() client.TxConfig +} + +// Run is a helper function that runs a simulation test with the given parameters. +// It calls the RunWithSeeds function with the default seeds and parameters. +// +// This is the entrypoint to run simulation tests that used to run with the runsim binary. +func Run[T SimulationApp]( + t *testing.T, + appFactory func( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + loadLatest bool, + appOpts servertypes.AppOptions, + baseAppOptions ...func(*baseapp.BaseApp), + ) T, + setupStateFactory func(app T) SimStateFactory, + postRunActions ...func(t *testing.T, app TestInstance[T]), +) { + t.Helper() + RunWithSeeds(t, appFactory, setupStateFactory, defaultSeeds, nil, postRunActions...) +} + +// RunWithSeeds is a helper function that runs a simulation test with the given parameters. +// It iterates over the provided seeds and runs the simulation test for each seed in parallel. +// +// It sets up the environment, creates an instance of the simulation app, +// calls the simulation.SimulateFromSeed function to run the simulation, and performs post-run actions for each seed. +// The execution is deterministic and can be used for fuzz tests as well. +// +// The system under test is isolated for each run but unlike the old runsim command, there is no Process separation. +// This means, global caches may be reused for example. This implementation build upon the vanialla Go stdlib test framework. +func RunWithSeeds[T SimulationApp]( + t *testing.T, + appFactory func( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + loadLatest bool, + appOpts servertypes.AppOptions, + baseAppOptions ...func(*baseapp.BaseApp), + ) T, + setupStateFactory func(app T) SimStateFactory, + seeds []int64, + fuzzSeed []byte, + postRunActions ...func(t *testing.T, app TestInstance[T]), +) { + t.Helper() + cfg := cli.NewConfigFromFlags() + cfg.ChainID = SimAppChainID + if deprecatedParams := cli.GetDeprecatedFlagUsed(); len(deprecatedParams) != 0 { + fmt.Printf("Warning: Deprecated flag are used: %s", strings.Join(deprecatedParams, ",")) + } + for i := range seeds { + seed := seeds[i] + t.Run(fmt.Sprintf("seed: %d", seed), func(t *testing.T) { + t.Parallel() + // setup environment + tCfg := cfg.With(t, seed, fuzzSeed) + testInstance := NewSimulationAppInstance(t, tCfg, appFactory) + var runLogger log.Logger + if cli.FlagVerboseValue { + runLogger = log.NewTestLogger(t) + } else { + runLogger = log.NewTestLoggerInfo(t) + } + runLogger = runLogger.With("seed", tCfg.Seed) + + app := testInstance.App + stateFactory := setupStateFactory(app) + simParams, err := simulation.SimulateFromSeedX( + t, + runLogger, + WriteToDebugLog(runLogger), + app.GetBaseApp(), + stateFactory.AppStateFn, + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simtestutil.BuildSimulationOperations(app, stateFactory.Codec, tCfg, testInstance.App.TxConfig()), + stateFactory.BlockedAddr, + tCfg, + stateFactory.Codec, + testInstance.ExecLogWriter, + ) + require.NoError(t, err) + err = simtestutil.CheckExportSimulation(app, tCfg, simParams) + require.NoError(t, err) + if tCfg.Commit { + simtestutil.PrintStats(testInstance.DB) + } + for _, step := range postRunActions { + step(t, testInstance) + } + }) + } +} + +// TestInstance is a generic type that represents an instance of a SimulationApp used for testing simulations. +// It contains the following fields: +// - App: The instance of the SimulationApp under test. +// - DB: The LevelDB database for the simulation app. +// - WorkDir: The temporary working directory for the simulation app. +// - Cfg: The configuration flags for the simulator. +// - AppLogger: The logger used for logging in the app during the simulation, with seed value attached. +// - ExecLogWriter: Captures block and operation data coming from the simulation +type TestInstance[T SimulationApp] struct { + App T + DB dbm.DB + WorkDir string + Cfg simtypes.Config + AppLogger log.Logger + ExecLogWriter simulation.LogWriter +} + +// NewSimulationAppInstance initializes and returns a TestInstance of a SimulationApp. +// The function takes a testing.T instance, a simtypes.Config instance, and an appFactory function as parameters. +// It creates a temporary working directory and a LevelDB database for the simulation app. +// The function then initializes a logger based on the verbosity flag and sets the logger's seed to the test configuration's seed. +// The database is closed and cleaned up on test completion. +func NewSimulationAppInstance[T SimulationApp]( + t *testing.T, + tCfg simtypes.Config, + appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T, +) TestInstance[T] { + t.Helper() + workDir := t.TempDir() + dbDir := filepath.Join(workDir, "leveldb-app-sim") + var logger log.Logger + if cli.FlagVerboseValue { + logger = log.NewTestLogger(t) + } else { + logger = log.NewTestLoggerError(t) + } + logger = logger.With("seed", tCfg.Seed) + + db, err := dbm.NewDB("Simulation", dbm.BackendType(tCfg.DBBackend), dbDir) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + appOptions := make(simtestutil.AppOptionsMap) + appOptions[flags.FlagHome] = workDir + + app := appFactory(logger, db, nil, true, appOptions, baseapp.SetChainID(SimAppChainID)) + if !cli.FlagSigverifyTxValue { + app.SetNotSigverifyTx() + } + return TestInstance[T]{ + App: app, + DB: db, + WorkDir: workDir, + Cfg: tCfg, + AppLogger: logger, + ExecLogWriter: &simulation.StandardLogWriter{Seed: tCfg.Seed}, + } +} + +var _ io.Writer = writerFn(nil) + +type writerFn func(p []byte) (n int, err error) + +func (w writerFn) Write(p []byte) (n int, err error) { + return w(p) +} + +// WriteToDebugLog is an adapter to io.Writer interface +func WriteToDebugLog(logger log.Logger) io.Writer { + return writerFn(func(p []byte) (n int, err error) { + logger.Debug(string(p)) + return len(p), nil + }) +} + +// 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() +} diff --git a/types/simulation/account.go b/types/simulation/account.go index b9f69ffa61..67b57d2581 100644 --- a/types/simulation/account.go +++ b/types/simulation/account.go @@ -31,22 +31,32 @@ func RandomAcc(r *rand.Rand, accs []Account) (Account, int) { return accs[idx], idx } -// RandomAccounts generates n random accounts +// RandomAccounts deterministically generates n random accounts without duplicates. func RandomAccounts(r *rand.Rand, n int) []Account { accs := make([]Account, n) - - for i := 0; i < n; i++ { + idx := make(map[string]struct{}, n) + var i int + for i < n { // don't need that much entropy for simulation privkeySeed := make([]byte, 15) - r.Read(privkeySeed) - - accs[i].PrivKey = secp256k1.GenPrivKeyFromSecret(privkeySeed) - accs[i].PubKey = accs[i].PrivKey.PubKey() - accs[i].Address = sdk.AccAddress(accs[i].PubKey.Address()) - - accs[i].ConsKey = ed25519.GenPrivKeyFromSecret(privkeySeed) + if _, err := r.Read(privkeySeed); err != nil { + panic(err) + } + privKey := secp256k1.GenPrivKeyFromSecret(privkeySeed) + pubKey := privKey.PubKey() + addr := sdk.AccAddress(pubKey.Address()) + if _, exists := idx[string(addr.Bytes())]; exists { + continue + } + idx[string(addr.Bytes())] = struct{}{} + accs[i] = Account{ + Address: addr, + PrivKey: privKey, + PubKey: pubKey, + ConsKey: ed25519.GenPrivKeyFromSecret(privkeySeed), + } + i++ } - return accs } diff --git a/types/simulation/config.go b/types/simulation/config.go index 4ed9530a92..1e385fdfa1 100644 --- a/types/simulation/config.go +++ b/types/simulation/config.go @@ -1,5 +1,7 @@ package simulation +import "testing" + // Config contains the necessary configuration flags for the simulator type Config struct { GenesisFile string // custom simulation genesis file; cannot be used with params file @@ -12,6 +14,7 @@ type Config struct { Seed int64 // simulation random seed InitialBlockHeight int // initial block to start the simulation + GenesisTime int64 // genesis time to start the simulation NumBlocks int // number of new blocks to simulate from the initial block height BlockSize int // operations per block ChainID string // chain-id used on the simulation @@ -19,9 +22,28 @@ type Config struct { Lean bool // lean simulation log output Commit bool // have the simulation commit - OnOperation bool // run slow invariants every operation - AllInvariants bool // print all failed invariants if a broken invariant is found - DBBackend string // custom db backend type BlockMaxGas int64 // custom max gas for block + FuzzSeed []byte + TB testing.TB + FauxMerkle bool + + // Deprecated: unused and will be removed + OnOperation bool // run slow invariants every operation + // Deprecated: unused and will be removed + AllInvariants bool // print all failed invariants if a broken invariant is found +} + +func (c Config) shallowCopy() Config { + return c +} + +// With sets the values of t, seed, and fuzzSeed in a copy of the Config and returns the copy. +func (c Config) With(tb testing.TB, seed int64, fuzzSeed []byte) Config { + tb.Helper() + r := c.shallowCopy() + r.TB = tb + r.Seed = seed + r.FuzzSeed = fuzzSeed + return r } diff --git a/types/simulation/rand_util.go b/types/simulation/rand_util.go index adacd90ad4..2dd39ead89 100644 --- a/types/simulation/rand_util.go +++ b/types/simulation/rand_util.go @@ -147,31 +147,8 @@ func RandSubsetCoins(r *rand.Rand, coins sdk.Coins) sdk.Coins { } // DeriveRand derives a new Rand deterministically from another random source. -// Unlike rand.New(rand.NewSource(seed)), the result is "more random" -// depending on the source and state of r. // // NOTE: not crypto safe. func DeriveRand(r *rand.Rand) *rand.Rand { - const num = 8 // TODO what's a good number? Too large is too slow. - ms := multiSource(make([]rand.Source, num)) - - for i := 0; i < num; i++ { - ms[i] = rand.NewSource(r.Int63()) - } - - return rand.New(ms) -} - -type multiSource []rand.Source - -func (ms multiSource) Int63() (r int64) { - for _, source := range ms { - r ^= source.Int63() - } - - return r -} - -func (ms multiSource) Seed(_ int64) { - panic("multiSource Seed should not be called") + return rand.New(rand.NewSource(r.Int63())) } diff --git a/x/gov/simulation/operations.go b/x/gov/simulation/operations.go index 013e89fd52..9e173f6224 100644 --- a/x/gov/simulation/operations.go +++ b/x/gov/simulation/operations.go @@ -3,6 +3,7 @@ package simulation import ( "math" "math/rand" + "sync/atomic" "time" sdkmath "cosmossdk.io/math" @@ -18,7 +19,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/simulation" ) -var initialProposalID = uint64(100000000000000) +const unsetProposalID = 100000000000000 // Governance message types and routes var ( @@ -43,6 +44,26 @@ const ( DefaultWeightMsgCancelProposal = 5 ) +// sharedState shared state between message invocations +type sharedState struct { + minProposalID atomic.Uint64 +} + +// newSharedState constructor +func newSharedState() *sharedState { + r := &sharedState{} + r.setMinProposalID(unsetProposalID) + return r +} + +func (s *sharedState) getMinProposalID() uint64 { + return s.minProposalID.Load() +} + +func (s *sharedState) setMinProposalID(id uint64) { + s.minProposalID.Store(id) +} + // WeightedOperations returns all the operations from the module with their respective weights func WeightedOperations( appParams simtypes.AppParams, @@ -119,19 +140,19 @@ func WeightedOperations( ), ) } - + state := newSharedState() wGovOps := simulation.WeightedOperations{ simulation.NewWeightedOperation( weightMsgDeposit, - SimulateMsgDeposit(txGen, ak, bk, k), + simulateMsgDeposit(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgVote, - SimulateMsgVote(txGen, ak, bk, k), + simulateMsgVote(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgVoteWeighted, - SimulateMsgVoteWeighted(txGen, ak, bk, k), + simulateMsgVoteWeighted(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgCancelProposal, @@ -205,7 +226,7 @@ func simulateMsgSubmitProposal( // column 3: 75% vote // column 4: 40% vote // column 5: 15% vote - // column 6: noone votes + // column 6: no one votes // All columns sum to 100 for simplicity, values chosen by @valardragon semi-arbitrarily, // feel free to change. numVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ @@ -291,13 +312,13 @@ func simulateMsgSubmitProposal( whoVotes = whoVotes[:numVotes] params, _ := k.Params.Get(ctx) votingPeriod := params.VotingPeriod - + s := newSharedState() fops := make([]simtypes.FutureOperation, numVotes+1) for i := 0; i < numVotes; i++ { whenVote := ctx.BlockHeader().Time.Add(time.Duration(r.Int63n(int64(votingPeriod.Seconds()))) * time.Second) fops[i] = simtypes.FutureOperation{ BlockTime: whenVote, - Op: operationSimulateMsgVote(txGen, ak, bk, k, accs[whoVotes[i]], int64(proposalID)), + Op: operationSimulateMsgVote(txGen, ak, bk, k, accs[whoVotes[i]], int64(proposalID), s), } } @@ -306,18 +327,29 @@ func simulateMsgSubmitProposal( } // SimulateMsgDeposit generates a MsgDeposit with random values. +// Deprecated: this is an internal method and will be removed func SimulateMsgDeposit( txGen client.TxConfig, ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, +) simtypes.Operation { + return simulateMsgDeposit(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgDeposit( + txGen client.TxConfig, + ak types.AccountKeeper, + bk types.BankKeeper, + k *keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { simAccount, _ := simtypes.RandomAcc(r, accs) - proposalID, ok := randomProposalID(r, k, ctx, v1.StatusDepositPeriod) + proposalID, ok := randomProposalID(r, k, ctx, v1.StatusDepositPeriod, s) if !ok { return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "unable to generate proposalID"), nil, nil } @@ -366,13 +398,24 @@ func SimulateMsgDeposit( } // SimulateMsgVote generates a MsgVote with random values. +// Deprecated: this is an internal method and will be removed func SimulateMsgVote( txGen client.TxConfig, ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, ) simtypes.Operation { - return operationSimulateMsgVote(txGen, ak, bk, k, simtypes.Account{}, -1) + return simulateMsgVote(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgVote( + txGen client.TxConfig, + ak types.AccountKeeper, + bk types.BankKeeper, + k *keeper.Keeper, + s *sharedState, +) simtypes.Operation { + return operationSimulateMsgVote(txGen, ak, bk, k, simtypes.Account{}, -1, s) } func operationSimulateMsgVote( @@ -382,6 +425,7 @@ func operationSimulateMsgVote( k *keeper.Keeper, simAccount simtypes.Account, proposalIDInt int64, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, @@ -396,7 +440,7 @@ func operationSimulateMsgVote( switch { case proposalIDInt < 0: var ok bool - proposalID, ok = randomProposalID(r, k, ctx, v1.StatusVotingPeriod) + proposalID, ok = randomProposalID(r, k, ctx, v1.StatusVotingPeriod, s) if !ok { return simtypes.NoOpMsg(types.ModuleName, TypeMsgVote, "unable to generate proposalID"), nil, nil } @@ -435,7 +479,17 @@ func SimulateMsgVoteWeighted( bk types.BankKeeper, k *keeper.Keeper, ) simtypes.Operation { - return operationSimulateMsgVoteWeighted(txGen, ak, bk, k, simtypes.Account{}, -1) + return simulateMsgVoteWeighted(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgVoteWeighted( + txGen client.TxConfig, + ak types.AccountKeeper, + bk types.BankKeeper, + k *keeper.Keeper, + s *sharedState, +) simtypes.Operation { + return operationSimulateMsgVoteWeighted(txGen, ak, bk, k, simtypes.Account{}, -1, s) } func operationSimulateMsgVoteWeighted( @@ -445,6 +499,7 @@ func operationSimulateMsgVoteWeighted( k *keeper.Keeper, simAccount simtypes.Account, proposalIDInt int64, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, @@ -459,7 +514,7 @@ func operationSimulateMsgVoteWeighted( switch { case proposalIDInt < 0: var ok bool - proposalID, ok = randomProposalID(r, k, ctx, v1.StatusVotingPeriod) + proposalID, ok = randomProposalID(r, k, ctx, v1.StatusVotingPeriod, s) if !ok { return simtypes.NoOpMsg(types.ModuleName, TypeMsgVoteWeighted, "unable to generate proposalID"), nil, nil } @@ -626,20 +681,13 @@ func randomProposal(r *rand.Rand, k *keeper.Keeper, ctx sdk.Context) *v1.Proposa // (defined in gov GenesisState) and the latest proposal ID // that matches a given Status. // It does not provide a default ID. -func randomProposalID(r *rand.Rand, k *keeper.Keeper, ctx sdk.Context, status v1.ProposalStatus) (proposalID uint64, found bool) { +func randomProposalID(r *rand.Rand, k *keeper.Keeper, ctx sdk.Context, status v1.ProposalStatus, s *sharedState) (proposalID uint64, found bool) { proposalID, _ = k.ProposalID.Peek(ctx) - - switch { - case proposalID > initialProposalID: - // select a random ID between [initialProposalID, proposalID] + if initialProposalID := s.getMinProposalID(); initialProposalID == unsetProposalID { + s.setMinProposalID(proposalID) + } else if initialProposalID < proposalID { proposalID = uint64(simtypes.RandIntBetween(r, int(initialProposalID), int(proposalID))) - - default: - // This is called on the first call to this funcion - // in order to update the global variable - initialProposalID = proposalID } - proposal, err := k.Proposals.Get(ctx, proposalID) if err != nil || proposal.Status != status { return proposalID, false diff --git a/x/gov/simulation/operations_test.go b/x/gov/simulation/operations_test.go index ea88bca877..f39b2490fe 100644 --- a/x/gov/simulation/operations_test.go +++ b/x/gov/simulation/operations_test.go @@ -325,13 +325,13 @@ func TestSimulateMsgVote(t *testing.T) { proposal, err := v1.NewProposal([]sdk.Msg{contentMsg}, 1, submitTime, submitTime.Add(*depositPeriod), "", "text proposal", "description", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r"), false) require.NoError(t, err) - suite.GovKeeper.ActivateVotingPeriod(ctx, proposal) + require.NoError(t, suite.GovKeeper.ActivateVotingPeriod(ctx, proposal)) - app.FinalizeBlock(&abci.RequestFinalizeBlock{ + _, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{ Height: app.LastBlockHeight() + 1, Hash: app.LastCommitID().Hash, }) - + require.NoError(t, err) // execute operation op := simulation.SimulateMsgVote(suite.TxConfig, suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper) operationMsg, _, err := op(r, app.BaseApp, ctx, accounts, "") diff --git a/x/group/simulation/operations.go b/x/group/simulation/operations.go index e7ec1911e5..39beb76d00 100644 --- a/x/group/simulation/operations.go +++ b/x/group/simulation/operations.go @@ -5,6 +5,7 @@ import ( "fmt" "math/rand" "strings" + "sync/atomic" "time" "github.com/cosmos/cosmos-sdk/baseapp" @@ -19,7 +20,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/simulation" ) -var initialGroupID = uint64(100000000000000) +const unsetGroupID = 100000000000000 // group message types var ( @@ -76,12 +77,32 @@ const ( WeightMsgCreateGroupWithPolicy = 50 ) +// sharedState shared state between message invocations +type sharedState struct { + minGroupID atomic.Uint64 +} + +// newSharedState constructor +func newSharedState() *sharedState { + r := &sharedState{} + r.setMinGroupID(unsetGroupID) + return r +} + +func (s *sharedState) getMinGroupID() uint64 { + return s.minGroupID.Load() +} + +func (s *sharedState) setMinGroupID(id uint64) { + s.minGroupID.Store(id) +} + // WeightedOperations returns all the operations from the module with their respective weights func WeightedOperations( registry cdctypes.InterfaceRegistry, - appParams simtypes.AppParams, cdc codec.JSONCodec, txGen client.TxConfig, + appParams simtypes.AppParams, _ codec.JSONCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, - appCdc cdctypes.AnyUnpacker, + _ cdctypes.AnyUnpacker, ) simulation.WeightedOperations { var ( weightMsgCreateGroup int @@ -145,12 +166,14 @@ func WeightedOperations( pCdc := codec.NewProtoCodec(registry) + state := newSharedState() + // create two proposals for weightedOperations var createProposalOps simulation.WeightedOperations for i := 0; i < 2; i++ { createProposalOps = append(createProposalOps, simulation.NewWeightedOperation( weightMsgSubmitProposal, - SimulateMsgSubmitProposal(pCdc, txGen, ak, bk, k), + simulateMsgSubmitProposal(pCdc, txGen, ak, bk, k, state), )) } @@ -161,7 +184,7 @@ func WeightedOperations( ), simulation.NewWeightedOperation( weightMsgCreateGroupPolicy, - SimulateMsgCreateGroupPolicy(pCdc, txGen, ak, bk, k), + simulateMsgCreateGroupPolicy(pCdc, txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgCreateGroupWithPolicy, @@ -172,43 +195,43 @@ func WeightedOperations( wPostCreateProposalOps := simulation.WeightedOperations{ simulation.NewWeightedOperation( WeightMsgWithdrawProposal, - SimulateMsgWithdrawProposal(pCdc, txGen, ak, bk, k), + simulateMsgWithdrawProposal(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgVote, - SimulateMsgVote(pCdc, txGen, ak, bk, k), + simulateMsgVote(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgExec, - SimulateMsgExec(pCdc, txGen, ak, bk, k), + simulateMsgExec(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgUpdateGroupMetadata, - SimulateMsgUpdateGroupMetadata(pCdc, txGen, ak, bk, k), + simulateMsgUpdateGroupMetadata(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgUpdateGroupAdmin, - SimulateMsgUpdateGroupAdmin(pCdc, txGen, ak, bk, k), + simulateMsgUpdateGroupAdmin(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgUpdateGroupMembers, - SimulateMsgUpdateGroupMembers(pCdc, txGen, ak, bk, k), + simulateMsgUpdateGroupMembers(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgUpdateGroupPolicyAdmin, - SimulateMsgUpdateGroupPolicyAdmin(pCdc, txGen, ak, bk, k), + simulateMsgUpdateGroupPolicyAdmin(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgUpdateGroupPolicyDecisionPolicy, - SimulateMsgUpdateGroupPolicyDecisionPolicy(pCdc, txGen, ak, bk, k), + simulateMsgUpdateGroupPolicyDecisionPolicy(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgUpdateGroupPolicyMetadata, - SimulateMsgUpdateGroupPolicyMetadata(pCdc, txGen, ak, bk, k), + simulateMsgUpdateGroupPolicyMetadata(txGen, ak, bk, k, state), ), simulation.NewWeightedOperation( weightMsgLeaveGroup, - SimulateMsgLeaveGroup(pCdc, txGen, k, ak, bk), + simulateMsgLeaveGroup(txGen, k, ak, bk, state), ), } @@ -216,8 +239,9 @@ func WeightedOperations( } // SimulateMsgCreateGroup generates a MsgCreateGroup with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgCreateGroup( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, @@ -263,8 +287,9 @@ func SimulateMsgCreateGroup( } // SimulateMsgCreateGroupWithPolicy generates a MsgCreateGroupWithPolicy with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgCreateGroupWithPolicy( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, @@ -297,7 +322,7 @@ func SimulateMsgCreateGroupWithPolicy( GroupPolicyMetadata: simtypes.RandStringOfLength(r, 10), GroupPolicyAsAdmin: r.Float32() < 0.5, } - msg.SetDecisionPolicy(decisionPolicy) + err = msg.SetDecisionPolicy(decisionPolicy) if err != nil { return simtypes.NoOpMsg(group.ModuleName, sdk.MsgTypeURL(msg), "unable to set decision policy"), nil, err } @@ -327,17 +352,29 @@ func SimulateMsgCreateGroupWithPolicy( } // SimulateMsgCreateGroupPolicy generates a NewMsgCreateGroupPolicy with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgCreateGroupPolicy( cdc *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgCreateGroupPolicy(cdc, txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgCreateGroupPolicy( + _ *codec.ProtoCodec, + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - groupInfo, acc, account, err := randomGroup(r, k, ak, sdkCtx, accounts) + groupInfo, acc, account, err := randomGroup(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgCreateGroupPolicy, ""), nil, err } @@ -393,17 +430,29 @@ func SimulateMsgCreateGroupPolicy( } // SimulateMsgSubmitProposal generates a NewMsgSubmitProposal with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgSubmitProposal( cdc *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgSubmitProposal(cdc, txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgSubmitProposal( + _ *codec.ProtoCodec, + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - g, groupPolicy, _, _, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts) + g, groupPolicy, _, _, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgSubmitProposal, ""), nil, err } @@ -474,17 +523,28 @@ func SimulateMsgSubmitProposal( } // SimulateMsgUpdateGroupAdmin generates a MsgUpdateGroupAdmin with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgUpdateGroupAdmin( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgUpdateGroupAdmin(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgUpdateGroupAdmin( + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - groupInfo, acc, account, err := randomGroup(r, k, ak, sdkCtx, accounts) + groupInfo, acc, account, err := randomGroup(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgUpdateGroupAdmin, ""), nil, err } @@ -539,17 +599,28 @@ func SimulateMsgUpdateGroupAdmin( } // SimulateMsgUpdateGroupMetadata generates a MsgUpdateGroupMetadata with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgUpdateGroupMetadata( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgUpdateGroupMetadata(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgUpdateGroupMetadata( + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - groupInfo, acc, account, err := randomGroup(r, k, ak, sdkCtx, accounts) + groupInfo, acc, account, err := randomGroup(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgUpdateGroupMetadata, ""), nil, err } @@ -595,17 +666,28 @@ func SimulateMsgUpdateGroupMetadata( } // SimulateMsgUpdateGroupMembers generates a MsgUpdateGroupMembers with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgUpdateGroupMembers( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgUpdateGroupMembers(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgUpdateGroupMembers( + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - groupInfo, acc, account, err := randomGroup(r, k, ak, sdkCtx, accounts) + groupInfo, acc, account, err := randomGroup(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgUpdateGroupMembers, ""), nil, err } @@ -678,17 +760,28 @@ func SimulateMsgUpdateGroupMembers( } // SimulateMsgUpdateGroupPolicyAdmin generates a MsgUpdateGroupPolicyAdmin with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgUpdateGroupPolicyAdmin( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgUpdateGroupPolicyAdmin(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgUpdateGroupPolicyAdmin( + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - _, groupPolicy, acc, account, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts) + _, groupPolicy, acc, account, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgUpdateGroupPolicyAdmin, ""), nil, err } @@ -742,18 +835,29 @@ func SimulateMsgUpdateGroupPolicyAdmin( } } -// // SimulateMsgUpdateGroupPolicyDecisionPolicy generates a NewMsgUpdateGroupPolicyDecisionPolicy with random values +// SimulateMsgUpdateGroupPolicyDecisionPolicy generates a NewMsgUpdateGroupPolicyDecisionPolicy with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgUpdateGroupPolicyDecisionPolicy( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgUpdateGroupPolicyDecisionPolicy(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgUpdateGroupPolicyDecisionPolicy( + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - _, groupPolicy, acc, account, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts) + _, groupPolicy, acc, account, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgUpdateGroupPolicyDecisionPolicy, ""), nil, err } @@ -806,18 +910,29 @@ func SimulateMsgUpdateGroupPolicyDecisionPolicy( } } -// // SimulateMsgUpdateGroupPolicyMetadata generates a MsgUpdateGroupPolicyMetadata with random values +// SimulateMsgUpdateGroupPolicyMetadata generates a MsgUpdateGroupPolicyMetadata with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgUpdateGroupPolicyMetadata( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgUpdateGroupPolicyMetadata(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgUpdateGroupPolicyMetadata( + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - _, groupPolicy, acc, account, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts) + _, groupPolicy, acc, account, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgUpdateGroupPolicyMetadata, ""), nil, err } @@ -863,17 +978,29 @@ func SimulateMsgUpdateGroupPolicyMetadata( } // SimulateMsgWithdrawProposal generates a MsgWithdrawProposal with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgWithdrawProposal( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgWithdrawProposal(txGen, ak, bk, k, newSharedState()) +} + +// simulateMsgWithdrawProposal generates a MsgWithdrawProposal with random values +func simulateMsgWithdrawProposal( + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - g, groupPolicy, _, _, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts) + g, groupPolicy, _, _, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgWithdrawProposal, ""), nil, err } @@ -958,7 +1085,6 @@ func SimulateMsgWithdrawProposal( } _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) - if err != nil { if strings.Contains(err.Error(), "group was modified") || strings.Contains(err.Error(), "group policy was modified") { return simtypes.NoOpMsg(group.ModuleName, sdk.MsgTypeURL(msg), "no-op:group/group-policy was modified"), nil, nil @@ -971,17 +1097,28 @@ func SimulateMsgWithdrawProposal( } // SimulateMsgVote generates a MsgVote with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgVote( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgVote(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgVote( + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - g, groupPolicy, _, _, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts) + g, groupPolicy, _, _, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgVote, ""), nil, err } @@ -1066,7 +1203,6 @@ func SimulateMsgVote( } _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) - if err != nil { if strings.Contains(err.Error(), "group was modified") || strings.Contains(err.Error(), "group policy was modified") { return simtypes.NoOpMsg(group.ModuleName, sdk.MsgTypeURL(msg), "no-op:group/group-policy was modified"), nil, nil @@ -1078,18 +1214,29 @@ func SimulateMsgVote( } } -// // SimulateMsgExec generates a MsgExec with random values +// SimulateMsgExec generates a MsgExec with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgExec( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, ak group.AccountKeeper, bk group.BankKeeper, k keeper.Keeper, +) simtypes.Operation { + return simulateMsgExec(txGen, ak, bk, k, newSharedState()) +} + +func simulateMsgExec( + txGen client.TxConfig, + ak group.AccountKeeper, + bk group.BankKeeper, + k keeper.Keeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - _, groupPolicy, acc, account, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts) + _, groupPolicy, acc, account, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgExec, ""), nil, err } @@ -1159,17 +1306,28 @@ func SimulateMsgExec( } // SimulateMsgLeaveGroup generates a MsgLeaveGroup with random values +// Deprecated: this is an internal method and will be removed func SimulateMsgLeaveGroup( - cdc *codec.ProtoCodec, + _ *codec.ProtoCodec, txGen client.TxConfig, k keeper.Keeper, ak group.AccountKeeper, bk group.BankKeeper, +) simtypes.Operation { + return simulateMsgLeaveGroup(txGen, k, ak, bk, newSharedState()) +} + +func simulateMsgLeaveGroup( + txGen client.TxConfig, + k keeper.Keeper, + ak group.AccountKeeper, + bk group.BankKeeper, + s *sharedState, ) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, sdkCtx sdk.Context, accounts []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - groupInfo, policyInfo, _, _, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts) + groupInfo, policyInfo, _, _, err := randomGroupPolicy(r, k, ak, sdkCtx, accounts, s) if err != nil { return simtypes.NoOpMsg(group.ModuleName, TypeMsgLeaveGroup, ""), nil, err } @@ -1223,20 +1381,14 @@ func SimulateMsgLeaveGroup( } func randomGroup(r *rand.Rand, k keeper.Keeper, ak group.AccountKeeper, - ctx sdk.Context, accounts []simtypes.Account, + ctx sdk.Context, accounts []simtypes.Account, s *sharedState, ) (groupInfo *group.GroupInfo, acc simtypes.Account, account sdk.AccountI, err error) { groupID := k.GetGroupSequence(ctx) - switch { - case groupID > initialGroupID: - // select a random ID between (initialGroupID, groupID] - // if there is at least one group information, then the groupID at this time must be greater than or equal to 1 + if initialGroupID := s.getMinGroupID(); initialGroupID == unsetGroupID { + s.setMinGroupID(groupID) + } else if initialGroupID < groupID { groupID = uint64(simtypes.RandIntBetween(r, int(initialGroupID+1), int(groupID+1))) - - default: - // This is called on the first call to this function - // in order to update the global variable - initialGroupID = groupID } // when groupID is 0, it proves that SimulateMsgCreateGroup has never been called. that is, no group exists in the chain @@ -1267,9 +1419,9 @@ func randomGroup(r *rand.Rand, k keeper.Keeper, ak group.AccountKeeper, } func randomGroupPolicy(r *rand.Rand, k keeper.Keeper, ak group.AccountKeeper, - ctx sdk.Context, accounts []simtypes.Account, + ctx sdk.Context, accounts []simtypes.Account, s *sharedState, ) (groupInfo *group.GroupInfo, groupPolicyInfo *group.GroupPolicyInfo, acc simtypes.Account, account sdk.AccountI, err error) { - groupInfo, _, _, err = randomGroup(r, k, ak, ctx, accounts) + groupInfo, _, _, err = randomGroup(r, k, ak, ctx, accounts, s) if err != nil { return nil, nil, simtypes.Account{}, nil, err } @@ -1352,12 +1504,8 @@ func genGroupMembers(r *rand.Rand, accounts []simtypes.Account) []group.MemberRe } } - max := 5 - if len(accounts) < max { - max = len(accounts) - } - - membersLen := simtypes.RandIntBetween(r, 1, max) + maxMembers := min(len(accounts), 5) + membersLen := simtypes.RandIntBetween(r, 1, maxMembers) members := make([]group.MemberRequest, membersLen) for i := 0; i < membersLen; i++ { diff --git a/x/group/simulation/operations_test.go b/x/group/simulation/operations_test.go index b00039145b..09377bbce5 100644 --- a/x/group/simulation/operations_test.go +++ b/x/group/simulation/operations_test.go @@ -782,7 +782,7 @@ func (suite *SimTestSuite) TestSimulateLeaveGroup() { }) // execute operation - op := simulation.SimulateMsgLeaveGroup(codec.NewProtoCodec(suite.interfaceRegistry), suite.txConfig, suite.groupKeeper, suite.accountKeeper, suite.bankKeeper) + op := simulation.SimulateMsgLeaveGroup(nil, suite.txConfig, suite.groupKeeper, suite.accountKeeper, suite.bankKeeper) operationMsg, futureOperations, err := op(r, suite.app.BaseApp, suite.ctx, accounts, "") suite.Require().NoError(err) diff --git a/x/simulation/client/cli/flags.go b/x/simulation/client/cli/flags.go index c157bdb8e8..8479b83f5e 100644 --- a/x/simulation/client/cli/flags.go +++ b/x/simulation/client/cli/flags.go @@ -2,6 +2,7 @@ package cli import ( "flag" + "time" "github.com/cosmos/cosmos-sdk/types/simulation" ) @@ -22,15 +23,21 @@ var ( FlagBlockSizeValue int FlagLeanValue bool FlagCommitValue bool - FlagOnOperationValue bool // TODO: Remove in favor of binary search for invariant violation - FlagAllInvariantsValue bool FlagDBBackendValue string - FlagEnabledValue bool FlagVerboseValue bool - FlagPeriodValue uint FlagGenesisTimeValue int64 FlagSigverifyTxValue bool + FlagFauxMerkle bool + + // Deprecated: This flag is unused and will be removed in a future release. + FlagPeriodValue uint + // Deprecated: This flag is unused and will be removed in a future release. + FlagEnabledValue bool + // Deprecated: This flag is unused and will be removed in a future release. + FlagOnOperationValue bool + // Deprecated: This flag is unused and will be removed in a future release. + FlagAllInvariantsValue bool ) // GetSimulatorFlags gets the values of all the available simulation flags @@ -47,17 +54,19 @@ func GetSimulatorFlags() { flag.IntVar(&FlagNumBlocksValue, "NumBlocks", 500, "number of new blocks to simulate from the initial block height") flag.IntVar(&FlagBlockSizeValue, "BlockSize", 200, "operations per block") flag.BoolVar(&FlagLeanValue, "Lean", false, "lean simulation log output") - flag.BoolVar(&FlagCommitValue, "Commit", false, "have the simulation commit") - flag.BoolVar(&FlagOnOperationValue, "SimulateEveryOperation", false, "run slow invariants every operation") - flag.BoolVar(&FlagAllInvariantsValue, "PrintAllInvariants", false, "print all invariants if a broken invariant is found") - flag.StringVar(&FlagDBBackendValue, "DBBackend", "goleveldb", "custom db backend type") + flag.BoolVar(&FlagCommitValue, "Commit", true, "have the simulation commit") + flag.StringVar(&FlagDBBackendValue, "DBBackend", "memdb", "custom db backend type: goleveldb, pebbledb, memdb") // simulation flags - flag.BoolVar(&FlagEnabledValue, "Enabled", false, "enable the simulation") flag.BoolVar(&FlagVerboseValue, "Verbose", false, "verbose log output") - flag.UintVar(&FlagPeriodValue, "Period", 0, "run slow invariants only once every period assertions") - flag.Int64Var(&FlagGenesisTimeValue, "GenesisTime", 0, "override genesis UNIX time instead of using a random UNIX time") + flag.Int64Var(&FlagGenesisTimeValue, "GenesisTime", time.Now().Unix(), "use current time as genesis UNIX time for default") flag.BoolVar(&FlagSigverifyTxValue, "SigverifyTx", true, "whether to sigverify check for transaction ") + flag.BoolVar(&FlagFauxMerkle, "FauxMerkle", false, "use faux merkle instead of iavl") + + flag.UintVar(&FlagPeriodValue, "Period", 0, "This parameter is unused and will be removed") + flag.BoolVar(&FlagEnabledValue, "Enabled", false, "This parameter is unused and will be removed") + flag.BoolVar(&FlagOnOperationValue, "SimulateEveryOperation", false, "This parameter is unused and will be removed") + flag.BoolVar(&FlagAllInvariantsValue, "PrintAllInvariants", false, "This parameter is unused and will be removed") } // NewConfigFromFlags creates a simulation from the retrieved values of the flags. @@ -71,12 +80,28 @@ func NewConfigFromFlags() simulation.Config { ExportStatsPath: FlagExportStatsPathValue, Seed: FlagSeedValue, InitialBlockHeight: FlagInitialBlockHeightValue, + GenesisTime: FlagGenesisTimeValue, NumBlocks: FlagNumBlocksValue, BlockSize: FlagBlockSizeValue, Lean: FlagLeanValue, Commit: FlagCommitValue, - OnOperation: FlagOnOperationValue, - AllInvariants: FlagAllInvariantsValue, DBBackend: FlagDBBackendValue, } } + +// GetDeprecatedFlagUsed return list of deprecated flag names that are being used. +// This function is for internal usage only and may be removed with the deprecated fields. +func GetDeprecatedFlagUsed() []string { + var usedFlags []string + for _, flagName := range []string{ + "Enabled", + "SimulateEveryOperation", + "PrintAllInvariants", + "Period", + } { + if flag.Lookup(flagName) != nil { + usedFlags = append(usedFlags, flagName) + } + } + return usedFlags +} diff --git a/x/simulation/doc.go b/x/simulation/doc.go index f92a6cac8b..cca8eb224d 100644 --- a/x/simulation/doc.go +++ b/x/simulation/doc.go @@ -46,70 +46,65 @@ others state execution outcome. # Usage +Switch to `simapp/` directory: + + $ cd simapp/ + To execute a completely pseudo-random simulation: - $ go test -mod=readonly github.com/cosmos/cosmos-sdk/simapp \ + $ go test -mod=readonly . \ + -tags='sims' \ -run=TestFullAppSimulation \ - -Enabled=true \ -NumBlocks=100 \ -BlockSize=200 \ - -Commit=true \ -Seed=99 \ - -Period=5 \ -v -timeout 24h To execute simulation from a genesis file: - $ go test -mod=readonly github.com/cosmos/cosmos-sdk/simapp \ + $ go test -mod=readonly . \ + -tags='sims' \ -run=TestFullAppSimulation \ - -Enabled=true \ -NumBlocks=100 \ -BlockSize=200 \ -Commit=true \ -Seed=99 \ - -Period=5 \ -Genesis=/path/to/genesis.json \ -v -timeout 24h To execute simulation from a simulation params file: - $ go test -mod=readonly github.com/cosmos/cosmos-sdk/simapp \ + $ go test -mod=readonly . \ + -tags='sims' \ -run=TestFullAppSimulation \ - -Enabled=true \ -NumBlocks=100 \ -BlockSize=200 \ - -Commit=true \ -Seed=99 \ - -Period=5 \ -Params=/path/to/params.json \ -v -timeout 24h To export the simulation params to a file at a given block height: - $ go test -mod=readonly github.com/cosmos/cosmos-sdk/simapp \ + $ go test -mod=readonly . \ + -tags='sims' \ -run=TestFullAppSimulation \ - -Enabled=true \ -NumBlocks=100 \ -BlockSize=200 \ - -Commit=true \ -Seed=99 \ - -Period=5 \ -ExportParamsPath=/path/to/params.json \ -ExportParamsHeight=50 \ - -v -timeout 24h + -v -timeout 24h To export the simulation app state (i.e genesis) to a file: - $ go test -mod=readonly github.com/cosmos/cosmos-sdk/simapp \ + $ go test -mod=readonly . \ + -tags='sims' \ -run=TestFullAppSimulation \ - -Enabled=true \ -NumBlocks=100 \ -BlockSize=200 \ - -Commit=true \ -Seed=99 \ - -Period=5 \ -ExportStatePath=/path/to/genesis.json \ - v -timeout 24h + -v -timeout 24h # Params diff --git a/x/simulation/log.go b/x/simulation/log.go index 42eba43b89..cd0a77c86a 100644 --- a/x/simulation/log.go +++ b/x/simulation/log.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path" + "sync" "time" ) @@ -24,7 +25,11 @@ func NewLogWriter(testingmode bool) LogWriter { // log writter type StandardLogWriter struct { + Seed int64 + OpEntries []OperationEntry `json:"op_entries" yaml:"op_entries"` + wMtx sync.Mutex + written bool } // add an entry to the log writter @@ -34,7 +39,12 @@ func (lw *StandardLogWriter) AddEntry(opEntry OperationEntry) { // PrintLogs - print the logs to a simulation file func (lw *StandardLogWriter) PrintLogs() { - f := createLogFile() + lw.wMtx.Lock() + defer lw.wMtx.Unlock() + if lw.written { // print once only + return + } + f := createLogFile(lw.Seed) defer f.Close() for i := 0; i < len(lw.OpEntries); i++ { @@ -44,12 +54,16 @@ func (lw *StandardLogWriter) PrintLogs() { panic("Failed to write logs to file") } } + lw.written = true } -func createLogFile() *os.File { +func createLogFile(seed int64) *os.File { var f *os.File - - fileName := fmt.Sprintf("%d.log", time.Now().UnixMilli()) + var prefix string + if seed != 0 { + prefix = fmt.Sprintf("seed_%10d", seed) + } + fileName := fmt.Sprintf("%s--%d.log", prefix, time.Now().UnixNano()) folderPath := path.Join(os.ExpandEnv("$HOME"), ".simapp", "simulations") filePath := path.Join(folderPath, fileName) diff --git a/x/simulation/params.go b/x/simulation/params.go index 7be6d77ae0..a41edcf3a3 100644 --- a/x/simulation/params.go +++ b/x/simulation/params.go @@ -2,7 +2,6 @@ package simulation import ( "encoding/json" - "fmt" "math/rand" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" @@ -197,12 +196,5 @@ func randomConsensusParams(r *rand.Rand, appState json.RawMessage, cdc codec.JSO MaxAgeDuration: stakingGenesisState.Params.UnbondingTime, }, } - - bz, err := json.MarshalIndent(&consensusParams, "", " ") - if err != nil { - panic(err) - } - fmt.Printf("Selected randomly generated consensus parameters:\n%s\n", bz) - return consensusParams } diff --git a/x/simulation/simulate.go b/x/simulation/simulate.go index 0e7819b650..44133bdca7 100644 --- a/x/simulation/simulate.go +++ b/x/simulation/simulate.go @@ -1,18 +1,21 @@ package simulation import ( + "bytes" + "encoding/binary" + "encoding/hex" "fmt" "io" "math/rand" - "os" - "os/signal" - "syscall" "testing" "time" abci "github.com/cometbft/cometbft/abci/types" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + "cosmossdk.io/core/header" + "cosmossdk.io/log" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -65,14 +68,37 @@ func SimulateFromSeed( config simulation.Config, cdc codec.JSONCodec, ) (stopEarly bool, exportedParams Params, err error) { + tb.Helper() + mode, _, _ := getTestingMode(tb) + expParams, err := SimulateFromSeedX(tb, log.NewTestLogger(tb), w, app, appStateFn, randAccFn, ops, blockedAddrs, config, cdc, NewLogWriter(mode)) + return false, expParams, err +} + +// SimulateFromSeedX tests an application by running the provided +// operations, testing the provided invariants, but using the provided config.Seed. +func SimulateFromSeedX( + tb testing.TB, + logger log.Logger, + w io.Writer, + app *baseapp.BaseApp, + appStateFn simulation.AppStateFn, + randAccFn simulation.RandomAccountFn, + ops WeightedOperations, + blockedAddrs map[string]bool, + config simulation.Config, + cdc codec.JSONCodec, + logWriter LogWriter, +) (exportedParams Params, err error) { + tb.Helper() // in case we have to end early, don't os.Exit so that we can run cleanup code. testingMode, _, b := getTestingMode(tb) - r := rand.New(rand.NewSource(config.Seed)) + r := rand.New(newByteSource(config.FuzzSeed, config.Seed)) params := RandomParams(r) - fmt.Fprintf(w, "Starting SimulateFromSeed with randomness created with seed %d\n", int(config.Seed)) - fmt.Fprintf(w, "Randomized simulation params: \n%s\n", mustMarshalJSONIndent(params)) + startTime := time.Now() + logger.Info("Starting SimulateFromSeed with randomness", "time", startTime) + logger.Debug("Randomized simulation setup", "params", mustMarshalJSONIndent(params)) timeDiff := maxTimePerBlock - minTimePerBlock accs := randAccFn(r, params.NumKeys()) @@ -84,16 +110,11 @@ func SimulateFromSeed( // At least 2 accounts must be added here, otherwise when executing SimulateMsgSend // two accounts will be selected to meet the conditions from != to and it will fall into an infinite loop. if len(accs) <= 1 { - return true, params, fmt.Errorf("at least two genesis accounts are required") + return params, fmt.Errorf("at least two genesis accounts are required") } config.ChainID = chainID - fmt.Printf( - "Starting the simulation from time %v (unixtime %v)\n", - blockTime.UTC().Format(time.UnixDate), blockTime.Unix(), - ) - // remove module account address if they exist in accs var tmpAccs []simulation.Account @@ -107,7 +128,7 @@ func SimulateFromSeed( nextValidators := validators if len(nextValidators) == 0 { tb.Skip("skipping: empty validator set in genesis") - return true, params, nil + return params, nil } var ( @@ -120,17 +141,6 @@ func SimulateFromSeed( opCount = 0 ) - // Setup code to catch SIGTERM's - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) - - go func() { - receivedSignal := <-c - fmt.Fprintf(w, "\nExiting early due to %s, on block %d, operation %d\n", receivedSignal, blockHeight, opCount) - err = fmt.Errorf("exited due to %s", receivedSignal) - stopEarly = true - }() - finalizeBlockReq := RandomRequestFinalizeBlock( r, params, @@ -145,11 +155,10 @@ func SimulateFromSeed( // These are operations which have been queued by previous operations operationQueue := NewOperationQueue() - logWriter := NewLogWriter(testingMode) blockSimulator := createBlockSimulator( - testingMode, tb, + testingMode, w, params, eventStats.Tally, @@ -166,7 +175,7 @@ func SimulateFromSeed( // recover logs in case of panic defer func() { if r := recover(); r != nil { - _, _ = fmt.Fprintf(w, "simulation halted due to panic on block %d\n", blockHeight) + logger.Error("simulation halted due to panic", "height", blockHeight) logWriter.PrintLogs() panic(r) } @@ -178,7 +187,7 @@ func SimulateFromSeed( exportedParams = params } - for blockHeight < int64(config.NumBlocks+config.InitialBlockHeight) && !stopEarly { + for blockHeight < int64(config.NumBlocks+config.InitialBlockHeight) { pastTimes = append(pastTimes, blockTime) pastVoteInfos = append(pastVoteInfos, finalizeBlockReq.DecidedLastCommit.Votes) @@ -187,7 +196,7 @@ func SimulateFromSeed( res, err := app.FinalizeBlock(finalizeBlockReq) if err != nil { - return true, params, err + return params, fmt.Errorf("block finalization failed at height %d: %w", blockHeight, err) } ctx := app.NewContextLegacy(false, cmtproto.Header{ @@ -195,17 +204,21 @@ func SimulateFromSeed( Time: blockTime, ProposerAddress: proposerAddress, ChainID: config.ChainID, + }).WithHeaderInfo(header.Info{ + Height: blockHeight, + Time: blockTime, + ChainID: config.ChainID, }) // run queued operations; ignores block size if block size is too small numQueuedOpsRan, futureOps := runQueuedOperations( - operationQueue, int(blockHeight), tb, r, app, ctx, accs, logWriter, + tb, operationQueue, blockTime, int(blockHeight), r, app, ctx, accs, logWriter, eventStats.Tally, config.Lean, config.ChainID, ) - numQueuedTimeOpsRan, timeFutureOps := runQueuedTimeOperations( + numQueuedTimeOpsRan, timeFutureOps := runQueuedTimeOperations(tb, timeOperationQueue, int(blockHeight), blockTime, - tb, r, app, ctx, accs, logWriter, eventStats.Tally, + r, app, ctx, accs, logWriter, eventStats.Tally, config.Lean, config.ChainID, ) @@ -223,19 +236,20 @@ func SimulateFromSeed( blockHeight++ + logWriter.AddEntry(EndBlockEntry(blockHeight)) + blockTime = blockTime.Add(time.Duration(minTimePerBlock) * time.Second) blockTime = blockTime.Add(time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second) proposerAddress = validators.randomProposer(r) - logWriter.AddEntry(EndBlockEntry(blockHeight)) - if config.Commit { - app.Commit() + if _, err := app.Commit(); err != nil { + return params, fmt.Errorf("commit failed at height %d: %w", blockHeight, err) + } } if proposerAddress == nil { - fmt.Fprintf(w, "\nSimulation stopped early as all validators have been unbonded; nobody left to propose a block!\n") - stopEarly = true + logger.Info("Simulation stopped early as all validators have been unbonded; nobody left to propose a block", "height", blockHeight) break } @@ -249,7 +263,7 @@ func SimulateFromSeed( nextValidators = updateValidators(tb, r, params, validators, res.ValidatorUpdates, eventStats.Tally) if len(nextValidators) == 0 { tb.Skip("skipping: empty validator set") - return true, params, nil + return params, nil } // update the exported params @@ -258,22 +272,8 @@ func SimulateFromSeed( } } - if stopEarly { - if config.ExportStatsPath != "" { - fmt.Println("Exporting simulation statistics...") - eventStats.ExportJSON(config.ExportStatsPath) - } else { - eventStats.Print(w) - } - - return true, exportedParams, err - } - - fmt.Fprintf( - w, - "\nSimulation complete; Final height (blocks): %d, final time (seconds): %v, operations ran: %d\n", - blockHeight, blockTime, opCount, - ) + logger.Info("Simulation complete", "height", blockHeight, "block-time", blockTime, "opsCount", opCount, + "run-time", time.Since(startTime), "app-hash", hex.EncodeToString(app.LastCommitID().Hash)) if config.ExportStatsPath != "" { fmt.Println("Exporting simulation statistics...") @@ -281,8 +281,7 @@ func SimulateFromSeed( } else { eventStats.Print(w) } - - return false, exportedParams, nil + return exportedParams, err } type blockSimFn func( @@ -294,12 +293,13 @@ type blockSimFn func( ) (opCount int) // Returns a function to simulate blocks. Written like this to avoid constant -// parameters being passed everytime, to minimize memory overhead. -func createBlockSimulator(testingMode bool, tb testing.TB, w io.Writer, params Params, +// parameters being passed every time, to minimize memory overhead. +func createBlockSimulator(tb testing.TB, printProgress bool, w io.Writer, params Params, event func(route, op, evResult string), ops WeightedOperations, operationQueue OperationQueue, timeOperationQueue []simulation.FutureOperation, logWriter LogWriter, config simulation.Config, ) blockSimFn { + tb.Helper() lastBlockSizeState := 0 // state for [4 * uniform distribution] blocksize := 0 selectOp := ops.getSelectOpFn() @@ -325,7 +325,7 @@ func createBlockSimulator(testingMode bool, tb testing.TB, w io.Writer, params P for i := 0; i < blocksize; i++ { opAndRz = append(opAndRz, opAndR{ op: selectOp(r), - rand: simulation.DeriveRand(r), + rand: r, }) } @@ -350,8 +350,8 @@ Comment: %s`, queueOperations(operationQueue, timeOperationQueue, futureOps) - if testingMode && opCount%50 == 0 { - fmt.Fprintf(w, "\rSimulating... block %d/%d, operation %d/%d. ", + if printProgress && opCount%50 == 0 { + _, _ = fmt.Fprintf(w, "\rSimulating... block %d/%d, operation %d/%d. ", header.Height, config.NumBlocks, opCount, blocksize) } @@ -362,11 +362,21 @@ Comment: %s`, } } -func runQueuedOperations(queueOps map[int][]simulation.Operation, - height int, tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, - ctx sdk.Context, accounts []simulation.Account, logWriter LogWriter, - event func(route, op, evResult string), lean bool, chainID string, +func runQueuedOperations( + tb testing.TB, + queueOps map[int][]simulation.Operation, + blockTime time.Time, + height int, + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accounts []simulation.Account, + logWriter LogWriter, + event func(route, op, evResult string), + lean bool, + chainID string, ) (numOpsRan int, allFutureOps []simulation.FutureOperation) { + tb.Helper() queuedOp, ok := queueOps[height] if !ok { return 0, nil @@ -398,12 +408,13 @@ func runQueuedOperations(queueOps map[int][]simulation.Operation, return numOpsRan, allFutureOps } -func runQueuedTimeOperations(queueOps []simulation.FutureOperation, - height int, currentTime time.Time, tb testing.TB, r *rand.Rand, +func runQueuedTimeOperations(tb testing.TB, queueOps []simulation.FutureOperation, + height int, currentTime time.Time, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simulation.Account, logWriter LogWriter, event func(route, op, evResult string), lean bool, chainID string, ) (numOpsRan int, allFutureOps []simulation.FutureOperation) { + tb.Helper() // Keep all future operations allFutureOps = make([]simulation.FutureOperation, 0) @@ -432,3 +443,41 @@ func runQueuedTimeOperations(queueOps []simulation.FutureOperation, return numOpsRan, allFutureOps } + +const ( + rngMax = 1 << 63 + rngMask = rngMax - 1 +) + +// byteSource offers deterministic pseudo-random numbers for math.Rand with fuzzer support. +// The 'seed' data is read in big endian to uint64. When exhausted, +// it falls back to a standard random number generator initialized with a specific 'seed' value. +type byteSource struct { + seed *bytes.Reader + fallback *rand.Rand +} + +// newByteSource creates a new byteSource with a specified byte slice and seed. This gives a fixed sequence of pseudo-random numbers. +// Initially, it utilizes the byte slice. Once that's exhausted, it continues generating numbers using the provided seed. +func newByteSource(fuzzSeed []byte, seed int64) *byteSource { + return &byteSource{ + seed: bytes.NewReader(fuzzSeed), + fallback: rand.New(rand.NewSource(seed)), + } +} + +func (s *byteSource) Uint64() uint64 { + if s.seed.Len() < 8 { + return s.fallback.Uint64() + } + var b [8]byte + if _, err := s.seed.Read(b[:]); err != nil && err != io.EOF { + panic(err) // Should not happen. + } + return binary.BigEndian.Uint64(b[:]) +} + +func (s *byteSource) Int63() int64 { + return int64(s.Uint64() & rngMask) +} +func (s *byteSource) Seed(seed int64) {}