546 lines
17 KiB
Go
546 lines
17 KiB
Go
package simapp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"iter"
|
|
"maps"
|
|
"math/rand"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
cmtproto "github.com/cometbft/cometbft/api/cometbft/types/v1"
|
|
cmttypes "github.com/cometbft/cometbft/types"
|
|
"github.com/spf13/viper"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"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"
|
|
"cosmossdk.io/server/v2/appmanager"
|
|
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"
|
|
|
|
// DefaultSeeds 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
|
|
}
|
|
|
|
StakingKeeper interface {
|
|
UnbondingTime(ctx context.Context) (time.Duration, error)
|
|
}
|
|
|
|
ModuleManager interface {
|
|
Modules() map[string]appmodulev2.AppModule
|
|
}
|
|
|
|
// SimulationApp abstract blockchain app
|
|
SimulationApp[T Tx] interface {
|
|
appmanager.TransactionFuzzer[T]
|
|
InitGenesis(
|
|
ctx context.Context,
|
|
blockRequest *server.BlockRequest[T],
|
|
initGenesisJSON []byte,
|
|
txDecoder transaction.Codec[T],
|
|
) (*server.BlockResponse, store.WriterMap, error)
|
|
|
|
GetApp() *runtime.App[T]
|
|
TxConfig() client.TxConfig
|
|
AppCodec() codec.Codec
|
|
DefaultGenesis() map[string]json.RawMessage
|
|
Store() storev2.RootStore
|
|
Close() error
|
|
}
|
|
|
|
// TestInstance system under test
|
|
TestInstance[T Tx] struct {
|
|
Seed int64
|
|
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)
|
|
AppConfigFactory func() depinject.Config
|
|
)
|
|
|
|
// SetupTestInstance initializes and returns the system under test.
|
|
func SetupTestInstance[T Tx, V SimulationApp[T]](
|
|
tb testing.TB,
|
|
appFactory AppFactory[T, V],
|
|
appConfigFactory AppConfigFactory,
|
|
seed int64,
|
|
) TestInstance[T] {
|
|
tb.Helper()
|
|
vp := viper.New()
|
|
vp.Set("store.app-db-backend", "memdb")
|
|
vp.Set("home", tb.TempDir())
|
|
|
|
depInjCfg := depinject.Configs(
|
|
depinject.Supply(log.NewNopLogger(), runtime.GlobalConfig(vp.AllSettings())),
|
|
appConfigFactory(),
|
|
)
|
|
var (
|
|
bankKeeper BankKeeper
|
|
authKeeper AuthKeeper
|
|
stKeeper StakingKeeper
|
|
)
|
|
|
|
err := depinject.Inject(depInjCfg,
|
|
&authKeeper,
|
|
&bankKeeper,
|
|
&stKeeper,
|
|
)
|
|
require.NoError(tb, err)
|
|
|
|
xapp, err := appFactory(depinject.Configs(depinject.Supply(log.NewNopLogger(), runtime.GlobalConfig(vp.AllSettings()))))
|
|
require.NoError(tb, err)
|
|
return TestInstance[T]{
|
|
Seed: seed,
|
|
App: xapp,
|
|
BankKeeper: bankKeeper,
|
|
AuthKeeper: authKeeper,
|
|
StakingKeeper: stKeeper,
|
|
AppManager: xapp.GetApp(),
|
|
ModuleManager: xapp.GetApp().ModuleManager(),
|
|
TxDecoder: simsxv2.NewGenericTxDecoder[T](xapp.TxConfig()),
|
|
TXBuilder: simsxv2.NewSDKTXBuilder[T](xapp.TxConfig(), simsxv2.DefaultGenTxGas),
|
|
}
|
|
}
|
|
|
|
// InitializeChain sets up the blockchain with an initial state, validator set, and history using the provided genesis data.
|
|
func (ti TestInstance[T]) InitializeChain(
|
|
tb testing.TB,
|
|
ctx context.Context,
|
|
chainID string,
|
|
genesisTimestamp time.Time,
|
|
initialHeight uint64,
|
|
genesisAppState json.RawMessage,
|
|
) ChainState[T] {
|
|
tb.Helper()
|
|
initRsp, stateRoot := doChainInitWithGenesis(
|
|
tb,
|
|
ctx,
|
|
chainID,
|
|
genesisTimestamp,
|
|
initialHeight,
|
|
genesisAppState,
|
|
ti,
|
|
)
|
|
activeValidatorSet := simsxv2.NewValSet().Update(initRsp.ValidatorUpdates)
|
|
valsetHistory := simsxv2.NewValSetHistory(initialHeight)
|
|
valsetHistory.Add(genesisTimestamp, activeValidatorSet)
|
|
return ChainState[T]{
|
|
ChainID: chainID,
|
|
BlockTime: genesisTimestamp,
|
|
BlockHeight: initialHeight,
|
|
ActiveValidatorSet: activeValidatorSet,
|
|
ValsetHistory: valsetHistory,
|
|
AppHash: stateRoot,
|
|
}
|
|
}
|
|
|
|
// RunWithSeeds runs a series of subtests using the default set of random seeds for deterministic simulation testing.
|
|
func RunWithSeeds[T Tx, V SimulationApp[T]](
|
|
t *testing.T,
|
|
appFactory AppFactory[T, V],
|
|
appConfigFactory AppConfigFactory,
|
|
seeds []int64,
|
|
postRunActions ...func(t testing.TB, cs ChainState[T], app TestInstance[T], accs []simtypes.Account),
|
|
) {
|
|
t.Helper()
|
|
cfg := cli.NewConfigFromFlags()
|
|
cfg.ChainID = SimAppChainID
|
|
for _, seed := range seeds {
|
|
t.Run(fmt.Sprintf("seed: %d", seed), func(t *testing.T) {
|
|
t.Parallel()
|
|
RunWithSeed(t, appFactory, appConfigFactory, cfg, seed, postRunActions...)
|
|
})
|
|
}
|
|
}
|
|
|
|
// RunWithSeed initializes and executes a simulation run with the given seed, generating blocks and transactions.
|
|
func RunWithSeed[T Tx, V SimulationApp[T]](
|
|
tb testing.TB,
|
|
appFactory AppFactory[T, V],
|
|
appConfigFactory AppConfigFactory,
|
|
tCfg simtypes.Config,
|
|
seed int64,
|
|
postRunActions ...func(t testing.TB, cs ChainState[T], app TestInstance[T], accs []simtypes.Account),
|
|
) {
|
|
tb.Helper()
|
|
initialBlockHeight := tCfg.InitialBlockHeight
|
|
require.NotEmpty(tb, initialBlockHeight, "initial block height must not be 0")
|
|
|
|
setupFn := func(ctx context.Context, r *rand.Rand) (TestInstance[T], ChainState[T], []simtypes.Account) {
|
|
testInstance := SetupTestInstance[T, V](tb, appFactory, appConfigFactory, seed)
|
|
accounts, genesisAppState, chainID, genesisTimestamp := prepareInitialGenesisState(
|
|
testInstance.App,
|
|
r,
|
|
testInstance.BankKeeper,
|
|
tCfg,
|
|
testInstance.ModuleManager,
|
|
)
|
|
cs := testInstance.InitializeChain(
|
|
tb,
|
|
ctx,
|
|
chainID,
|
|
genesisTimestamp,
|
|
initialBlockHeight,
|
|
genesisAppState,
|
|
)
|
|
|
|
return testInstance, cs, accounts
|
|
}
|
|
RunWithSeedX(tb, tCfg, setupFn, seed, postRunActions...)
|
|
}
|
|
|
|
// RunWithSeedX entrypoint for custom chain setups.
|
|
// The function runs the full simulation test circle for the specified seed and setup function, followed by optional post-run actions.
|
|
func RunWithSeedX[T Tx](
|
|
tb testing.TB,
|
|
tCfg simtypes.Config,
|
|
setupChainStateFn func(ctx context.Context, r *rand.Rand) (TestInstance[T], ChainState[T], []simtypes.Account),
|
|
seed int64,
|
|
postRunActions ...func(t testing.TB, cs ChainState[T], app TestInstance[T], accs []simtypes.Account),
|
|
) {
|
|
tb.Helper()
|
|
r := rand.New(rand.NewSource(seed))
|
|
rootCtx, done := context.WithCancel(context.Background())
|
|
defer done()
|
|
|
|
testInstance, chainState, accounts := setupChainStateFn(rootCtx, r)
|
|
|
|
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))
|
|
|
|
doMainLoop(
|
|
tb,
|
|
rootCtx,
|
|
testInstance,
|
|
&chainState,
|
|
msgFactoriesFn,
|
|
r,
|
|
tCfg,
|
|
accounts,
|
|
)
|
|
|
|
for _, step := range postRunActions {
|
|
step(tb, chainState, testInstance, accounts)
|
|
}
|
|
require.NoError(tb, testInstance.App.Close(), "closing app")
|
|
}
|
|
|
|
// prepareInitialGenesisState initializes the genesis state for simulation by generating accounts, app state, chain ID, and timestamp.
|
|
// It uses a random seed, configuration parameters, and module manager to customize the state.
|
|
// Blocked accounts are removed from the simulation accounts list based on the bank keeper's configuration.
|
|
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
|
|
}
|
|
|
|
// doChainInitWithGenesis initializes the blockchain state with the provided genesis data and returns the initial block response and state root.
|
|
func doChainInitWithGenesis[T Tx](
|
|
tb testing.TB,
|
|
ctx context.Context,
|
|
chainID string,
|
|
genesisTimestamp time.Time,
|
|
initialHeight uint64,
|
|
genesisAppState json.RawMessage,
|
|
testInstance TestInstance[T],
|
|
) (*server.BlockResponse, store.Hash) {
|
|
tb.Helper()
|
|
app := testInstance.App
|
|
txDecoder := testInstance.TxDecoder
|
|
appStore := testInstance.App.Store()
|
|
genesisReq := &server.BlockRequest[T]{
|
|
Height: initialHeight,
|
|
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(tb, err)
|
|
|
|
require.NoError(tb, appStore.SetInitialVersion(initialHeight-1))
|
|
changeSet, err := genesisStateChanges.GetStateChanges()
|
|
require.NoError(tb, err)
|
|
|
|
stateRoot, err := appStore.Commit(&store.Changeset{Changes: changeSet, Version: initialHeight - 1})
|
|
require.NoError(tb, err)
|
|
return initRsp, stateRoot
|
|
}
|
|
|
|
// ChainState represents the state of a blockchain during a simulation run.
|
|
type ChainState[T Tx] struct {
|
|
ChainID string
|
|
BlockTime time.Time
|
|
BlockHeight uint64
|
|
ActiveValidatorSet simsxv2.WeightedValidators
|
|
ValsetHistory *simsxv2.ValSetHistory
|
|
AppHash store.Hash
|
|
}
|
|
|
|
// doMainLoop executes the main simulation loop after chain setup with genesis block.
|
|
// Based on the initial seed and configurations, a deterministic set of messages is generated
|
|
// and executed. Events like validators missing votes or double signing are included in this
|
|
// process. The runtime tracks the validator's state and history.
|
|
func doMainLoop[T Tx](
|
|
tb testing.TB,
|
|
rootCtx context.Context,
|
|
testInstance TestInstance[T],
|
|
cs *ChainState[T],
|
|
nextMsgFactory func() simsx.SimMsgFactoryX,
|
|
r *rand.Rand,
|
|
tCfg simtypes.Config,
|
|
accounts []simtypes.Account,
|
|
) {
|
|
tb.Helper()
|
|
if len(cs.ActiveValidatorSet) == 0 {
|
|
tb.Fatal("no active validators in chain setup")
|
|
return
|
|
}
|
|
|
|
numBlocks := tCfg.NumBlocks
|
|
maxTXPerBlock := tCfg.BlockSize
|
|
|
|
var (
|
|
txSkippedCounter int
|
|
txTotalCounter int
|
|
)
|
|
rootReporter := simsx.NewBasicSimulationReporter()
|
|
futureOpsReg := simsxv2.NewFutureOpsRegistry()
|
|
|
|
for end := cs.BlockHeight + numBlocks; cs.BlockHeight < end; cs.BlockHeight++ {
|
|
if len(cs.ActiveValidatorSet) == 0 {
|
|
tb.Skipf("run out of validators in block: %d\n", cs.BlockHeight)
|
|
return
|
|
}
|
|
cs.BlockTime = cs.BlockTime.Add(minTimePerBlock).
|
|
Add(time.Duration(int64(r.Intn(int(timeRangePerBlock/time.Second)))) * time.Second)
|
|
cs.ValsetHistory.Add(cs.BlockTime, cs.ActiveValidatorSet)
|
|
blockReqN := &server.BlockRequest[T]{
|
|
Height: cs.BlockHeight,
|
|
Time: cs.BlockTime,
|
|
Hash: cs.AppHash,
|
|
AppHash: cs.AppHash,
|
|
ChainId: cs.ChainID,
|
|
}
|
|
|
|
cometInfo := comet.Info{
|
|
ValidatorsHash: nil,
|
|
Evidence: cs.ValsetHistory.MissBehaviour(r),
|
|
ProposerAddress: cs.ActiveValidatorSet[0].Address, // todo: pick random one
|
|
LastCommit: cs.ActiveValidatorSet.NewCommitInfo(r),
|
|
}
|
|
fOps, pos := futureOpsReg.PopScheduledFor(cs.BlockTime), 0
|
|
addressCodec := testInstance.App.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 := testInstance.App.DeliverSims(simsCtx, blockReqN, func(ctx context.Context) iter.Seq[T] {
|
|
return func(yield func(T) bool) {
|
|
unbondingTime, err := testInstance.StakingKeeper.UnbondingTime(ctx)
|
|
require.NoError(tb, err)
|
|
cs.ValsetHistory.SetMaxHistory(minBlocksInUnbondingPeriod(unbondingTime))
|
|
testData := simsx.NewChainDataSource(ctx, r, testInstance.AuthKeeper, testInstance.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(tb, reporter.Close())
|
|
continue
|
|
}
|
|
resultHandlers = append(resultHandlers, mergedMsgFactory.DeliveryResultHandler())
|
|
reporter.Success(msg)
|
|
require.NoError(tb, reporter.Close())
|
|
|
|
tx, err := testInstance.TXBuilder.Build(ctx, testInstance.AuthKeeper, signers, msg, r, cs.ChainID)
|
|
require.NoError(tb, err)
|
|
if !yield(tx) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
})
|
|
require.NoError(tb, err, "%d, %s", blockReqN.Height, blockReqN.Time)
|
|
changeSet, err := updates.GetStateChanges()
|
|
require.NoError(tb, err)
|
|
cs.AppHash, err = testInstance.App.Store().Commit(&store.Changeset{
|
|
Version: blockReqN.Height,
|
|
Changes: changeSet,
|
|
})
|
|
|
|
require.NoError(tb, err)
|
|
require.Equal(tb, len(resultHandlers), len(blockRsp.TxResults), "txPerBlockCounter: %d, totalSkipped: %d", txPerBlockCounter, txSkippedCounter)
|
|
for i, v := range blockRsp.TxResults {
|
|
require.NoError(tb, resultHandlers[i](v.Error))
|
|
}
|
|
txTotalCounter += txPerBlockCounter
|
|
cs.ActiveValidatorSet = cs.ActiveValidatorSet.Update(blockRsp.ValidatorUpdates)
|
|
}
|
|
fmt.Println("+++ reporter:\n" + rootReporter.Summary().String())
|
|
fmt.Printf("Tx total: %d skipped: %d\n", txTotalCounter, txSkippedCounter)
|
|
}
|
|
|
|
// prepareSimsMsgFactories constructs and returns a function to retrieve simulation message factories for all modules.
|
|
// It initializes proposal and factory registries, registers proposals and weighted operations, and sorts deterministically.
|
|
func prepareSimsMsgFactories(
|
|
r *rand.Rand,
|
|
modules map[string]appmodulev2.AppModule,
|
|
weights simsx.WeightSource,
|
|
) func() simsx.SimMsgFactoryX {
|
|
moduleNames := slices.Collect(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 := slices.Collect(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)
|
|
}
|