diff --git a/runtime/v2/app.go b/runtime/v2/app.go index 5c5c78532f33..b89febe8117e 100644 --- a/runtime/v2/app.go +++ b/runtime/v2/app.go @@ -106,3 +106,8 @@ func (a *App[T]) SchemaDecoderResolver() decoding.DecoderResolver { func (a *App[T]) Close() error { return nil } + +// GetApp return self +func (a *App[T]) GetApp() *App[T] { + return a +} diff --git a/scripts/build/simulations.mk b/scripts/build/simulations.mk index 86b96c8c8d61..932ae31ffb11 100644 --- a/scripts/build/simulations.mk +++ b/scripts/build/simulations.mk @@ -47,10 +47,16 @@ test-sim-multi-seed-long: # @cd ${CURRENT_DIR}/simapp && go test -failfast -mod=readonly -timeout=2h -tags='sims' -run TestFullAppSimulation \ # -NumBlocks=150 -Period=50 -test-sim-multi-seed-short: +test-sim-multi-seed-short: test-v2-sim-wip # @echo "Running short multi-seed application simulation. This may take awhile!" # @cd ${CURRENT_DIR}/simapp && go test -failfast -mod=readonly -timeout 30m -tags='sims' -run TestFullAppSimulation \ - # -NumBlocks=50 -Period=10 -FauxMerkle=true + # -NumBlocks=50 -Period=10 -FauxMerkle=true + +.Phony: test-v2-sim-wip +test-v2-sim-wip: + @echo "Running short multi-seed application simulation. This may take awhile!" + @cd ${CURRENT_DIR}/simapp/v2 && go test -failfast -mod=readonly -timeout 30m -tags='sims' -run TestSimsAppV2 \ +# -NumBlocks=50 -Period=10 -FauxMerkle=true test-sim-benchmark-invariants: # @echo "Running simulation invariant benchmarks..." diff --git a/server/v2/appmanager/appmanager.go b/server/v2/appmanager/appmanager.go index 19f7eaae545c..cc76a53945db 100644 --- a/server/v2/appmanager/appmanager.go +++ b/server/v2/appmanager/appmanager.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "iter" "cosmossdk.io/core/server" corestore "cosmossdk.io/core/store" @@ -53,6 +54,12 @@ type AppManager[T transaction.Tx] interface { // independently of the db state. For example, it can be used to process a query with temporary // and uncommitted state QueryWithState(ctx context.Context, state corestore.ReaderMap, request transaction.Msg) (transaction.Msg, error) + + DeliverSims( + ctx context.Context, + block *server.BlockRequest[T], + simsBuilder func(ctx context.Context) iter.Seq[T], + ) (*server.BlockResponse, corestore.WriterMap, error) } // Store defines the underlying storage behavior needed by AppManager. @@ -187,6 +194,28 @@ func (a appManager[T]) DeliverBlock( return blockResponse, newState, nil } +func (a appManager[T]) DeliverSims( + ctx context.Context, + block *server.BlockRequest[T], + simsBuilder func(ctx context.Context) iter.Seq[T], +) (*server.BlockResponse, corestore.WriterMap, error) { + latestVersion, currentState, err := a.db.StateLatest() + if err != nil { + return nil, nil, fmt.Errorf("unable to create new state for height %d: %w", block.Height, err) + } + + if latestVersion+1 != block.Height { + return nil, nil, fmt.Errorf("invalid DeliverBlock height wanted %d, got %d", latestVersion+1, block.Height) + } + + blockResponse, newState, err := a.stf.DeliverSims(ctx, block, currentState, simsBuilder) + if err != nil { + return nil, nil, fmt.Errorf("block delivery failed: %w", err) + } + + return blockResponse, newState, nil +} + // ValidateTx will validate the tx against the latest storage state. This means that // only the stateful validation will be run, not the execution portion of the tx. // If full execution is needed, Simulate must be used. diff --git a/server/v2/appmanager/stf.go b/server/v2/appmanager/stf.go index 1e769c13ff9c..ae1a51de3ba1 100644 --- a/server/v2/appmanager/stf.go +++ b/server/v2/appmanager/stf.go @@ -2,6 +2,7 @@ package appmanager import ( "context" + "iter" "cosmossdk.io/core/server" "cosmossdk.io/core/store" @@ -40,4 +41,11 @@ type StateTransitionFunction[T transaction.Tx] interface { gasLimit uint64, req transaction.Msg, ) (transaction.Msg, error) + + DeliverSims( + ctx context.Context, + block *server.BlockRequest[T], + state store.ReaderMap, + simsBuilder func(ctx context.Context) iter.Seq[T], + ) (blockResult *server.BlockResponse, newState store.WriterMap, err error) } diff --git a/server/v2/stf/sims_entry.go b/server/v2/stf/sims_entry.go new file mode 100644 index 000000000000..7de238676445 --- /dev/null +++ b/server/v2/stf/sims_entry.go @@ -0,0 +1,34 @@ +package stf + +import ( + "context" + "iter" + + "cosmossdk.io/core/header" + "cosmossdk.io/core/server" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" +) + +// doSimsTXs constructs a function to simulate transactions in a block execution context using the provided simsBuilder. +func (s STF[T]) doSimsTXs(simsBuilder func(ctx context.Context) iter.Seq[T]) doInBlockDeliveryFn[T] { + return func( + exCtx context.Context, + _ []T, + newState store.WriterMap, + hi header.Info, + ) ([]server.TxResult, error) { + const key = "sims.header.time" + simsCtx := context.WithValue(exCtx, key, hi.Time) //nolint: staticcheck // using string key to decouple + var results []server.TxResult + var i int32 + for tx := range simsBuilder(simsCtx) { + if err := isCtxCancelled(simsCtx); err != nil { + return nil, err + } + results = append(results, s.deliverTx(simsCtx, newState, tx, transaction.ExecModeFinalize, hi, i+1)) + i++ + } + return results, nil + } +} diff --git a/server/v2/stf/stf.go b/server/v2/stf/stf.go index 4b85c5ffe499..dc693ffefe9d 100644 --- a/server/v2/stf/stf.go +++ b/server/v2/stf/stf.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "iter" "strings" appmodulev2 "cosmossdk.io/core/appmodule/v2" @@ -84,6 +85,16 @@ func New[T transaction.Tx]( }, nil } +// DeliverSims entrypoint to processes sims transactions similar to DeliverBlock. +func (s STF[T]) DeliverSims( + ctx context.Context, + block *server.BlockRequest[T], + state store.ReaderMap, + simsBuilder func(ctx context.Context) iter.Seq[T], +) (blockResult *server.BlockResponse, newState store.WriterMap, err error) { + return s.deliverBlock(ctx, block, state, s.doSimsTXs(simsBuilder)) +} + // DeliverBlock is our state transition function. // It takes a read only view of the state to apply the block to, // executes the block and returns the block results and the new state. @@ -91,6 +102,23 @@ func (s STF[T]) DeliverBlock( ctx context.Context, block *server.BlockRequest[T], state store.ReaderMap, +) (blockResult *server.BlockResponse, newState store.WriterMap, err error) { + return s.deliverBlock(ctx, block, state, s.doDeliverTXs) +} + +// common code path for DeliverSims and DeliverBlock +type doInBlockDeliveryFn[T transaction.Tx] func( + ctx context.Context, + txs []T, + newState store.WriterMap, + hi header.Info, +) ([]server.TxResult, error) + +func (s STF[T]) deliverBlock( + ctx context.Context, + block *server.BlockRequest[T], + state store.ReaderMap, + doInBlockDelivery doInBlockDeliveryFn[T], ) (blockResult *server.BlockResponse, newState store.WriterMap, err error) { // creates a new branchFn state, from the readonly view of the state // that can be written to. @@ -141,14 +169,9 @@ func (s STF[T]) DeliverBlock( } // execute txs - txResults := make([]server.TxResult, len(block.Txs)) - // TODO: skip first tx if vote extensions are enabled (marko) - for i, txBytes := range block.Txs { - // check if we need to return early or continue delivering txs - if err = isCtxCancelled(ctx); err != nil { - return nil, nil, err - } - txResults[i] = s.deliverTx(exCtx, newState, txBytes, transaction.ExecModeFinalize, hi, int32(i+1)) + txResults, err := doInBlockDelivery(exCtx, block.Txs, newState, hi) + if err != nil { + return nil, nil, err } // reset events exCtx.events = make([]event.Event, 0) @@ -167,6 +190,25 @@ func (s STF[T]) DeliverBlock( }, newState, nil } +func (s STF[T]) doDeliverTXs( + exCtx context.Context, + txs []T, + newState store.WriterMap, + hi header.Info, +) ([]server.TxResult, error) { + // execute txs + txResults := make([]server.TxResult, len(txs)) + // TODO: skip first tx if vote extensions are enabled (marko) + for i, txBytes := range txs { + // check if we need to return early or continue delivering txs + if err := isCtxCancelled(exCtx); err != nil { + return nil, err + } + txResults[i] = s.deliverTx(exCtx, newState, txBytes, transaction.ExecModeFinalize, hi, int32(i+1)) + } + return txResults, nil +} + // deliverTx executes a TX and returns the result. func (s STF[T]) deliverTx( ctx context.Context, diff --git a/server/v2/streaming/examples/file/file.go b/server/v2/streaming/examples/file/file.go index 607cb8ed6293..8f9dac85f81f 100644 --- a/server/v2/streaming/examples/file/file.go +++ b/server/v2/streaming/examples/file/file.go @@ -6,8 +6,9 @@ import ( "os" "path/filepath" - "cosmossdk.io/server/v2/streaming" "github.com/hashicorp/go-plugin" + + "cosmossdk.io/server/v2/streaming" ) // FilePlugin is the implementation of the baseapp.ABCIListener interface diff --git a/simapp/v2/sim_runner.go b/simapp/v2/sim_runner.go new file mode 100644 index 000000000000..33cf3889be34 --- /dev/null +++ b/simapp/v2/sim_runner.go @@ -0,0 +1,469 @@ +package simapp + +import ( + "context" + "encoding/json" + "fmt" + "iter" + "math/rand" + "os" + "path/filepath" + "slices" + "testing" + "time" + + cmtproto "github.com/cometbft/cometbft/api/cometbft/types/v1" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "cosmossdk.io/core/appmodule" + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/comet" + corecontext "cosmossdk.io/core/context" + "cosmossdk.io/core/server" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/depinject" + "cosmossdk.io/log" + "cosmossdk.io/runtime/v2" + serverv2 "cosmossdk.io/server/v2" + "cosmossdk.io/server/v2/appmanager" + cometbfttypes "cosmossdk.io/server/v2/cometbft/types" + storev2 "cosmossdk.io/store/v2" + consensustypes "cosmossdk.io/x/consensus/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/simsx" + simsxv2 "github.com/cosmos/cosmos-sdk/simsx/v2" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" +) + +type Tx = transaction.Tx +type ( + HasWeightedOperationsX = simsx.HasWeightedOperationsX + HasWeightedOperationsXWithProposals = simsx.HasWeightedOperationsXWithProposals + HasProposalMsgsX = simsx.HasProposalMsgsX +) + +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, +} + +const ( + maxTimePerBlock = 10_000 * time.Second + minTimePerBlock = maxTimePerBlock / 2 + timeRangePerBlock = maxTimePerBlock - minTimePerBlock +) + +type ( + AuthKeeper interface { + simsx.ModuleAccountSource + simsx.AccountSource + } + + BankKeeper interface { + simsx.BalanceSource + GetBlockedAddresses() map[string]bool + } + + SimulationApp[T Tx] interface { + GetApp() *runtime.App[T] + ModuleManager() *runtime.MM[T] + TxConfig() client.TxConfig + AppCodec() codec.Codec + DefaultGenesis() map[string]json.RawMessage + Store() storev2.RootStore + Close() error + } + + StakingKeeper interface { + UnbondingTime(ctx context.Context) (time.Duration, error) + } + + ModuleManager interface { + Modules() map[string]appmodulev2.AppModule + } + + TestInstance[T Tx] struct { + App SimulationApp[T] + TxDecoder transaction.Codec[T] + BankKeeper BankKeeper + AuthKeeper AuthKeeper + StakingKeeper StakingKeeper + TXBuilder simsxv2.TXBuilder[T] + AppManager appmanager.AppManager[T] + ModuleManager ModuleManager + } + + AppFactory[T Tx, V SimulationApp[T]] func(config depinject.Config, outputs ...any) (V, error) +) + +func SetupTestInstance[T Tx, V SimulationApp[T]](t *testing.T, factory AppFactory[T, V], appConfig depinject.Config) TestInstance[T] { + t.Helper() + nodeHome := t.TempDir() + currentDir, err := os.Getwd() + require.NoError(t, err) + configPath := filepath.Join(currentDir, "testdata") + v, err := serverv2.ReadConfig(configPath) + require.NoError(t, err) + v.Set("home", nodeHome) + v.Set("store.app-db-backend", "memdb") + + depInjCfg := depinject.Configs( + depinject.Supply(log.NewNopLogger(), runtime.GlobalConfig(v.AllSettings())), + appConfig, + ) + var ( + bankKeeper BankKeeper + authKeeper AuthKeeper + stKeeper StakingKeeper + ) + + err = depinject.Inject(depInjCfg, + &authKeeper, + &bankKeeper, + &stKeeper, + ) + require.NoError(t, err) + + xapp, err := factory(depinject.Configs(depinject.Supply(log.NewNopLogger(), runtime.GlobalConfig(v.AllSettings())))) + require.NoError(t, err) + return TestInstance[T]{ + App: xapp, + BankKeeper: bankKeeper, + AuthKeeper: authKeeper, + StakingKeeper: stKeeper, + AppManager: xapp.GetApp().AppManager, + ModuleManager: xapp.ModuleManager(), + TxDecoder: simsxv2.NewGenericTxDecoder[T](xapp.TxConfig()), + TXBuilder: simsxv2.NewSDKTXBuilder[T](xapp.TxConfig(), simsxv2.DefaultGenTxGas), + } +} + +func RunWithSeeds[T Tx](t *testing.T, seeds []int64) { + t.Helper() + cfg := cli.NewConfigFromFlags() + cfg.ChainID = SimAppChainID + for i := range seeds { + seed := seeds[i] + t.Run(fmt.Sprintf("seed: %d", seed), func(t *testing.T) { + t.Parallel() + RunWithSeed(t, NewSimApp[T], AppConfig(), cfg, seed) + }) + } +} + +func RunWithSeed[T Tx, V SimulationApp[T]](t *testing.T, appFactory AppFactory[T, V], appConfig depinject.Config, tCfg simtypes.Config, seed int64) { + t.Helper() + r := rand.New(rand.NewSource(seed)) + testInstance := SetupTestInstance[T, V](t, appFactory, appConfig) + accounts, genesisAppState, chainID, genesisTimestamp := prepareInitialGenesisState(testInstance.App, r, testInstance.BankKeeper, tCfg, testInstance.ModuleManager) + + appManager := testInstance.AppManager + appStore := testInstance.App.Store() + txConfig := testInstance.App.TxConfig() + rootCtx, done := context.WithCancel(context.Background()) + defer done() + initRsp, stateRoot := doChainInitWithGenesis(t, rootCtx, chainID, genesisTimestamp, appManager, testInstance.TxDecoder, genesisAppState, appStore) + activeValidatorSet := simsxv2.NewValSet().Update(initRsp.ValidatorUpdates) + valsetHistory := simsxv2.NewValSetHistory(1) + valsetHistory.Add(genesisTimestamp, activeValidatorSet) + + emptySimParams := make(map[string]json.RawMessage) // todo read sims params from disk as before + + modules := testInstance.ModuleManager.Modules() + msgFactoriesFn := prepareSimsMsgFactories(r, modules, simsx.ParamWeightSource(emptySimParams)) + + cs := chainState[T]{ + chainID: chainID, + blockTime: genesisTimestamp, + activeValidatorSet: activeValidatorSet, + valsetHistory: valsetHistory, + stateRoot: stateRoot, + app: appManager, + appStore: appStore, + txConfig: txConfig, + } + doMainLoop( + t, + rootCtx, + cs, + msgFactoriesFn, + r, + testInstance.AuthKeeper, + testInstance.BankKeeper, + accounts, + testInstance.TXBuilder, + testInstance.StakingKeeper, + ) + require.NoError(t, testInstance.App.Close(), "closing app") +} + +func prepareInitialGenesisState[T Tx]( + app SimulationApp[T], + r *rand.Rand, + bankKeeper BankKeeper, + tCfg simtypes.Config, + moduleManager ModuleManager, +) ([]simtypes.Account, json.RawMessage, string, time.Time) { + txConfig := app.TxConfig() + // todo: replace legacy testdata functions ? + appStateFn := simtestutil.AppStateFn( + app.AppCodec(), + txConfig.SigningContext().AddressCodec(), + txConfig.SigningContext().ValidatorAddressCodec(), + toLegacySimsModule(moduleManager.Modules()), + app.DefaultGenesis(), + ) + params := simulation.RandomParams(r) + accounts := slices.DeleteFunc(simtypes.RandomAccounts(r, params.NumKeys()), + func(acc simtypes.Account) bool { // remove blocked accounts + return bankKeeper.GetBlockedAddresses()[acc.AddressBech32] + }) + + appState, accounts, chainID, genesisTimestamp := appStateFn(r, accounts, tCfg) + return accounts, appState, chainID, genesisTimestamp +} + +func doChainInitWithGenesis[T Tx]( + t *testing.T, + ctx context.Context, + chainID string, + genesisTimestamp time.Time, + app appmanager.AppManager[T], + txDecoder transaction.Codec[T], + genesisAppState json.RawMessage, + appStore cometbfttypes.Store, +) (*server.BlockResponse, store.Hash) { + t.Helper() + genesisReq := &server.BlockRequest[T]{ + Height: 0, + Time: genesisTimestamp, + Hash: make([]byte, 32), + ChainId: chainID, + AppHash: make([]byte, 32), + IsGenesis: true, + } + + initialConsensusParams := &consensustypes.MsgUpdateParams{ + Block: &cmtproto.BlockParams{ + MaxBytes: 200000, + MaxGas: 100_000_000, + }, + Evidence: &cmtproto.EvidenceParams{ + MaxAgeNumBlocks: 302400, + MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration + MaxBytes: 10000, + }, + Validator: &cmtproto.ValidatorParams{PubKeyTypes: []string{cmttypes.ABCIPubKeyTypeEd25519, cmttypes.ABCIPubKeyTypeSecp256k1}}, + } + genesisCtx := context.WithValue(ctx, corecontext.CometParamsInitInfoKey, initialConsensusParams) + initRsp, genesisStateChanges, err := app.InitGenesis(genesisCtx, genesisReq, genesisAppState, txDecoder) + require.NoError(t, err) + + require.NoError(t, appStore.SetInitialVersion(0)) + changeSet, err := genesisStateChanges.GetStateChanges() + require.NoError(t, err) + + stateRoot, err := appStore.Commit(&store.Changeset{Changes: changeSet}) + require.NoError(t, err) + return initRsp, stateRoot +} + +type chainState[T Tx] struct { + chainID string + blockTime time.Time + activeValidatorSet simsxv2.WeightedValidators + valsetHistory *simsxv2.ValSetHistory + stateRoot store.Hash + app appmanager.AppManager[T] + appStore storev2.RootStore + txConfig client.TxConfig +} + +func doMainLoop[T Tx]( + t *testing.T, + rootCtx context.Context, + cs chainState[T], + nextMsgFactory func() simsx.SimMsgFactoryX, + r *rand.Rand, + authKeeper AuthKeeper, + bankKeeper simsx.BalanceSource, + accounts []simtypes.Account, + txBuilder simsxv2.TXBuilder[T], + stakingKeeper StakingKeeper, +) { + t.Helper() + blockTime := cs.blockTime + activeValidatorSet := cs.activeValidatorSet + if len(activeValidatorSet) == 0 { + t.Fatal("no active validators in chain setup") + return + } + valsetHistory := cs.valsetHistory + stateRoot := cs.stateRoot + chainID := cs.chainID + app := cs.app + appStore := cs.appStore + + const ( // todo: read from CLI instead + numBlocks = 100 // 500 default + maxTXPerBlock = 200 // 200 default + ) + + var ( + txSkippedCounter int + txTotalCounter int + ) + rootReporter := simsx.NewBasicSimulationReporter() + futureOpsReg := simsxv2.NewFutureOpsRegistry() + + for i := 0; i < numBlocks; i++ { + if len(activeValidatorSet) == 0 { + t.Skipf("run out of validators in block: %d\n", i+1) + return + } + blockTime = blockTime.Add(minTimePerBlock) + blockTime = blockTime.Add(time.Duration(int64(r.Intn(int(timeRangePerBlock/time.Second)))) * time.Second) + valsetHistory.Add(blockTime, activeValidatorSet) + blockReqN := &server.BlockRequest[T]{ + Height: uint64(1 + i), + Time: blockTime, + Hash: stateRoot, + AppHash: stateRoot, + ChainId: chainID, + } + + cometInfo := comet.Info{ + ValidatorsHash: nil, + Evidence: valsetHistory.MissBehaviour(r), + ProposerAddress: activeValidatorSet[0].Address, + LastCommit: activeValidatorSet.NewCommitInfo(r), + } + fOps, pos := futureOpsReg.PopScheduledFor(blockTime), 0 + addressCodec := cs.txConfig.SigningContext().AddressCodec() + simsCtx := context.WithValue(rootCtx, corecontext.CometInfoKey, cometInfo) // required for ContextAwareCometInfoService + resultHandlers := make([]simsx.SimDeliveryResultHandler, 0, maxTXPerBlock) + var txPerBlockCounter int + blockRsp, updates, err := app.DeliverSims(simsCtx, blockReqN, func(ctx context.Context) iter.Seq[T] { + return func(yield func(T) bool) { + unbondingTime, err := stakingKeeper.UnbondingTime(ctx) + require.NoError(t, err) + valsetHistory.SetMaxHistory(minBlocksInUnbondingPeriod(unbondingTime)) + testData := simsx.NewChainDataSource(ctx, r, authKeeper, bankKeeper, addressCodec, accounts...) + + for txPerBlockCounter < maxTXPerBlock { + txPerBlockCounter++ + mergedMsgFactory := func() simsx.SimMsgFactoryX { + if pos < len(fOps) { + pos++ + return fOps[pos-1] + } + return nextMsgFactory() + }() + reporter := rootReporter.WithScope(mergedMsgFactory.MsgType()) + if fx, ok := mergedMsgFactory.(simsx.HasFutureOpsRegistry); ok { + fx.SetFutureOpsRegistry(futureOpsReg) + continue + } + + // the stf context is required to access state via keepers + signers, msg := mergedMsgFactory.Create()(ctx, testData, reporter) + if reporter.IsSkipped() { + txSkippedCounter++ + require.NoError(t, reporter.Close()) + continue + } + resultHandlers = append(resultHandlers, mergedMsgFactory.DeliveryResultHandler()) + reporter.Success(msg) + require.NoError(t, reporter.Close()) + + tx, err := txBuilder.Build(ctx, authKeeper, signers, msg, r, chainID) + require.NoError(t, err) + if !yield(tx) { + return + } + } + } + }) + require.NoError(t, err, "%d, %s", blockReqN.Height, blockReqN.Time) + changeSet, err := updates.GetStateChanges() + require.NoError(t, err) + stateRoot, err = appStore.Commit(&store.Changeset{ + Version: blockReqN.Height, + Changes: changeSet, + }) + + require.NoError(t, err) + require.Equal(t, len(resultHandlers), len(blockRsp.TxResults), "txPerBlockCounter: %d, totalSkipped: %d", txPerBlockCounter, txSkippedCounter) + for i, v := range blockRsp.TxResults { + require.NoError(t, resultHandlers[i](v.Error)) + } + txTotalCounter += txPerBlockCounter + activeValidatorSet = activeValidatorSet.Update(blockRsp.ValidatorUpdates) + } + fmt.Println("+++ reporter:\n" + rootReporter.Summary().String()) + fmt.Printf("Tx total: %d skipped: %d\n", txTotalCounter, txSkippedCounter) +} + +func prepareSimsMsgFactories( + r *rand.Rand, + modules map[string]appmodulev2.AppModule, + weights simsx.WeightSource, +) func() simsx.SimMsgFactoryX { + moduleNames := maps.Keys(modules) + slices.Sort(moduleNames) // make deterministic + + // get all proposal types + proposalRegistry := simsx.NewUniqueTypeRegistry() + for _, n := range moduleNames { + switch xm := modules[n].(type) { // nolint: gocritic // extended in the future + case HasProposalMsgsX: + xm.ProposalMsgsX(weights, proposalRegistry) + // todo: register legacy and v1 msg proposals + } + } + // register all msg factories + factoryRegistry := simsx.NewUnorderedRegistry() + for _, n := range moduleNames { + switch xm := modules[n].(type) { + case HasWeightedOperationsX: + xm.WeightedOperationsX(weights, factoryRegistry) + case HasWeightedOperationsXWithProposals: + xm.WeightedOperationsX(weights, factoryRegistry, proposalRegistry.Iterator(), nil) + } + } + return simsxv2.NextFactoryFn(factoryRegistry.Elements(), r) +} + +func toLegacySimsModule(modules map[string]appmodule.AppModule) []module.AppModuleSimulation { + r := make([]module.AppModuleSimulation, 0, len(modules)) + names := maps.Keys(modules) + slices.Sort(names) // make deterministic + for _, v := range names { + if m, ok := modules[v].(module.AppModuleSimulation); ok { + r = append(r, m) + } + } + return r +} + +func minBlocksInUnbondingPeriod(unbondingTime time.Duration) int { + maxblocks := unbondingTime / maxTimePerBlock + return max(int(maxblocks)-1, 1) +} diff --git a/simapp/v2/sim_test.go b/simapp/v2/sim_test.go new file mode 100644 index 000000000000..f72eec17b549 --- /dev/null +++ b/simapp/v2/sim_test.go @@ -0,0 +1,8 @@ +package simapp + +import "testing" + +func TestSimsAppV2(t *testing.T) { + RunWithSeeds[Tx](t, defaultSeeds) + // RunWithSeed(t, cli.NewConfigFromFlags(), 99) +} diff --git a/simapp/v2/testdata/app.toml b/simapp/v2/testdata/app.toml new file mode 100644 index 000000000000..3c5f377ea0e7 --- /dev/null +++ b/simapp/v2/testdata/app.toml @@ -0,0 +1,68 @@ +[comet] +# min-retain-blocks defines the minimum block height offset from the current block being committed, such that all blocks past this offset are pruned from CometBFT. A value of 0 indicates that no blocks should be pruned. +min-retain-blocks = 0 +# index-events defines the set of events in the form {eventType}.{attributeKey}, which informs CometBFT what to index. If empty, all events will be indexed. +index-events = [] +# halt-height contains a non-zero block height at which a node will gracefully halt and shutdown that can be used to assist upgrades and testing. +halt-height = 0 +# halt-time contains a non-zero minimum block time (in Unix seconds) at which a node will gracefully halt and shutdown that can be used to assist upgrades and testing. +halt-time = 0 +# address defines the CometBFT RPC server address to bind to. +address = 'tcp://127.0.0.1:26658' +# transport defines the CometBFT RPC server transport protocol: socket, grpc +transport = 'socket' +# trace enables the CometBFT RPC server to output trace information about its internal operations. +trace = false +# standalone starts the application without the CometBFT node. The node should be started separately. +standalone = false + +# mempool defines the configuration for the SDK built-in app-side mempool implementations. +[comet.mempool] +# max-txs defines the maximum number of transactions that can be in the mempool. A value of 0 indicates an unbounded mempool, a negative value disables the app-side mempool. +max-txs = -1 + +[grpc] +# Enable defines if the gRPC server should be enabled. +enable = false +# Address defines the gRPC server address to bind to. +address = 'localhost:9090' +# MaxRecvMsgSize defines the max message size in bytes the server can receive. +# The default value is 10MB. +max-recv-msg-size = 10485760 +# MaxSendMsgSize defines the max message size in bytes the server can send. +# The default value is math.MaxInt32. +max-send-msg-size = 2147483647 + +[server] +# minimum-gas-prices defines the price which a validator is willing to accept for processing a transaction. A transaction's fees must meet the minimum of any denomination specified in this config (e.g. 0.25token1;0.0001token2). +minimum-gas-prices = '0stake' + +[store] +# The type of database for application and snapshots databases. +app-db-backend = 'goleveldb' + +[store.options] +# State storage database type. Currently we support: "pebble" and "rocksdb" +ss-type = 'pebble' +# State commitment database type. Currently we support: "iavl" and "iavl-v2" +sc-type = 'iavl' + +# Pruning options for state storage +[store.options.ss-pruning-option] +# Number of recent heights to keep on disk. +keep-recent = 2 +# Height interval at which pruned heights are removed from disk. +interval = 100 + +# Pruning options for state commitment +[store.options.sc-pruning-option] +# Number of recent heights to keep on disk. +keep-recent = 2 +# Height interval at which pruned heights are removed from disk. +interval = 100 + +[store.options.iavl-config] +# CacheSize set the size of the iavl tree cache. +cache-size = 100000 +# If true, the tree will work like no fast storage and always not upgrade fast storage. +skip-fast-storage-upgrade = true diff --git a/simapp/v2/testdata/config.toml b/simapp/v2/testdata/config.toml new file mode 100644 index 000000000000..e8ba6cffe18b --- /dev/null +++ b/simapp/v2/testdata/config.toml @@ -0,0 +1,598 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +# NOTE: Any path below can be absolute (e.g. "/var/myawesomeapp/data") or +# relative to the home directory (e.g. "data"). The home directory is +# "$HOME/.cometbft" by default, but could be changed via $CMTHOME env variable +# or --home cmd flag. + +# The version of the CometBFT binary that created or +# last modified the config file. Do not modify this. +version = "1.0.0-rc1" + +####################################################################### +### Main Base Config Options ### +####################################################################### + +# TCP or UNIX socket address of the ABCI application, +# or the name of an ABCI application compiled in with the CometBFT binary +proxy_app = "tcp://127.0.0.1:26658" + +# A custom human readable name for this node +moniker = "simapp-v2-node" + +# Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb | pebbledb +# * goleveldb (github.com/syndtr/goleveldb) +# - UNMAINTAINED +# - stable +# - pure go +# * cleveldb (uses levigo wrapper) +# - DEPRECATED +# - requires gcc +# - use cleveldb build tag (go build -tags cleveldb) +# * boltdb (uses etcd's fork of bolt - github.com/etcd-io/bbolt) +# - DEPRECATED +# - EXPERIMENTAL +# - stable +# - use boltdb build tag (go build -tags boltdb) +# * rocksdb (uses github.com/linxGnu/grocksdb) +# - EXPERIMENTAL +# - requires gcc +# - use rocksdb build tag (go build -tags rocksdb) +# * badgerdb (uses github.com/dgraph-io/badger) +# - EXPERIMENTAL +# - stable +# - use badgerdb build tag (go build -tags badgerdb) +# * pebbledb (uses github.com/cockroachdb/pebble) +# - EXPERIMENTAL +# - stable +# - pure go +# - use pebbledb build tag (go build -tags pebbledb) +db_backend = "goleveldb" + +# Database directory +db_dir = "data" + +# Output level for logging, including package level options +log_level = "*:warn,p2p:info,state:info" + +# Output format: 'plain' (colored text) or 'json' +log_format = "plain" + +##### additional base config options ##### + +# Path to the JSON file containing the initial validator set and other meta data +genesis_file = "config/genesis.json" + +# Path to the JSON file containing the private key to use as a validator in the consensus protocol +priv_validator_key_file = "config/priv_validator_key.json" + +# Path to the JSON file containing the last sign state of a validator +priv_validator_state_file = "data/priv_validator_state.json" + +# TCP or UNIX socket address for CometBFT to listen on for +# connections from an external PrivValidator process +priv_validator_laddr = "" + +# Path to the JSON file containing the private key to use for node authentication in the p2p protocol +node_key_file = "config/node_key.json" + +# Mechanism to connect to the ABCI application: socket | grpc +abci = "socket" + +# If true, query the ABCI app on connecting to a new peer +# so the app can decide if we should keep the connection or not +filter_peers = false + + +####################################################################### +### Advanced Configuration Options ### +####################################################################### + +####################################################### +### RPC Server Configuration Options ### +####################################################### +[rpc] + +# TCP or UNIX socket address for the RPC server to listen on +laddr = "tcp://127.0.0.1:26657" + +# A list of origins a cross-domain request can be executed from +# Default value '[]' disables cors support +# Use '["*"]' to allow any origin +cors_allowed_origins = [] + +# A list of methods the client is allowed to use with cross-domain requests +cors_allowed_methods = ["HEAD", "GET", "POST", ] + +# A list of non simple headers the client is allowed to use with cross-domain requests +cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time", ] + +# Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool +unsafe = false + +# Maximum number of simultaneous connections (including WebSocket). +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +max_open_connections = 900 + +# Maximum number of unique clientIDs that can /subscribe. +# If you're using /broadcast_tx_commit, set to the estimated maximum number +# of broadcast_tx_commit calls per block. +max_subscription_clients = 100 + +# Maximum number of unique queries a given client can /subscribe to. +# If you're using /broadcast_tx_commit, set to the estimated maximum number +# of broadcast_tx_commit calls per block. +max_subscriptions_per_client = 5 + +# Experimental parameter to specify the maximum number of events a node will +# buffer, per subscription, before returning an error and closing the +# subscription. Must be set to at least 100, but higher values will accommodate +# higher event throughput rates (and will use more memory). +experimental_subscription_buffer_size = 200 + +# Experimental parameter to specify the maximum number of RPC responses that +# can be buffered per WebSocket client. If clients cannot read from the +# WebSocket endpoint fast enough, they will be disconnected, so increasing this +# parameter may reduce the chances of them being disconnected (but will cause +# the node to use more memory). +# +# Must be at least the same as "experimental_subscription_buffer_size", +# otherwise connections could be dropped unnecessarily. This value should +# ideally be somewhat higher than "experimental_subscription_buffer_size" to +# accommodate non-subscription-related RPC responses. +experimental_websocket_write_buffer_size = 200 + +# If a WebSocket client cannot read fast enough, at present we may +# silently drop events instead of generating an error or disconnecting the +# client. +# +# Enabling this experimental parameter will cause the WebSocket connection to +# be closed instead if it cannot read fast enough, allowing for greater +# predictability in subscription behavior. +experimental_close_on_slow_client = false + +# How long to wait for a tx to be committed during /broadcast_tx_commit. +# WARNING: Using a value larger than 10s will result in increasing the +# global HTTP write timeout, which applies to all connections and endpoints. +# See https://github.com/tendermint/tendermint/issues/3435 +timeout_broadcast_tx_commit = "10s" + +# Maximum number of requests that can be sent in a batch +# If the value is set to '0' (zero-value), then no maximum batch size will be +# enforced for a JSON-RPC batch request. +max_request_batch_size = 10 + +# Maximum size of request body, in bytes +max_body_bytes = 1000000 + +# Maximum size of request header, in bytes +max_header_bytes = 1048576 + +# The path to a file containing certificate that is used to create the HTTPS server. +# Might be either absolute path or path related to CometBFT's config directory. +# If the certificate is signed by a certificate authority, +# the certFile should be the concatenation of the server's certificate, any intermediates, +# and the CA's certificate. +# NOTE: both tls_cert_file and tls_key_file must be present for CometBFT to create HTTPS server. +# Otherwise, HTTP server is run. +tls_cert_file = "" + +# The path to a file containing matching private key that is used to create the HTTPS server. +# Might be either absolute path or path related to CometBFT's config directory. +# NOTE: both tls_cert_file and tls_key_file must be present for CometBFT to create HTTPS server. +# Otherwise, HTTP server is run. +tls_key_file = "" + +# pprof listen address (https://golang.org/pkg/net/http/pprof) +pprof_laddr = "localhost:6060" + +####################################################### +### gRPC Server Configuration Options ### +####################################################### + +# +# Note that the gRPC server is exposed unauthenticated. It is critical that +# this server not be exposed directly to the public internet. If this service +# must be accessed via the public internet, please ensure that appropriate +# precautions are taken (e.g. fronting with a reverse proxy like nginx with TLS +# termination and authentication, using DDoS protection services like +# CloudFlare, etc.). +# + +[grpc] + +# TCP or UNIX socket address for the RPC server to listen on. If not specified, +# the gRPC server will be disabled. +laddr = "" + +# +# Each gRPC service can be turned on/off, and in some cases configured, +# individually. If the gRPC server is not enabled, all individual services' +# configurations are ignored. +# + +# The gRPC version service provides version information about the node and the +# protocols it uses. +[grpc.version_service] +enabled = true + +# The gRPC block service returns block information +[grpc.block_service] +enabled = true + +# The gRPC block results service returns block results for a given height. If no height +# is given, it will return the block results from the latest height. +[grpc.block_results_service] +enabled = true + +# +# Configuration for privileged gRPC endpoints, which should **never** be exposed +# to the public internet. +# +[grpc.privileged] +# The host/port on which to expose privileged gRPC endpoints. +laddr = "" + +# +# Configuration specifically for the gRPC pruning service, which is considered a +# privileged service. +# +[grpc.privileged.pruning_service] + +# Only controls whether the pruning service is accessible via the gRPC API - not +# whether a previously set pruning service retain height is honored by the +# node. See the [storage.pruning] section for control over pruning. +# +# Disabled by default. +enabled = false + +####################################################### +### P2P Configuration Options ### +####################################################### +[p2p] + +# Address to listen for incoming connections +laddr = "tcp://0.0.0.0:26656" + +# Address to advertise to peers for them to dial. If empty, will use the same +# port as the laddr, and will introspect on the listener to figure out the +# address. IP and port are required. Example: 159.89.10.97:26656 +external_address = "" + +# Comma separated list of seed nodes to connect to +seeds = "" + +# Comma separated list of nodes to keep persistent connections to +persistent_peers = "" + +# Path to address book +addr_book_file = "config/addrbook.json" + +# Set true for strict address routability rules +# Set false for private or local networks +addr_book_strict = true + +# Maximum number of inbound peers +max_num_inbound_peers = 40 + +# Maximum number of outbound peers to connect to, excluding persistent peers +max_num_outbound_peers = 10 + +# List of node IDs, to which a connection will be (re)established ignoring any existing limits +unconditional_peer_ids = "" + +# Maximum pause when redialing a persistent peer (if zero, exponential backoff is used) +persistent_peers_max_dial_period = "0s" + +# Time to wait before flushing messages out on the connection +flush_throttle_timeout = "10ms" + +# Maximum size of a message packet payload, in bytes +max_packet_msg_payload_size = 1024 + +# Rate at which packets can be sent, in bytes/second +send_rate = 5120000 + +# Rate at which packets can be received, in bytes/second +recv_rate = 5120000 + +# Set true to enable the peer-exchange reactor +pex = true + +# Seed mode, in which node constantly crawls the network and looks for +# peers. If another node asks it for addresses, it responds and disconnects. +# +# Does not work if the peer-exchange reactor is disabled. +seed_mode = false + +# Comma separated list of peer IDs to keep private (will not be gossiped to other peers) +private_peer_ids = "" + +# Toggle to disable guard against peers connecting from the same ip. +allow_duplicate_ip = false + +# Peer connection configuration. +handshake_timeout = "20s" +dial_timeout = "3s" + +####################################################### +### Mempool Configuration Options ### +####################################################### +[mempool] + +# The type of mempool for this node to use. +# +# Possible types: +# - "flood" : concurrent linked list mempool with flooding gossip protocol +# (default) +# - "nop" : nop-mempool (short for no operation; the ABCI app is responsible +# for storing, disseminating and proposing txs). "create_empty_blocks=false" is +# not supported. +type = "flood" + +# recheck (default: true) defines whether CometBFT should recheck the +# validity for all remaining transaction in the mempool after a block. +# Since a block affects the application state, some transactions in the +# mempool may become invalid. If this does not apply to your application, +# you can disable rechecking. +recheck = true + +# recheck_timeout is the time the application has during the rechecking process +# to return CheckTx responses, once all requests have been sent. Responses that +# arrive after the timeout expires are discarded. It only applies to +# non-local ABCI clients and when recheck is enabled. +recheck_timeout = "1s" + +# broadcast (default: true) defines whether the mempool should relay +# transactions to other peers. Setting this to false will stop the mempool +# from relaying transactions to other peers until they are included in a +# block. In other words, if Broadcast is disabled, only the peer you send +# the tx to will see it until it is included in a block. +broadcast = true + +# wal_dir (default: "") configures the location of the Write Ahead Log +# (WAL) for the mempool. The WAL is disabled by default. To enable, set +# wal_dir to where you want the WAL to be written (e.g. +# "data/mempool.wal"). +wal_dir = "" + +# Maximum number of transactions in the mempool +size = 5000 + +# Maximum size in bytes of a single transaction accepted into the mempool. +max_tx_bytes = 1048576 + +# The maximum size in bytes of all transactions stored in the mempool. +# This is the raw, total transaction size. For example, given 1MB +# transactions and a 5MB maximum mempool byte size, the mempool will +# only accept five transactions. +max_txs_bytes = 67108864 + +# Size of the cache (used to filter transactions we saw earlier) in transactions +cache_size = 10000 + +# Do not remove invalid transactions from the cache (default: false) +# Set to true if it's not possible for any invalid transaction to become valid +# again in the future. +keep-invalid-txs-in-cache = false + +# Experimental parameters to limit gossiping txs to up to the specified number of peers. +# We use two independent upper values for persistent and non-persistent peers. +# Unconditional peers are not affected by this feature. +# If we are connected to more than the specified number of persistent peers, only send txs to +# ExperimentalMaxGossipConnectionsToPersistentPeers of them. If one of those +# persistent peers disconnects, activate another persistent peer. +# Similarly for non-persistent peers, with an upper limit of +# ExperimentalMaxGossipConnectionsToNonPersistentPeers. +# If set to 0, the feature is disabled for the corresponding group of peers, that is, the +# number of active connections to that group of peers is not bounded. +# For non-persistent peers, if enabled, a value of 10 is recommended based on experimental +# performance results using the default P2P configuration. +experimental_max_gossip_connections_to_persistent_peers = 0 +experimental_max_gossip_connections_to_non_persistent_peers = 0 + +####################################################### +### State Sync Configuration Options ### +####################################################### +[statesync] +# State sync rapidly bootstraps a new node by discovering, fetching, and restoring a state machine +# snapshot from peers instead of fetching and replaying historical blocks. Requires some peers in +# the network to take and serve state machine snapshots. State sync is not attempted if the node +# has any local state (LastBlockHeight > 0). The node will have a truncated block history, +# starting from the height of the snapshot. +enable = false + +# RPC servers (comma-separated) for light client verification of the synced state machine and +# retrieval of state data for node bootstrapping. Also needs a trusted height and corresponding +# header hash obtained from a trusted source, and a period during which validators can be trusted. +# +# For Cosmos SDK-based chains, trust_period should usually be about 2/3 of the unbonding time (~2 +# weeks) during which they can be financially punished (slashed) for misbehavior. +rpc_servers = "" +trust_height = 0 +trust_hash = "" +trust_period = "168h0m0s" + +# Time to spend discovering snapshots before initiating a restore. +discovery_time = "15s" + +# Temporary directory for state sync snapshot chunks, defaults to the OS tempdir (typically /tmp). +# Will create a new, randomly named directory within, and remove it when done. +temp_dir = "" + +# The timeout duration before re-requesting a chunk, possibly from a different +# peer (default: 1 minute). +chunk_request_timeout = "10s" + +# The number of concurrent chunk fetchers to run (default: 1). +chunk_fetchers = "4" + +####################################################### +### Block Sync Configuration Options ### +####################################################### +[blocksync] + +# Block Sync version to use: +# +# In v0.37, v1 and v2 of the block sync protocols were deprecated. +# Please use v0 instead. +# +# 1) "v0" - the default block sync implementation +version = "v0" + +####################################################### +### Consensus Configuration Options ### +####################################################### +[consensus] + +wal_file = "data/cs.wal/wal" + +# How long we wait for a proposal block before prevoting nil +timeout_propose = "3s" +# How much timeout_propose increases with each round +timeout_propose_delta = "500ms" +# How long we wait after receiving +2/3 prevotes for “anything” (ie. not a single block or nil) +timeout_prevote = "1s" +# How much the timeout_prevote increases with each round +timeout_prevote_delta = "500ms" +# How long we wait after receiving +2/3 precommits for “anything” (ie. not a single block or nil) +timeout_precommit = "1s" +# How much the timeout_precommit increases with each round +timeout_precommit_delta = "500ms" +# How long we wait after committing a block, before starting on the new +# height (this gives us a chance to receive some more precommits, even +# though we already have +2/3). +# Set to 0 if you want to make progress as soon as the node has all the precommits. +timeout_commit = "5s" + +# Deprecated: set `timeout_commit` to 0 instead. +skip_timeout_commit = false + +# How many blocks to look back to check existence of the node's consensus votes before joining consensus +# When non-zero, the node will panic upon restart +# if the same consensus key was used to sign {double_sign_check_height} last blocks. +# So, validators should stop the state machine, wait for some blocks, and then restart the state machine to avoid panic. +double_sign_check_height = 0 + +# EmptyBlocks mode and possible interval between empty blocks +create_empty_blocks = true +create_empty_blocks_interval = "0s" + +# Reactor sleep duration parameters +peer_gossip_sleep_duration = "100ms" +peer_gossip_intraloop_sleep_duration = "0s" +peer_query_maj23_sleep_duration = "2s" + +####################################################### +### Storage Configuration Options ### +####################################################### +[storage] + +# Set to true to discard ABCI responses from the state store, which can save a +# considerable amount of disk space. Set to false to ensure ABCI responses are +# persisted. ABCI responses are required for /block_results RPC queries, and to +# reindex events in the command-line tool. +discard_abci_responses = false + +# The representation of keys in the database. +# The current representation of keys in Comet's stores is considered to be v1 +# Users can experiment with a different layout by setting this field to v2. +# Note that this is an experimental feature and switching back from v2 to v1 +# is not supported by CometBFT. +# If the database was initially created with v1, it is necessary to migrate the DB +# before switching to v2. The migration is not done automatically. +# v1 - the legacy layout existing in Comet prior to v1. +# v2 - Order preserving representation ordering entries by height. +experimental_db_key_layout = "v1" + +# If set to true, CometBFT will force compaction to happen for databases that support this feature. +# and save on storage space. Setting this to true is most benefits when used in combination +# with pruning as it will physically delete the entries marked for deletion. +# false by default (forcing compaction is disabled). +compact = false + +# To avoid forcing compaction every time, this parameter instructs CometBFT to wait +# the given amount of blocks to be pruned before triggering compaction. +# It should be tuned depending on the number of items. If your retain height is 1 block, +# it is too much of an overhead to try compaction every block. But it should also not be a very +# large multiple of your retain height as it might occur bigger overheads. +compaction_interval = "1000" + +[storage.pruning] + +# The time period between automated background pruning operations. +interval = "10s" + +# +# Storage pruning configuration relating only to the data companion. +# +[storage.pruning.data_companion] + +# Whether automatic pruning respects values set by the data companion. Disabled +# by default. All other parameters in this section are ignored when this is +# disabled. +# +# If disabled, only the application retain height will influence block pruning +# (but not block results pruning). Only enabling this at a later stage will +# potentially mean that blocks below the application-set retain height at the +# time will not be available to the data companion. +enabled = false + +# The initial value for the data companion block retain height if the data +# companion has not yet explicitly set one. If the data companion has already +# set a block retain height, this is ignored. +initial_block_retain_height = 0 + +# The initial value for the data companion block results retain height if the +# data companion has not yet explicitly set one. If the data companion has +# already set a block results retain height, this is ignored. +initial_block_results_retain_height = 0 + +####################################################### +### Transaction Indexer Configuration Options ### +####################################################### +[tx_index] + +# What indexer to use for transactions +# +# The application will set which txs to index. In some cases a node operator will be able +# to decide which txs to index based on configuration set in the application. +# +# Options: +# 1) "null" +# 2) "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). +# - When "kv" is chosen "tx.height" and "tx.hash" will always be indexed. +# 3) "psql" - the indexer services backed by PostgreSQL. +# When "kv" or "psql" is chosen "tx.height" and "tx.hash" will always be indexed. +indexer = "kv" + +# The PostgreSQL connection configuration, the connection format: +# postgresql://:@:/? +psql-conn = "" + +####################################################### +### Instrumentation Configuration Options ### +####################################################### +[instrumentation] + +# When true, Prometheus metrics are served under /metrics on +# PrometheusListenAddr. +# Check out the documentation for the list of available metrics. +prometheus = false + +# Address to listen for Prometheus collector(s) connections +prometheus_listen_addr = ":26660" + +# Maximum number of simultaneous connections. +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +max_open_connections = 3 + +# Instrumentation namespace +namespace = "cometbft" diff --git a/simsx/msg_factory.go b/simsx/msg_factory.go index 1c6b6e4b9611..c2e751c84f56 100644 --- a/simsx/msg_factory.go +++ b/simsx/msg_factory.go @@ -61,24 +61,20 @@ type ResultHandlingSimMsgFactory[T sdk.Msg] struct { // NewSimMsgFactoryWithDeliveryResultHandler constructor func NewSimMsgFactoryWithDeliveryResultHandler[T sdk.Msg](f FactoryMethodWithDeliveryResultHandler[T]) *ResultHandlingSimMsgFactory[T] { - // the result handler is always called after the factory. so we initialize it lazy for syntactic sugar and simplicity - // in the message factory function that is implemented by the users - var lazyResultHandler SimDeliveryResultHandler r := &ResultHandlingSimMsgFactory[T]{ - resultHandler: func(err error) error { - return lazyResultHandler(err) - }, + resultHandler: expectNoError, } r.SimMsgFactoryFn = func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg T) { - signer, msg, lazyResultHandler = f(ctx, testData, reporter) - if lazyResultHandler == nil { - lazyResultHandler = expectNoError + signer, msg, r.resultHandler = f(ctx, testData, reporter) + if r.resultHandler == nil { + r.resultHandler = expectNoError } return } return r } +// DeliveryResultHandler result handler of the last msg factory invocation func (f ResultHandlingSimMsgFactory[T]) DeliveryResultHandler() SimDeliveryResultHandler { return f.resultHandler } diff --git a/simsx/registry.go b/simsx/registry.go index 6aee009f379d..aeb3577ee081 100644 --- a/simsx/registry.go +++ b/simsx/registry.go @@ -160,6 +160,12 @@ func NewUniqueTypeRegistry() UniqueTypeRegistry { } func (s UniqueTypeRegistry) Add(weight uint32, f SimMsgFactoryX) { + if weight == 0 { + return + } + if f == nil { + panic("message factory must not be nil") + } msgType := f.MsgType() msgTypeURL := sdk.MsgTypeURL(msgType) if _, exists := s[msgTypeURL]; exists { @@ -170,8 +176,7 @@ func (s UniqueTypeRegistry) Add(weight uint32, f SimMsgFactoryX) { // Iterator returns an iterator function for a Go for loop sorted by weight desc. func (s UniqueTypeRegistry) Iterator() WeightedProposalMsgIter { - x := maps.Values(s) - sortedWeightedFactory := slices.SortedFunc(x, func(a, b WeightedFactory) int { + sortedWeightedFactory := slices.SortedFunc(maps.Values(s), func(a, b WeightedFactory) int { return a.Compare(b) }) @@ -184,6 +189,33 @@ func (s UniqueTypeRegistry) Iterator() WeightedProposalMsgIter { } } +var _ Registry = &UnorderedRegistry{} + +// UnorderedRegistry represents a collection of WeightedFactory elements without guaranteed order. +// It is used to maintain factories coupled with their associated weights for simulation purposes. +type UnorderedRegistry []WeightedFactory + +func NewUnorderedRegistry() *UnorderedRegistry { + r := make(UnorderedRegistry, 0) + return &r +} + +// Add appends a new WeightedFactory with the provided weight and factory to the UnorderedRegistry. +func (x *UnorderedRegistry) Add(weight uint32, f SimMsgFactoryX) { + if weight == 0 { + return + } + if f == nil { + panic("message factory must not be nil") + } + *x = append(*x, WeightedFactory{Weight: weight, Factory: f}) +} + +// Elements returns all collected elements +func (x *UnorderedRegistry) Elements() []WeightedFactory { + return *x +} + // WeightedFactory is a Weight Factory tuple type WeightedFactory struct { Weight uint32 diff --git a/simsx/registry_test.go b/simsx/registry_test.go index e8aba1239c7a..b9ceb3d96798 100644 --- a/simsx/registry_test.go +++ b/simsx/registry_test.go @@ -119,6 +119,9 @@ func TestUniqueTypeRegistry(t *testing.T) { exampleFactory := SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) { return []SimAccount{}, nil }) + exampleFactory2 := SimMsgFactoryFn[*testdata.MsgCreateDog](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.MsgCreateDog) { + return []SimAccount{}, nil + }) specs := map[string]struct { src []WeightedFactory @@ -129,6 +132,10 @@ func TestUniqueTypeRegistry(t *testing.T) { src: []WeightedFactory{{Weight: 1, Factory: exampleFactory}}, exp: []WeightedFactoryMethod{{Weight: 1, Factory: exampleFactory.Create()}}, }, + "sorted": { + src: []WeightedFactory{{Weight: 2, Factory: exampleFactory2}, {Weight: 1, Factory: exampleFactory}}, + exp: []WeightedFactoryMethod{{Weight: 1, Factory: exampleFactory.Create()}, {Weight: 2, Factory: exampleFactory2.Create()}}, + }, "duplicate": { src: []WeightedFactory{{Weight: 1, Factory: exampleFactory}, {Weight: 2, Factory: exampleFactory}}, expErr: true, diff --git a/simsx/v2/msg_factory.go b/simsx/v2/msg_factory.go new file mode 100644 index 000000000000..7b65db661be8 --- /dev/null +++ b/simsx/v2/msg_factory.go @@ -0,0 +1,31 @@ +package v2 + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/simsx" +) + +// NextFactoryFn shuffles and processes a list of weighted factories, returning a selection function for factory objects. +func NextFactoryFn(factories []simsx.WeightedFactory, r *rand.Rand) func() simsx.SimMsgFactoryX { + factCount := len(factories) + r.Shuffle(factCount, func(i, j int) { + factories[i], factories[j] = factories[j], factories[i] + }) + var totalWeight int + for k := range factories { + totalWeight += k + } + return func() simsx.SimMsgFactoryX { + // this is copied from old sims WeightedOperations.getSelectOpFn + x := r.Intn(totalWeight) + for i := 0; i < factCount; i++ { + if x <= int(factories[i].Weight) { + return factories[i].Factory + } + x -= int(factories[i].Weight) + } + // shouldn't happen + return factories[0].Factory + } +} diff --git a/simsx/v2/registry.go b/simsx/v2/registry.go new file mode 100644 index 000000000000..8811fa3702fb --- /dev/null +++ b/simsx/v2/registry.go @@ -0,0 +1,65 @@ +package v2 + +import ( + "math/rand" + "time" + + "github.com/huandu/skiplist" + + "github.com/cosmos/cosmos-sdk/simsx" +) + +// FutureOpsRegistry is a registry for scheduling and retrieving operations mapped to future block times. +type FutureOpsRegistry struct { + list *skiplist.SkipList +} + +// NewFutureOpsRegistry constructor +func NewFutureOpsRegistry() *FutureOpsRegistry { + list := skiplist.New(timeComparator{}) + list.SetRandSource(rand.NewSource(1)) + return &FutureOpsRegistry{list: list} +} + +// Add schedules a new operation for the given block time +func (l *FutureOpsRegistry) Add(blockTime time.Time, fx simsx.SimMsgFactoryX) { + if fx == nil { + panic("message factory must not be nil") + } + if blockTime.IsZero() { + return + } + var scheduledOps []simsx.SimMsgFactoryX + if e := l.list.Get(blockTime); e != nil { + scheduledOps = e.Value.([]simsx.SimMsgFactoryX) + } + scheduledOps = append(scheduledOps, fx) + l.list.Set(blockTime, scheduledOps) +} + +// PopScheduledFor retrieves and removes all scheduled operations up to the specified block time from the registry. +func (l *FutureOpsRegistry) PopScheduledFor(blockTime time.Time) []simsx.SimMsgFactoryX { + var r []simsx.SimMsgFactoryX + for { + e := l.list.Front() + if e == nil || e.Key().(time.Time).After(blockTime) { + break + } + r = append(r, e.Value.([]simsx.SimMsgFactoryX)...) + l.list.RemoveFront() + } + return r +} + +var _ skiplist.Comparable = timeComparator{} + +// used for SkipList +type timeComparator struct{} + +func (t timeComparator) Compare(lhs, rhs interface{}) int { + return lhs.(time.Time).Compare(rhs.(time.Time)) +} + +func (t timeComparator) CalcScore(key interface{}) float64 { + return float64(key.(time.Time).UnixNano()) +} diff --git a/simsx/v2/txutils.go b/simsx/v2/txutils.go new file mode 100644 index 000000000000..22005b84053a --- /dev/null +++ b/simsx/v2/txutils.go @@ -0,0 +1,197 @@ +package v2 + +import ( + "context" + "errors" + "math/rand" + + "cosmossdk.io/core/transaction" + + "github.com/cosmos/cosmos-sdk/client" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/simsx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsign "github.com/cosmos/cosmos-sdk/x/auth/signing" +) + +const DefaultGenTxGas = 10_000_000 + +type Tx = transaction.Tx + +// TXBuilder abstract transaction builder +type TXBuilder[T Tx] interface { + // Build creates a signed transaction + Build(ctx context.Context, + ak simsx.AccountSource, + senders []simsx.SimAccount, + msg sdk.Msg, + r *rand.Rand, + chainID string, + ) (T, error) +} + +var _ TXBuilder[Tx] = TXBuilderFn[Tx](nil) + +// TXBuilderFn adapter that implements the TXBuilder interface +type TXBuilderFn[T Tx] func(ctx context.Context, ak simsx.AccountSource, senders []simsx.SimAccount, msg sdk.Msg, r *rand.Rand, chainID string) (T, error) + +func (b TXBuilderFn[T]) Build(ctx context.Context, ak simsx.AccountSource, senders []simsx.SimAccount, msg sdk.Msg, r *rand.Rand, chainID string) (T, error) { + return b(ctx, ak, senders, msg, r, chainID) +} + +// NewSDKTXBuilder constructor to create a signed transaction builder for sdk.Tx type. +func NewSDKTXBuilder[T Tx](txConfig client.TxConfig, defaultGas uint64) TXBuilder[T] { + return TXBuilderFn[T](func(ctx context.Context, ak simsx.AccountSource, senders []simsx.SimAccount, msg sdk.Msg, r *rand.Rand, chainID string) (tx T, err error) { + accountNumbers := make([]uint64, len(senders)) + sequenceNumbers := make([]uint64, len(senders)) + for i := 0; i < len(senders); i++ { + acc := ak.GetAccount(ctx, senders[i].Address) + accountNumbers[i] = acc.GetAccountNumber() + sequenceNumbers[i] = acc.GetSequence() + } + fees := senders[0].LiquidBalance().RandFees() + sdkTx, err := GenSignedMockTx( + r, + txConfig, + []sdk.Msg{msg}, + fees, + defaultGas, + chainID, + accountNumbers, + sequenceNumbers, + simsx.Collect(senders, func(a simsx.SimAccount) cryptotypes.PrivKey { return a.PrivKey })..., + ) + if err != nil { + return tx, err + } + out, ok := sdkTx.(T) + if !ok { + return out, errors.New("unexpected Tx type") + } + return out, nil + }) +} + +// GenSignedMockTx generates a signed mock transaction. +func GenSignedMockTx( + r *rand.Rand, + txConfig client.TxConfig, + msgs []sdk.Msg, + feeAmt sdk.Coins, + gas uint64, + chainID string, + accNums, accSeqs []uint64, + priv ...cryptotypes.PrivKey, +) (sdk.Tx, error) { + sigs := make([]signing.SignatureV2, len(priv)) + + // create a random length memo + memo := simulation.RandStringOfLength(r, simulation.RandIntBetween(r, 0, 100)) + + signMode, err := authsign.APISignModeToInternal(txConfig.SignModeHandler().DefaultMode()) + if err != nil { + return nil, err + } + + // 1st round: set SignatureV2 with empty signatures, to set correct + // signer infos. + for i, p := range priv { + sigs[i] = signing.SignatureV2{ + PubKey: p.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: signMode, + }, + Sequence: accSeqs[i], + } + } + + tx := txConfig.NewTxBuilder() + err = tx.SetMsgs(msgs...) + if err != nil { + return nil, err + } + err = tx.SetSignatures(sigs...) + if err != nil { + return nil, err + } + tx.SetMemo(memo) + tx.SetFeeAmount(feeAmt) + tx.SetGasLimit(gas) + + // 2nd round: once all signer infos are set, every signer can sign. + for i, p := range priv { + signerData := authsign.SignerData{ + Address: sdk.AccAddress(p.PubKey().Address()).String(), + ChainID: chainID, + AccountNumber: accNums[i], + Sequence: accSeqs[i], + PubKey: p.PubKey(), + } + + signBytes, err := authsign.GetSignBytesAdapter( + context.Background(), txConfig.SignModeHandler(), signMode, signerData, + tx.GetTx()) + if err != nil { + panic(err) + } + sig, err := p.Sign(signBytes) + if err != nil { + panic(err) + } + sigs[i].Data.(*signing.SingleSignatureData).Signature = sig + } + err = tx.SetSignatures(sigs...) + if err != nil { + panic(err) + } + + return tx.GetTx(), nil +} + +var _ transaction.Codec[Tx] = &GenericTxDecoder[Tx]{} + +// GenericTxDecoder Encoder type that implements transaction.Codec +type GenericTxDecoder[T Tx] struct { + txConfig client.TxConfig +} + +// NewGenericTxDecoder constructor +func NewGenericTxDecoder[T Tx](txConfig client.TxConfig) *GenericTxDecoder[T] { + return &GenericTxDecoder[T]{txConfig: txConfig} +} + +// Decode implements transaction.Codec. +func (t *GenericTxDecoder[T]) Decode(bz []byte) (T, error) { + var out T + tx, err := t.txConfig.TxDecoder()(bz) + if err != nil { + return out, err + } + + var ok bool + out, ok = tx.(T) + if !ok { + return out, errors.New("unexpected Tx type") + } + + return out, nil +} + +// DecodeJSON implements transaction.Codec. +func (t *GenericTxDecoder[T]) DecodeJSON(bz []byte) (T, error) { + var out T + tx, err := t.txConfig.TxJSONDecoder()(bz) + if err != nil { + return out, err + } + + var ok bool + out, ok = tx.(T) + if !ok { + return out, errors.New("unexpected Tx type") + } + + return out, nil +} diff --git a/simsx/v2/valset.go b/simsx/v2/valset.go new file mode 100644 index 000000000000..00b68f21d9e0 --- /dev/null +++ b/simsx/v2/valset.go @@ -0,0 +1,109 @@ +package v2 + +import ( + "bytes" + "crypto/sha256" + "fmt" + "math/rand" + "slices" + + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/comet" + + "github.com/cosmos/cosmos-sdk/simsx" +) + +// WeightedValidator represents a validator for usage in the sims runner. +type WeightedValidator struct { + Power int64 + Address []byte + Offline bool +} + +// Compare determines the order between two WeightedValidator instances. +// Returns -1 if the caller has higher Power, 1 if it has lower Power, and defaults to comparing their Address bytes. +func (a WeightedValidator) Compare(b WeightedValidator) int { + switch { + case a.Power < b.Power: + return 1 + case a.Power > b.Power: + return -1 + default: + return bytes.Compare(a.Address, b.Address) + } +} + +// NewValSet constructor +func NewValSet() WeightedValidators { + return make(WeightedValidators, 0) +} + +// WeightedValidators represents a slice of WeightedValidator, used for managing and processing validator sets. +type WeightedValidators []WeightedValidator + +func (v WeightedValidators) Update(updates []appmodulev2.ValidatorUpdate) WeightedValidators { + if len(updates) == 0 { + return v + } + const truncatedSize = 20 + valUpdates := simsx.Collect(updates, func(u appmodulev2.ValidatorUpdate) WeightedValidator { + hash := sha256.Sum256(u.PubKey) + return WeightedValidator{Power: u.Power, Address: hash[:truncatedSize]} + }) + newValset := slices.Clone(v) + for _, u := range valUpdates { + pos := slices.IndexFunc(newValset, func(val WeightedValidator) bool { + return bytes.Equal(u.Address, val.Address) + }) + if pos == -1 { // new address + if u.Power > 0 { + newValset = append(newValset, u) + } else { + panic(fmt.Sprintf("Adding validator with power 0: %X\n", u.Address)) + } + continue + } + if u.Power == 0 { + newValset = append(newValset[0:pos], newValset[pos+1:]...) + continue + } + newValset[pos].Power = u.Power + } + + newValset = slices.DeleteFunc(newValset, func(validator WeightedValidator) bool { + return validator.Power == 0 + }) + + // sort vals by Power + slices.SortFunc(newValset, func(a, b WeightedValidator) int { + return a.Compare(b) + }) + return newValset +} + +// NewCommitInfo build Comet commit info for the validator set +func (v WeightedValidators) NewCommitInfo(r *rand.Rand) comet.CommitInfo { + // todo: refactor to transition matrix? + if r.Intn(10) == 0 { + v[r.Intn(len(v))].Offline = r.Intn(2) == 0 + } + votes := make([]comet.VoteInfo, 0, len(v)) + for i := range v { + if v[i].Offline { + continue + } + votes = append(votes, comet.VoteInfo{ + Validator: comet.Validator{Address: v[i].Address, Power: v[i].Power}, + BlockIDFlag: comet.BlockIDFlagCommit, + }) + } + return comet.CommitInfo{Round: int32(r.Uint32()), Votes: votes} +} + +func (v WeightedValidators) TotalPower() int64 { + var r int64 + for _, val := range v { + r += val.Power + } + return r +} diff --git a/simsx/v2/valset_history.go b/simsx/v2/valset_history.go new file mode 100644 index 000000000000..9c05978ef900 --- /dev/null +++ b/simsx/v2/valset_history.go @@ -0,0 +1,78 @@ +package v2 + +import ( + "math/rand" + "slices" + "time" + + "cosmossdk.io/core/comet" + + "github.com/cosmos/cosmos-sdk/simsx" +) + +type historicValSet struct { + blockTime time.Time + vals WeightedValidators +} +type ValSetHistory struct { + maxElements int + blockOffset int + vals []historicValSet +} + +func NewValSetHistory(maxElements int) *ValSetHistory { + return &ValSetHistory{ + maxElements: maxElements, + blockOffset: 1, // start at height 1 + vals: make([]historicValSet, 0, maxElements), + } +} + +func (h *ValSetHistory) Add(blockTime time.Time, vals WeightedValidators) { + vals = slices.DeleteFunc(vals, func(validator WeightedValidator) bool { + return validator.Power == 0 + }) + slices.SortFunc(vals, func(a, b WeightedValidator) int { + return b.Compare(a) + }) + newEntry := historicValSet{blockTime: blockTime, vals: vals} + if len(h.vals) >= h.maxElements { + h.vals = append(h.vals[1:], newEntry) + h.blockOffset++ + return + } + h.vals = append(h.vals, newEntry) +} + +// MissBehaviour determines if a random validator misbehaves, creating and returning evidence for duplicate voting. +// Returns a slice of comet.Evidence if misbehavior is detected; otherwise, returns nil. +// Has a 1% chance of generating evidence for a validator's misbehavior. +// Recursively checks for other misbehavior instances and combines their evidence if any. +// Utilizes a random generator to select a validator and evidence-related attributes. +func (h *ValSetHistory) MissBehaviour(r *rand.Rand) []comet.Evidence { + if r.Intn(100) != 0 { // 1% chance + return nil + } + n := r.Intn(len(h.vals)) + badVal := simsx.OneOf(r, h.vals[n].vals) + evidence := comet.Evidence{ + Type: comet.DuplicateVote, + Validator: comet.Validator{Address: badVal.Address, Power: badVal.Power}, + Height: int64(h.blockOffset + n), + Time: h.vals[n].blockTime, + TotalVotingPower: h.vals[n].vals.TotalPower(), + } + if otherEvidence := h.MissBehaviour(r); otherEvidence != nil { + return append([]comet.Evidence{evidence}, otherEvidence...) + } + return []comet.Evidence{evidence} +} + +func (h *ValSetHistory) SetMaxHistory(v int) { + h.maxElements = v + if len(h.vals) > h.maxElements { + diff := len(h.vals) - h.maxElements + h.vals = h.vals[diff:] + h.blockOffset += diff + } +} diff --git a/store/v2/db/db.go b/store/v2/db/db.go index dbe67e59792c..4b63766c22a5 100644 --- a/store/v2/db/db.go +++ b/store/v2/db/db.go @@ -14,6 +14,8 @@ const ( DBTypePebbleDB DBType = "pebbledb" DBTypePrefixDB DBType = "prefixdb" + DBTypeMemDB DBType = "memdb" // used for sims + DBFileSuffix string = ".db" ) @@ -24,6 +26,8 @@ func NewDB(dbType DBType, name, dataDir string, opts coreserver.DynamicConfig) ( case DBTypePebbleDB: return NewPebbleDB(name, dataDir) + case DBTypeMemDB: + return NewMemDB(), nil } return nil, fmt.Errorf("unsupported db type: %s", dbType) diff --git a/tests/integration/distribution/genesis_test.go b/tests/integration/distribution/genesis_test.go index a113341496ae..a7612713ae60 100644 --- a/tests/integration/distribution/genesis_test.go +++ b/tests/integration/distribution/genesis_test.go @@ -4,22 +4,23 @@ import ( "encoding/json" "testing" + abci "github.com/cometbft/cometbft/api/cometbft/abci/v1" + "github.com/stretchr/testify/suite" + corestore "cosmossdk.io/core/store" coretesting "cosmossdk.io/core/testing" "cosmossdk.io/depinject" "cosmossdk.io/log" sdkmath "cosmossdk.io/math" + bankkeeper "cosmossdk.io/x/bank/keeper" "cosmossdk.io/x/distribution/keeper" "cosmossdk.io/x/distribution/types" stakingkeeper "cosmossdk.io/x/staking/keeper" - abci "github.com/cometbft/cometbft/api/cometbft/abci/v1" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/runtime" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/suite" - - bankkeeper "cosmossdk.io/x/bank/keeper" _ "github.com/cosmos/cosmos-sdk/x/auth" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" ) diff --git a/tests/integration/v2/types/fuzz_test.go b/tests/integration/v2/types/fuzz_test.go index 973408439c25..a92bf8e53fca 100644 --- a/tests/integration/v2/types/fuzz_test.go +++ b/tests/integration/v2/types/fuzz_test.go @@ -14,10 +14,6 @@ import ( "github.com/cosmos/cosmos-sdk/types/query" ) -const ( - balancesPrefix = 0x2 -) - type fuzzTestSuite struct { paginationTestSuite } diff --git a/tests/systemtests/tx_test.go b/tests/systemtests/tx_test.go index abc17b86b76b..3759fbdb01a5 100644 --- a/tests/systemtests/tx_test.go +++ b/tests/systemtests/tx_test.go @@ -1118,7 +1118,7 @@ func TestSimulateTx_GasImprovements(t *testing.T) { testCases := []struct { name string simulateArgs []string - txArgs []string + txArgs []string }{ { "simulate without fees", @@ -1152,17 +1152,17 @@ func TestSimulateTx_GasImprovements(t *testing.T) { // create unsign tx res := cli.RunCommandWithArgs(tc.simulateArgs...) txFile := systest.StoreTempFile(t, []byte(res)) - + res = cli.RunCommandWithArgs("tx", "sign", txFile.Name(), "--from="+valAddr, "--chain-id="+cli.ChainID(), "--keyring-backend=test", "--home="+systest.Sut.NodeDir(0)) signedTxFile := systest.StoreTempFile(t, []byte(res)) - + res = cli.RunCommandWithArgs("tx", "encode", signedTxFile.Name()) txBz, err := base64.StdEncoding.DecodeString(res) require.NoError(t, err) - + reqBz, err := json.Marshal(&tx.SimulateRequest{TxBytes: txBz}) require.NoError(t, err) - + resBz, err := testutil.PostRequest(fmt.Sprintf("%s/cosmos/tx/v1beta1/simulate", baseURL), "application/json", reqBz) require.NoError(t, err) gasUsed := gjson.Get(string(resBz), "gas_info.gas_used").Int() @@ -1184,7 +1184,7 @@ func TestSimulateTx_GasImprovements(t *testing.T) { fmt.Println("gasAdjustment", i, gasAdjustment[i]) } } - + // Calculate average adjustments total := 0.0 for i := 0; i < txlen; i++ { diff --git a/testutil/sims/state_helpers.go b/testutil/sims/state_helpers.go index 76d06b8959d6..8e229891c7e7 100644 --- a/testutil/sims/state_helpers.go +++ b/testutil/sims/state_helpers.go @@ -212,6 +212,7 @@ func AppStateRandomizedFn( // AppStateFromGenesisFileFn util function to generate the genesis AppState // from a genesis.json file. +// Deprecated: the private keys are not matching the accounts read from app state func AppStateFromGenesisFileFn(_ io.Reader, cdc codec.JSONCodec, genesisFile string) (genutiltypes.AppGenesis, []simtypes.Account, error) { file, err := os.Open(filepath.Clean(genesisFile)) if err != nil { @@ -233,6 +234,8 @@ func AppStateFromGenesisFileFn(_ io.Reader, cdc codec.JSONCodec, genesisFile str return *genesis, newAccs, nil } +// AccountsFromAppState +// Deprecated: the private keys are not matching the accounts read from app state func AccountsFromAppState(cdc codec.JSONCodec, appStateJSON json.RawMessage) ([]simtypes.Account, error) { var appState map[string]json.RawMessage if err := json.Unmarshal(appStateJSON, &appState); err != nil { diff --git a/testutil/sims/tx_helpers.go b/testutil/sims/tx_helpers.go index 9f71f8aadb20..ac5685de2cff 100644 --- a/testutil/sims/tx_helpers.go +++ b/testutil/sims/tx_helpers.go @@ -22,7 +22,16 @@ import ( ) // GenSignedMockTx generates a signed mock transaction. -func GenSignedMockTx(r *rand.Rand, txConfig client.TxConfig, msgs []sdk.Msg, feeAmt sdk.Coins, gas uint64, chainID string, accNums, accSeqs []uint64, priv ...cryptotypes.PrivKey) (sdk.Tx, error) { +func GenSignedMockTx( + r *rand.Rand, + txConfig client.TxConfig, + msgs []sdk.Msg, + feeAmt sdk.Coins, + gas uint64, + chainID string, + accNums, accSeqs []uint64, + priv ...cryptotypes.PrivKey, +) (sdk.Tx, error) { sigs := make([]signing.SignatureV2, len(priv)) // create a random length memo diff --git a/testutil/testdata/tx.go b/testutil/testdata/tx.go index 6de23c818b42..cf2a93c06318 100644 --- a/testutil/testdata/tx.go +++ b/testutil/testdata/tx.go @@ -1,12 +1,13 @@ package testdata import ( - "cosmossdk.io/math" "testing" "gotest.tools/v3/assert" "pgregory.net/rapid" + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256r1" diff --git a/x/auth/simulation/genesis.go b/x/auth/simulation/genesis.go index 0e9f7e93eb4a..7c18882fd325 100644 --- a/x/auth/simulation/genesis.go +++ b/x/auth/simulation/genesis.go @@ -26,13 +26,6 @@ func RandomGenesisAccounts(simState *module.SimulationState) types.GenesisAccoun for i, acc := range simState.Accounts { bacc := types.NewBaseAccountWithAddress(acc.Address) - // check if vesting module is enabled - // if not, just use base account - if _, ok := simState.GenState["vesting"]; !ok { - genesisAccs[i] = bacc - continue - } - // Only consider making a vesting account once the initial bonded validator // set is exhausted due to needing to track DelegatedVesting. if !(int64(i) > simState.NumBonded && simState.Rand.Intn(100) < 50) { diff --git a/x/gov/simulation/msg_factory.go b/x/gov/simulation/msg_factory.go index b1267a9ea262..efe1b39790df 100644 --- a/x/gov/simulation/msg_factory.go +++ b/x/gov/simulation/msg_factory.go @@ -131,8 +131,7 @@ func submitProposalWithVotesScheduled( proposalMsgs ...sdk.Msg, ) ([]simsx.SimAccount, *v1.MsgSubmitProposal) { r := testData.Rand() - expedited := true - // expedited := r.Bool() + expedited := r.Bool() params := must(k.Params.Get(ctx)) minDeposits := params.MinDeposit if expedited { diff --git a/x/nft/simulation/msg_factory.go b/x/nft/simulation/msg_factory.go index 14db2666dee9..8ec1c63ce6c2 100644 --- a/x/nft/simulation/msg_factory.go +++ b/x/nft/simulation/msg_factory.go @@ -14,7 +14,9 @@ func MsgSendFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*nft.MsgSend] { return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *nft.MsgSend) { from := testData.AnyAccount(reporter, simsx.WithSpendableBalance()) to := testData.AnyAccount(reporter, simsx.ExcludeAccounts(from)) - + if reporter.IsSkipped() { + return nil, nil + } n, err := randNFT(ctx, testData.Rand(), k, from.Address) if err != nil { reporter.Skip(err.Error()) diff --git a/x/staking/simulation/msg_factory.go b/x/staking/simulation/msg_factory.go index a0a8c4a9df24..d2ebce042db3 100644 --- a/x/staking/simulation/msg_factory.go +++ b/x/staking/simulation/msg_factory.go @@ -321,21 +321,11 @@ func MsgRotateConsPubKeyFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.M return nil, nil } // check whether the new cons key associated with another validator - newConsAddr := sdk.ConsAddress(otherAccount.ConsKey.PubKey().Address()) - - if _, err := k.GetValidatorByConsAddr(ctx, newConsAddr); err == nil { - reporter.Skip("cons key already used") + assertKeyUnused(ctx, reporter, k, otherAccount.ConsKey.PubKey()) + if reporter.IsSkipped() { return nil, nil } msg := must(types.NewMsgRotateConsPubKey(val.GetOperator(), otherAccount.ConsKey.PubKey())) - - // check if there's another key rotation for this same key in the same block - for _, r := range must(k.GetBlockConsPubKeyRotationHistory(ctx)) { - if r.NewConsPubkey.Compare(msg.NewPubkey) == 0 { - reporter.Skip("cons key already used in this block") - return nil, nil - } - } return []simsx.SimAccount{valOper}, msg } } @@ -371,6 +361,16 @@ func randomValidator(ctx context.Context, reporter simsx.SimulationReporter, k * // skips execution if there's another key rotation for the same key in the same block func assertKeyUnused(ctx context.Context, reporter simsx.SimulationReporter, k *keeper.Keeper, newPubKey cryptotypes.PubKey) { + newConsAddr := sdk.ConsAddress(newPubKey.Address()) + if rotatedTo, _ := k.ConsAddrToValidatorIdentifierMap.Get(ctx, newConsAddr); rotatedTo != nil { + reporter.Skip("consensus key already used") + return + } + if _, err := k.GetValidatorByConsAddr(ctx, newConsAddr); err == nil { + reporter.Skip("cons key already used") + return + } + allRotations, err := k.GetBlockConsPubKeyRotationHistory(ctx) if err != nil { reporter.Skipf("cannot get block cons key rotation history: %s", err.Error())