feat(sims): introduce message factories/ simsx (#24126)

Co-authored-by: Alex | Interchain Labs <alex@interchainlabs.io>
This commit is contained in:
Alexander Peters 2025-03-28 17:08:23 +01:00 committed by GitHub
parent 0fbbf26f63
commit 150f12e4ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 2933 additions and 291 deletions

View File

@ -13,8 +13,8 @@ import (
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/simsx"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
"github.com/cosmos/cosmos-sdk/testutils/sims"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/simulation"
simcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli"
@ -26,7 +26,7 @@ func BenchmarkFullAppSimulation(b *testing.B) {
b.ReportAllocs()
config := simcli.NewConfigFromFlags()
config.ChainID = sims.SimAppChainID
config.ChainID = simsx.SimAppChainID
db, dir, logger, skip, err := simtestutil.SetupSimulation(config, "goleveldb-app-sim", "Simulation", simcli.FlagVerboseValue, true)
if err != nil {
@ -45,10 +45,10 @@ func BenchmarkFullAppSimulation(b *testing.B) {
appOptions := viper.New()
appOptions.SetDefault(flags.FlagHome, DefaultNodeHome)
app := NewSimApp(logger, db, nil, true, appOptions, interBlockCacheOpt(), baseapp.SetChainID(sims.SimAppChainID))
app := NewSimApp(logger, db, nil, true, appOptions, interBlockCacheOpt(), baseapp.SetChainID(simsx.SimAppChainID))
// run randomized simulation
simParams, simErr := simulation.SimulateFromSeedX(
simParams, _, simErr := simulation.SimulateFromSeedX(
b,
log.NewNopLogger(),
os.Stdout,

View File

@ -25,8 +25,8 @@ import (
"github.com/cosmos/cosmos-sdk/baseapp"
servertypes "github.com/cosmos/cosmos-sdk/server/types"
sims "github.com/cosmos/cosmos-sdk/simsx"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
"github.com/cosmos/cosmos-sdk/testutils/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
@ -56,9 +56,11 @@ func TestFullAppSimulation(t *testing.T) {
func setupStateFactory(app *SimApp) sims.SimStateFactory {
return sims.SimStateFactory{
Codec: app.AppCodec(),
AppStateFn: simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()),
BlockedAddr: BlockedAddresses(),
Codec: app.AppCodec(),
AppStateFn: simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()),
BlockedAddr: BlockedAddresses(),
AccountSource: app.AccountKeeper,
BalanceSource: app.BankKeeper,
}
}
@ -68,29 +70,29 @@ var (
)
func TestAppImportExport(t *testing.T) {
sims.Run(t, NewSimApp, setupStateFactory, func(t *testing.T, ti sims.TestInstance[*SimApp]) {
t.Helper()
sims.Run(t, NewSimApp, setupStateFactory, func(tb testing.TB, ti sims.TestInstance[*SimApp], accs []simtypes.Account) {
tb.Helper()
app := ti.App
t.Log("exporting genesis...\n")
tb.Log("exporting genesis...\n")
exported, err := app.ExportAppStateAndValidators(false, exportWithValidatorSet, exportAllModules)
require.NoError(t, err)
require.NoError(tb, err)
t.Log("importing genesis...\n")
newTestInstance := sims.NewSimulationAppInstance(t, ti.Cfg, NewSimApp)
tb.Log("importing genesis...\n")
newTestInstance := sims.NewSimulationAppInstance(tb, ti.Cfg, NewSimApp)
newApp := newTestInstance.App
var genesisState GenesisState
require.NoError(t, json.Unmarshal(exported.AppState, &genesisState))
require.NoError(tb, json.Unmarshal(exported.AppState, &genesisState))
ctxB := newApp.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()})
_, err = newApp.ModuleManager.InitGenesis(ctxB, newApp.appCodec, genesisState)
if IsEmptyValidatorSetErr(err) {
t.Skip("Skipping simulation as all validators have been unbonded")
tb.Skip("Skipping simulation as all validators have been unbonded")
return
}
require.NoError(t, err)
require.NoError(tb, err)
err = newApp.StoreConsensusParams(ctxB, exported.ConsensusParams)
require.NoError(t, err)
require.NoError(tb, err)
t.Log("comparing stores...")
tb.Log("comparing stores...")
// skip certain prefixes
skipPrefixes := map[string][][]byte{
stakingtypes.StoreKey: {
@ -103,7 +105,7 @@ func TestAppImportExport(t *testing.T) {
feegrant.StoreKey: {feegrant.FeeAllowanceQueueKeyPrefix},
slashingtypes.StoreKey: {slashingtypes.ValidatorMissedBlockBitmapKeyPrefix},
}
AssertEqualStores(t, app, newApp, app.SimulationManager().StoreDecoders, skipPrefixes)
AssertEqualStores(tb, app, newApp, app.SimulationManager().StoreDecoders, skipPrefixes)
})
}
@ -113,28 +115,28 @@ func TestAppImportExport(t *testing.T) {
// set up a new node instance, Init chain from exported genesis
// run new instance for n blocks
func TestAppSimulationAfterImport(t *testing.T) {
sims.Run(t, NewSimApp, setupStateFactory, func(t *testing.T, ti sims.TestInstance[*SimApp]) {
t.Helper()
sims.Run(t, NewSimApp, setupStateFactory, func(tb testing.TB, ti sims.TestInstance[*SimApp], accs []simtypes.Account) {
tb.Helper()
app := ti.App
t.Log("exporting genesis...\n")
tb.Log("exporting genesis...\n")
exported, err := app.ExportAppStateAndValidators(false, exportWithValidatorSet, exportAllModules)
require.NoError(t, err)
require.NoError(tb, err)
t.Log("importing genesis...\n")
newTestInstance := sims.NewSimulationAppInstance(t, ti.Cfg, NewSimApp)
tb.Log("importing genesis...\n")
newTestInstance := sims.NewSimulationAppInstance(tb, ti.Cfg, NewSimApp)
newApp := newTestInstance.App
_, err = newApp.InitChain(&abci.RequestInitChain{
AppStateBytes: exported.AppState,
ChainId: sims.SimAppChainID,
})
if IsEmptyValidatorSetErr(err) {
t.Skip("Skipping simulation as all validators have been unbonded")
tb.Skip("Skipping simulation as all validators have been unbonded")
return
}
require.NoError(t, err)
require.NoError(tb, err)
newStateFactory := setupStateFactory(newApp)
_, err = simulation.SimulateFromSeedX(
t,
_, _, err = simulation.SimulateFromSeedX(
tb,
newTestInstance.AppLogger,
sims.WriteToDebugLog(newTestInstance.AppLogger),
newApp.BaseApp,
@ -146,7 +148,7 @@ func TestAppSimulationAfterImport(t *testing.T) {
newStateFactory.Codec,
ti.ExecLogWriter,
)
require.NoError(t, err)
require.NoError(tb, err)
})
}
@ -192,8 +194,8 @@ func TestAppStateDeterminism(t *testing.T) {
var mx sync.Mutex
appHashResults := make(map[int64][][]byte)
appSimLogger := make(map[int64][]simulation.LogWriter)
captureAndCheckHash := func(t *testing.T, ti sims.TestInstance[*SimApp]) {
t.Helper()
captureAndCheckHash := func(tb testing.TB, ti sims.TestInstance[*SimApp], _ []simtypes.Account) {
tb.Helper()
seed, appHash := ti.Cfg.Seed, ti.App.LastCommitID().Hash
mx.Lock()
otherHashes, execWriters := appHashResults[seed], appSimLogger[seed]
@ -208,14 +210,14 @@ func TestAppStateDeterminism(t *testing.T) {
var failNow bool
// and check that all app hashes per seed are equal for each iteration
for i := 0; i < len(otherHashes); i++ {
if !assert.Equal(t, otherHashes[i], appHash) {
if !assert.Equal(tb, otherHashes[i], appHash) {
execWriters[i].PrintLogs()
failNow = true
}
}
if failNow {
ti.ExecLogWriter.PrintLogs()
t.Fatalf("non-determinism in seed %d", seed)
tb.Fatalf("non-determinism in seed %d", seed)
}
}
// run simulations
@ -229,13 +231,18 @@ type ComparableStoreApp interface {
GetStoreKeys() []storetypes.StoreKey
}
func AssertEqualStores(t *testing.T, app, newApp ComparableStoreApp, storeDecoders simtypes.StoreDecoderRegistry, skipPrefixes map[string][][]byte) {
t.Helper()
func AssertEqualStores(
tb testing.TB,
app, newApp ComparableStoreApp,
storeDecoders simtypes.StoreDecoderRegistry,
skipPrefixes map[string][][]byte,
) {
tb.Helper()
ctxA := app.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()})
ctxB := newApp.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()})
storeKeys := app.GetStoreKeys()
require.NotEmpty(t, storeKeys)
require.NotEmpty(tb, storeKeys)
for _, appKeyA := range storeKeys {
// only compare kvstores
@ -250,14 +257,14 @@ func AssertEqualStores(t *testing.T, app, newApp ComparableStoreApp, storeDecode
storeB := ctxB.KVStore(appKeyB)
failedKVAs, failedKVBs := simtestutil.DiffKVStores(storeA, storeB, skipPrefixes[keyName])
require.Equal(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare %s, key stores %s and %s", keyName, appKeyA, appKeyB)
require.Equal(tb, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare %s, key stores %s and %s", keyName, appKeyA, appKeyB)
t.Logf("compared %d different key/value pairs between %s and %s\n", len(failedKVAs), appKeyA, appKeyB)
if !assert.Equal(t, 0, len(failedKVAs), simtestutil.GetSimulationLog(keyName, storeDecoders, failedKVAs, failedKVBs)) {
tb.Logf("compared %d different key/value pairs between %s and %s\n", len(failedKVAs), appKeyA, appKeyB)
if !assert.Equal(tb, 0, len(failedKVAs), simtestutil.GetSimulationLog(keyName, storeDecoders, failedKVAs, failedKVBs)) {
for _, v := range failedKVAs {
t.Logf("store mismatch: %q\n", v)
tb.Logf("store mismatch: %q\n", v)
}
t.FailNow()
tb.FailNow()
}
}
}

47
simsx/README.md Normal file
View File

@ -0,0 +1,47 @@
# Simsx
This package introduces some new helper types to simplify message construction for simulations (sims). The focus is on better dev UX for new message factories.
Technically, they are adapters that build upon the existing sims framework.
## [Message factory](https://github.com/cosmos/cosmos-sdk/blob/main/simsx/msg_factory.go)
Simple functions as factories for dedicated sdk.Msgs. They have access to the context, reporter and test data environment. For example:
```go
func MsgSendFactory() simsx.SimMsgFactoryFn[*types.MsgSend] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgSend) {
from := testData.AnyAccount(reporter, simsx.WithSpendableBalance())
to := testData.AnyAccount(reporter, simsx.ExcludeAccounts(from))
coins := from.LiquidBalance().RandSubsetCoins(reporter, simsx.WithSendEnabledCoins())
return []simsx.SimAccount{from}, types.NewMsgSend(from.AddressBech32, to.AddressBech32, coins)
}
}
```
## [Sims registry](https://github.com/cosmos/cosmos-sdk/blob/main/simsx/registry.go)
A new helper to register message factories with a default weight value. They can be overwritten by a parameters file as before. The registry is passed to the AppModule type. For example:
```go
func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Registry) {
reg.Add(weights.Get("msg_send", 100), simulation.MsgSendFactory())
reg.Add(weights.Get("msg_multisend", 10), simulation.MsgMultiSendFactory())
}
```
## [Reporter](https://github.com/cosmos/cosmos-sdk/blob/main/simsx/reporter.go)
The reporter is a flow control structure that can be used in message factories to skip execution at any point. The idea is similar to the testing.T Skip in Go stdlib. Internally, it converts skip, success and failure events to legacy sim messages.
The reporter also provides some capability to print an execution summary.
It is also used to interact with the test data environment to not have errors checked all the time.
Message factories may want to abort early via
```go
if reporter.IsSkipped() {
return nil, nil
}
```
## [Test data environment](https://github.com/cosmos/cosmos-sdk/blob/main/simsx/environment.go)
The test data environment provides simple access to accounts and other test data used in most message factories. It also encapsulates some app internals like bank keeper or address codec.

159
simsx/common_test.go Normal file
View File

@ -0,0 +1,159 @@
package simsx
import (
"context"
"math/rand"
"github.com/cosmos/gogoproto/proto"
"cosmossdk.io/x/tx/signing"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/codec/address"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/std"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/auth/tx"
)
// SimAccountFixture testing only
func SimAccountFixture(mutators ...func(account *SimAccount)) SimAccount {
r := rand.New(rand.NewSource(1))
acc := SimAccount{
Account: simtypes.RandomAccounts(r, 1)[0],
}
acc.liquidBalance = NewSimsAccountBalance(&acc, r, sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1_000_000_000)))
for _, mutator := range mutators {
mutator(&acc)
}
return acc
}
// MemoryAccountSource testing only
func MemoryAccountSource(srcs ...SimAccount) AccountSourceFn {
accs := make(map[string]FakeAccountI, len(srcs))
for _, src := range srcs {
accs[src.AddressBech32] = FakeAccountI{SimAccount: src, id: 1, seq: 2}
}
return func(ctx context.Context, addr sdk.AccAddress) sdk.AccountI {
return accs[addr.String()]
}
}
// testing only
func txConfig() client.TxConfig {
ir := must(codectypes.NewInterfaceRegistryWithOptions(codectypes.InterfaceRegistryOptions{
ProtoFiles: proto.HybridResolver,
SigningOptions: signing.Options{
AddressCodec: address.NewBech32Codec("cosmos"),
ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"),
},
}))
std.RegisterInterfaces(ir)
ir.RegisterImplementations((*sdk.Msg)(nil), &testdata.TestMsg{})
protoCodec := codec.NewProtoCodec(ir)
return tx.NewTxConfig(protoCodec, tx.DefaultSignModes)
}
var _ AppEntrypoint = SimDeliverFn(nil)
type (
AppEntrypointFn = SimDeliverFn
SimDeliverFn func(_txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error)
)
func (m SimDeliverFn) SimDeliver(txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
return m(txEncoder, tx)
}
var _ AccountSource = AccountSourceFn(nil)
type AccountSourceFn func(ctx context.Context, addr sdk.AccAddress) sdk.AccountI
func (a AccountSourceFn) GetAccount(ctx context.Context, addr sdk.AccAddress) sdk.AccountI {
return a(ctx, addr)
}
var _ sdk.AccountI = &FakeAccountI{}
type FakeAccountI struct {
SimAccount
id, seq uint64
}
func (m FakeAccountI) GetAddress() sdk.AccAddress {
return m.Address
}
func (m FakeAccountI) GetPubKey() cryptotypes.PubKey {
return m.PubKey
}
func (m FakeAccountI) GetAccountNumber() uint64 {
return m.id
}
func (m FakeAccountI) GetSequence() uint64 {
return m.seq
}
func (m FakeAccountI) Reset() {
panic("implement me")
}
func (m FakeAccountI) String() string {
panic("implement me")
}
func (m FakeAccountI) ProtoMessage() {
panic("implement me")
}
func (m FakeAccountI) SetAddress(address sdk.AccAddress) error {
panic("implement me")
}
func (m FakeAccountI) SetPubKey(key cryptotypes.PubKey) error {
panic("implement me")
}
func (m FakeAccountI) SetAccountNumber(u uint64) error {
panic("implement me")
}
func (m FakeAccountI) SetSequence(u uint64) error {
panic("implement me")
}
var _ AccountSourceX = &MockAccountSourceX{}
// MockAccountSourceX mock impl for testing only
type MockAccountSourceX struct {
GetAccountFn func(ctx context.Context, addr sdk.AccAddress) sdk.AccountI
GetModuleAddressFn func(moduleName string) sdk.AccAddress
}
func (m MockAccountSourceX) GetAccount(ctx context.Context, addr sdk.AccAddress) sdk.AccountI {
if m.GetAccountFn == nil {
panic("not expected to be called")
}
return m.GetAccountFn(ctx, addr)
}
func (m MockAccountSourceX) GetModuleAddress(moduleName string) sdk.AccAddress {
if m.GetModuleAddressFn == nil {
panic("not expected to be called")
}
return m.GetModuleAddressFn(moduleName)
}
func must[T any](r T, err error) T {
if err != nil {
panic(err)
}
return r
}

100
simsx/delivery.go Normal file
View File

@ -0,0 +1,100 @@
package simsx
import (
"context"
"errors"
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/client"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
type (
// AppEntrypoint is the entrypoint to deliver sims TX to the system.
AppEntrypoint interface {
SimDeliver(txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error)
}
AccountSource interface {
GetAccount(ctx context.Context, addr sdk.AccAddress) sdk.AccountI
}
// SimDeliveryResultHandler processes the delivery response error. Some sims are supposed to fail and expect an error.
// An unhandled error returned indicates a failure
SimDeliveryResultHandler func(error) error
)
// DeliverSimsMsg delivers a simulation message by creating and signing a mock transaction,
// then delivering it to the application through the specified entrypoint. It returns a legacy
// operation message representing the result of the delivery.
//
// The function takes the following parameters:
// - reporter: SimulationReporter - Interface for reporting the result of the delivery
// - r: *rand.Rand - Random number generator used for creating the mock transaction
// - app: AppEntrypoint - Entry point for delivering the simulation transaction to the application
// - txGen: client.TxConfig - Configuration for generating transactions
// - ak: AccountSource - Source for retrieving accounts
// - msg: sdk.Msg - The simulation message to be delivered
// - ctx: sdk.Context - The simulation context
// - chainID: string - The chain ID
// - senders: ...SimAccount - Accounts from which to send the simulation message
//
// The function returns a simtypes.OperationMsg, which is a legacy representation of the result
// of the delivery.
func DeliverSimsMsg(
ctx context.Context,
reporter SimulationReporter,
app AppEntrypoint,
r *rand.Rand,
txGen client.TxConfig,
ak AccountSource,
chainID string,
msg sdk.Msg,
deliveryResultHandler SimDeliveryResultHandler,
senders ...SimAccount,
) simtypes.OperationMsg {
if reporter.IsSkipped() {
return reporter.ToLegacyOperationMsg()
}
if len(senders) == 0 {
reporter.Fail(errors.New("no senders"), "encoding TX")
return reporter.ToLegacyOperationMsg()
}
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()
tx, err := sims.GenSignedMockTx(
r,
txGen,
[]sdk.Msg{msg},
fees,
sims.DefaultGenTxGas,
chainID,
accountNumbers,
sequenceNumbers,
Collect(senders, func(a SimAccount) cryptotypes.PrivKey { return a.PrivKey })...,
)
if err != nil {
reporter.Fail(err, "encoding TX")
return reporter.ToLegacyOperationMsg()
}
_, _, err = app.SimDeliver(txGen.TxEncoder(), tx)
if err2 := deliveryResultHandler(err); err2 != nil {
var comment string
for _, msg := range tx.GetMsgs() {
comment += fmt.Sprintf("%#v", msg)
}
reporter.Fail(err2, fmt.Sprintf("delivering tx with msgs: %s", comment))
return reporter.ToLegacyOperationMsg()
}
reporter.Success(msg)
return reporter.ToLegacyOperationMsg()
}

74
simsx/delivery_test.go Normal file
View File

@ -0,0 +1,74 @@
package simsx
import (
"errors"
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
func TestDeliverSimsMsg(t *testing.T) {
var (
sender = SimAccountFixture()
ak = MemoryAccountSource(sender)
myMsg = testdata.NewTestMsg(sender.Address)
txConfig = txConfig()
r = rand.New(rand.NewSource(1))
ctx sdk.Context
)
noopResultHandler := func(err error) error { return err }
specs := map[string]struct {
app AppEntrypoint
reporter func() SimulationReporter
deliveryResultHandler SimDeliveryResultHandler
errDeliveryResultHandler error
expOps simtypes.OperationMsg
}{
"error when reporter skipped": {
app: SimDeliverFn(func(_txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
return sdk.GasInfo{GasWanted: 100, GasUsed: 20}, &sdk.Result{}, nil
}),
reporter: func() SimulationReporter {
r := NewBasicSimulationReporter()
r.Skip("testing")
return r
},
expOps: simtypes.NoOpMsg("", "", "testing"),
},
"successful delivery": {
app: SimDeliverFn(func(_txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
return sdk.GasInfo{GasWanted: 100, GasUsed: 20}, &sdk.Result{}, nil
}),
reporter: func() SimulationReporter { return NewBasicSimulationReporter() },
deliveryResultHandler: noopResultHandler,
expOps: simtypes.NewOperationMsgBasic("", "", "", true, []byte{}),
},
"error delivery": {
app: SimDeliverFn(func(_txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
return sdk.GasInfo{GasWanted: 100, GasUsed: 20}, &sdk.Result{}, errors.New("my error")
}),
reporter: func() SimulationReporter { return NewBasicSimulationReporter() },
deliveryResultHandler: noopResultHandler,
expOps: simtypes.NewOperationMsgBasic("", "", "delivering tx with msgs: &testdata.TestMsg{Signers:[]string{\"cosmos1tnh2q55v8wyygtt9srz5safamzdengsnqeycj3\"}}", false, []byte{}),
},
"error delivery handled": {
app: SimDeliverFn(func(_txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
return sdk.GasInfo{GasWanted: 100, GasUsed: 20}, &sdk.Result{}, errors.New("my error")
}),
reporter: func() SimulationReporter { return NewBasicSimulationReporter() },
deliveryResultHandler: func(err error) error { return nil },
expOps: simtypes.NewOperationMsgBasic("", "", "", true, []byte{}),
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
got := DeliverSimsMsg(ctx, spec.reporter(), spec.app, r, txConfig, ak, "testing", myMsg, spec.deliveryResultHandler, sender)
assert.Equal(t, spec.expOps, got)
})
}
}

470
simsx/environment.go Normal file
View File

@ -0,0 +1,470 @@
package simsx
import (
"context"
"errors"
"math/rand"
"slices"
"time"
"cosmossdk.io/core/address"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
// helper type for simple bank access
type contextAwareBalanceSource struct {
ctx context.Context
bank BalanceSource
}
func (s contextAwareBalanceSource) SpendableCoins(accAddress sdk.AccAddress) sdk.Coins {
return s.bank.SpendableCoins(s.ctx, accAddress)
}
func (s contextAwareBalanceSource) IsSendEnabledDenom(denom string) bool {
return s.bank.IsSendEnabledDenom(s.ctx, denom)
}
// SimAccount is an extended simtypes.Account
type SimAccount struct {
simtypes.Account
r *rand.Rand
liquidBalance *SimsAccountBalance
bank contextAwareBalanceSource
}
// LiquidBalance spendable balance. This excludes not spendable amounts like staked or vested amounts.
func (a *SimAccount) LiquidBalance() *SimsAccountBalance {
if a.liquidBalance == nil {
a.liquidBalance = NewSimsAccountBalance(a, a.r, a.bank.SpendableCoins(a.Address))
}
return a.liquidBalance
}
// SimsAccountBalance is a helper type for common access methods to balance amounts.
type SimsAccountBalance struct {
sdk.Coins
owner *SimAccount
r *rand.Rand
}
// NewSimsAccountBalance constructor
func NewSimsAccountBalance(o *SimAccount, r *rand.Rand, coins sdk.Coins) *SimsAccountBalance {
return &SimsAccountBalance{Coins: coins, r: r, owner: o}
}
type CoinsFilter interface {
Accept(c sdk.Coins) bool // returns false to reject
}
type CoinsFilterFn func(c sdk.Coins) bool
func (f CoinsFilterFn) Accept(c sdk.Coins) bool {
return f(c)
}
func WithSendEnabledCoins() CoinsFilter {
return statefulCoinsFilterFn(func(s *SimAccount, coins sdk.Coins) bool {
for _, coin := range coins {
if !s.bank.IsSendEnabledDenom(coin.Denom) {
return false
}
}
return true
})
}
// filter with context of SimAccount
type statefulCoinsFilter struct {
s *SimAccount
do func(s *SimAccount, c sdk.Coins) bool
}
// constructor
func statefulCoinsFilterFn(f func(s *SimAccount, c sdk.Coins) bool) CoinsFilter {
return &statefulCoinsFilter{do: f}
}
func (f statefulCoinsFilter) Accept(c sdk.Coins) bool {
if f.s == nil {
panic("account not set")
}
return f.do(f.s, c)
}
func (f *statefulCoinsFilter) visit(s *SimAccount) {
f.s = s
}
var _ visitable = &statefulCoinsFilter{}
type visitable interface {
visit(s *SimAccount)
}
// RandSubsetCoins return random amounts from the current balance. When the coins are empty, skip is called on the reporter.
// The amounts are removed from the liquid balance.
func (b *SimsAccountBalance) RandSubsetCoins(reporter SimulationReporter, filters ...CoinsFilter) sdk.Coins {
amount := b.randomAmount(5, reporter, b.Coins, filters...)
b.Coins = b.Sub(amount...)
if amount.Empty() {
reporter.Skip("got empty amounts")
}
return amount
}
// RandSubsetCoin return random amount from the current balance. When the coins are empty, skip is called on the reporter.
// The amount is removed from the liquid balance.
func (b *SimsAccountBalance) RandSubsetCoin(reporter SimulationReporter, denom string, filters ...CoinsFilter) sdk.Coin {
ok, coin := b.Find(denom)
if !ok {
reporter.Skipf("no such coin: %s", denom)
return sdk.NewCoin(denom, math.ZeroInt())
}
amounts := b.randomAmount(5, reporter, sdk.Coins{coin}, filters...)
if amounts.Empty() {
reporter.Skip("empty coin")
return sdk.NewCoin(denom, math.ZeroInt())
}
b.BlockAmount(amounts[0])
return amounts[0]
}
// BlockAmount returns true when balance is > requested amount and subtracts the amount from the liquid balance
func (b *SimsAccountBalance) BlockAmount(amount sdk.Coin) bool {
ok, coin := b.Find(amount.Denom)
if !ok || !coin.IsPositive() || !coin.IsGTE(amount) {
return false
}
b.Coins = b.Sub(amount)
return true
}
func (b *SimsAccountBalance) randomAmount(retryCount int, reporter SimulationReporter, coins sdk.Coins, filters ...CoinsFilter) sdk.Coins {
if retryCount < 0 || b.Empty() {
reporter.Skip("failed to find matching amount")
return sdk.Coins{}
}
amount := simtypes.RandSubsetCoins(b.r, coins)
for _, filter := range filters {
if f, ok := filter.(visitable); ok {
f.visit(b.owner)
}
if !filter.Accept(amount) {
return b.randomAmount(retryCount-1, reporter, coins, filters...)
}
}
return amount
}
func (b *SimsAccountBalance) RandFees() sdk.Coins {
amount, err := simtypes.RandomFees(b.r, sdk.Context{}, b.Coins)
if err != nil {
return sdk.Coins{}
}
return amount
}
type SimAccountFilter interface {
// Accept returns true to accept the account or false to reject
Accept(a SimAccount) bool
}
type SimAccountFilterFn func(a SimAccount) bool
func (s SimAccountFilterFn) Accept(a SimAccount) bool {
return s(a)
}
func ExcludeAccounts(others ...SimAccount) SimAccountFilter {
return SimAccountFilterFn(func(a SimAccount) bool {
return !slices.ContainsFunc(others, func(o SimAccount) bool {
return a.Address.Equals(o.Address)
})
})
}
// UniqueAccounts returns a stateful filter that rejects duplicate accounts.
// It uses a map to keep track of accounts that have been processed.
// If an account exists in the map, the filter function returns false
// to reject a duplicate, else it adds the account to the map and returns true.
//
// Example usage:
//
// uniqueAccountsFilter := simsx.UniqueAccounts()
//
// for {
// from := testData.AnyAccount(reporter, uniqueAccountsFilter)
// //... rest of the loop
// }
func UniqueAccounts() SimAccountFilter {
idx := make(map[string]struct{})
return SimAccountFilterFn(func(a SimAccount) bool {
if _, ok := idx[a.AddressBech32]; ok {
return false
}
idx[a.AddressBech32] = struct{}{}
return true
})
}
func ExcludeAddresses(addrs ...string) SimAccountFilter {
return SimAccountFilterFn(func(a SimAccount) bool {
return !slices.Contains(addrs, a.AddressBech32)
})
}
func WithDenomBalance(denom string) SimAccountFilter {
return SimAccountFilterFn(func(a SimAccount) bool {
return a.LiquidBalance().AmountOf(denom).IsPositive()
})
}
func WithLiquidBalanceGTE(amount ...sdk.Coin) SimAccountFilter {
return SimAccountFilterFn(func(a SimAccount) bool {
return a.LiquidBalance().IsAllGTE(amount)
})
}
// WithSpendableBalance Filters for liquid token but send may not be enabled for all or any
func WithSpendableBalance() SimAccountFilter {
return SimAccountFilterFn(func(a SimAccount) bool {
return !a.LiquidBalance().Empty()
})
}
type ModuleAccountSource interface {
GetModuleAddress(moduleName string) sdk.AccAddress
}
// BalanceSource is an interface for retrieving balance-related information for a given account.
type BalanceSource interface {
SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins
IsSendEnabledDenom(ctx context.Context, denom string) bool
}
// ChainDataSource provides common sims test data and helper methods
type ChainDataSource struct {
r *rand.Rand
addressToAccountsPosIndex map[string]int
accounts []SimAccount
accountSource ModuleAccountSource
addressCodec address.Codec
bank contextAwareBalanceSource
}
// NewChainDataSource constructor
func NewChainDataSource(
ctx context.Context,
r *rand.Rand,
ak ModuleAccountSource,
bk BalanceSource,
codec address.Codec,
oldSimAcc ...simtypes.Account,
) *ChainDataSource {
if len(oldSimAcc) == 0 {
panic("empty accounts")
}
acc := make([]SimAccount, len(oldSimAcc))
index := make(map[string]int, len(oldSimAcc))
bank := contextAwareBalanceSource{ctx: ctx, bank: bk}
for i, a := range oldSimAcc {
acc[i] = SimAccount{Account: a, r: r, bank: bank}
index[a.AddressBech32] = i
if a.AddressBech32 == "" {
panic("test account has empty bech32 address")
}
}
return &ChainDataSource{
r: r,
accountSource: ak,
addressCodec: codec,
accounts: acc,
bank: bank,
addressToAccountsPosIndex: index,
}
}
// AnyAccount returns a random SimAccount matching the filter criteria. Module accounts are excluded.
// In case of an error or no matching account found, the reporter is set to skip and an empty value is returned.
func (c *ChainDataSource) AnyAccount(r SimulationReporter, filters ...SimAccountFilter) SimAccount {
acc := c.randomAccount(r, 5, filters...)
return acc
}
// GetAccountbyAccAddr return SimAccount with given binary address. Reporter skip flag is set when not found.
func (c ChainDataSource) GetAccountbyAccAddr(reporter SimulationReporter, addr sdk.AccAddress) SimAccount {
if len(addr) == 0 {
reporter.Skip("can not find account for empty address")
return c.nullAccount()
}
addrStr, err := c.addressCodec.BytesToString(addr)
if err != nil {
reporter.Skipf("can not convert account address to string: %s", err)
return c.nullAccount()
}
return c.GetAccount(reporter, addrStr)
}
func (c ChainDataSource) HasAccount(addr string) bool {
_, ok := c.addressToAccountsPosIndex[addr]
return ok
}
// GetAccount return SimAccount with given bench32 address. Reporter skip flag is set when not found.
func (c ChainDataSource) GetAccount(reporter SimulationReporter, addr string) SimAccount {
pos, ok := c.addressToAccountsPosIndex[addr]
if !ok {
reporter.Skipf("no account: %s", addr)
return c.nullAccount()
}
return c.accounts[pos]
}
func (c *ChainDataSource) randomAccount(reporter SimulationReporter, retryCount int, filters ...SimAccountFilter) SimAccount {
if retryCount < 0 {
reporter.Skip("failed to find a matching account")
return c.nullAccount()
}
idx := c.r.Intn(len(c.accounts))
acc := c.accounts[idx]
for _, filter := range filters {
if !filter.Accept(acc) {
return c.randomAccount(reporter, retryCount-1, filters...)
}
}
return acc
}
// create null object
func (c ChainDataSource) nullAccount() SimAccount {
return SimAccount{
Account: simtypes.Account{},
r: c.r,
liquidBalance: &SimsAccountBalance{},
bank: c.accounts[0].bank,
}
}
func (c *ChainDataSource) ModuleAccountAddress(reporter SimulationReporter, moduleName string) string {
acc := c.accountSource.GetModuleAddress(moduleName)
if acc == nil {
reporter.Skipf("unknown module account: %s", moduleName)
return ""
}
res, err := c.addressCodec.BytesToString(acc)
if err != nil {
reporter.Skipf("failed to encode module address: %s", err)
return ""
}
return res
}
func (c *ChainDataSource) AddressCodec() address.Codec {
return c.addressCodec
}
func (c *ChainDataSource) Rand() *XRand {
return &XRand{c.r}
}
func (c *ChainDataSource) IsSendEnabledDenom(denom string) bool {
return c.bank.IsSendEnabledDenom(denom)
}
// AllAccounts returns all accounts in legacy format
func (c *ChainDataSource) AllAccounts() []simtypes.Account {
return Collect(c.accounts, func(a SimAccount) simtypes.Account { return a.Account })
}
func (c *ChainDataSource) AccountsCount() int {
return len(c.accounts)
}
// AccountAt return SimAccount within the accounts slice. Reporter skip flag is set when boundaries are exceeded.
func (c *ChainDataSource) AccountAt(reporter SimulationReporter, i int) SimAccount {
if i > len(c.accounts) {
reporter.Skipf("account index out of range: %d", i)
return c.nullAccount()
}
return c.accounts[i]
}
type XRand struct {
*rand.Rand
}
// NewXRand constructor
func NewXRand(rand *rand.Rand) *XRand {
return &XRand{Rand: rand}
}
func (r *XRand) StringN(max int) string {
return simtypes.RandStringOfLength(r.Rand, max)
}
func (r *XRand) SubsetCoins(src sdk.Coins) sdk.Coins {
return simtypes.RandSubsetCoins(r.Rand, src)
}
// Coin return one coin from the list
func (r *XRand) Coin(src sdk.Coins) sdk.Coin {
return src[r.Intn(len(src))]
}
func (r *XRand) DecN(max math.LegacyDec) math.LegacyDec {
return simtypes.RandomDecAmount(r.Rand, max)
}
func (r *XRand) IntInRange(min, max int) int {
return r.Intn(max-min) + min
}
// Uint64InRange returns a pseudo-random uint64 number in the range [min, max].
// It panics when min >= max
func (r *XRand) Uint64InRange(min, max uint64) uint64 {
if min >= max {
panic("min must be less than max")
}
return uint64(r.Int63n(int64(max-min)) + int64(min))
}
// Uint32InRange returns a pseudo-random uint32 number in the range [min, max].
// It panics when min >= max
func (r *XRand) Uint32InRange(min, max uint32) uint32 {
if min >= max {
panic("min must be less than max")
}
return uint32(r.Intn(int(max-min))) + min
}
func (r *XRand) PositiveSDKIntn(max math.Int) (math.Int, error) {
return simtypes.RandPositiveInt(r.Rand, max)
}
func (r *XRand) PositiveSDKIntInRange(min, max math.Int) (math.Int, error) {
diff := max.Sub(min)
if !diff.IsPositive() {
return math.Int{}, errors.New("min value must not be greater or equal to max")
}
result, err := r.PositiveSDKIntn(diff)
if err != nil {
return math.Int{}, err
}
return result.Add(min), nil
}
// Timestamp returns a timestamp between Jan 1, 2062 and Jan 1, 2262
func (r *XRand) Timestamp() time.Time {
return simtypes.RandTimestamp(r.Rand)
}
func (r *XRand) Bool() bool {
return r.Intn(100) > 50
}
func (r *XRand) Amount(balance math.Int) math.Int {
return simtypes.RandomAmount(r.Rand, balance)
}

67
simsx/environment_test.go Normal file
View File

@ -0,0 +1,67 @@
package simsx
import (
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
func TestChainDataSourceAnyAccount(t *testing.T) {
codec := txConfig().SigningContext().AddressCodec()
r := rand.New(rand.NewSource(1))
accs := simtypes.RandomAccounts(r, 3)
specs := map[string]struct {
filters []SimAccountFilter
assert func(t *testing.T, got SimAccount, reporter SimulationReporter)
}{
"no filters": {
assert: func(t *testing.T, got SimAccount, reporter SimulationReporter) { //nolint:thelper // not a helper
assert.NotEmpty(t, got.AddressBech32)
assert.False(t, reporter.IsSkipped())
},
},
"filter": {
filters: []SimAccountFilter{SimAccountFilterFn(func(a SimAccount) bool { return a.AddressBech32 == accs[2].AddressBech32 })},
assert: func(t *testing.T, got SimAccount, reporter SimulationReporter) { //nolint:thelper // not a helper
assert.Equal(t, accs[2].AddressBech32, got.AddressBech32)
assert.False(t, reporter.IsSkipped())
},
},
"no match": {
filters: []SimAccountFilter{SimAccountFilterFn(func(a SimAccount) bool { return false })},
assert: func(t *testing.T, got SimAccount, reporter SimulationReporter) { //nolint:thelper // not a helper
assert.Empty(t, got.AddressBech32)
assert.True(t, reporter.IsSkipped())
},
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
reporter := NewBasicSimulationReporter()
c := NewChainDataSource(sdk.Context{}, r, nil, nil, codec, accs...)
a := c.AnyAccount(reporter, spec.filters...)
spec.assert(t, a, reporter)
})
}
}
func TestChainDataSourceGetHasAccount(t *testing.T) {
codec := txConfig().SigningContext().AddressCodec()
r := rand.New(rand.NewSource(1))
accs := simtypes.RandomAccounts(r, 3)
reporter := NewBasicSimulationReporter()
c := NewChainDataSource(sdk.Context{}, r, nil, nil, codec, accs...)
exisingAddr := accs[0].AddressBech32
assert.Equal(t, exisingAddr, c.GetAccount(reporter, exisingAddr).AddressBech32)
assert.False(t, reporter.IsSkipped())
assert.True(t, c.HasAccount(exisingAddr))
// and non-existing account
reporter = NewBasicSimulationReporter()
assert.Empty(t, c.GetAccount(reporter, "non-existing").AddressBech32)
assert.False(t, c.HasAccount("non-existing"))
assert.True(t, reporter.IsSkipped())
}

175
simsx/msg_factory.go Normal file
View File

@ -0,0 +1,175 @@
package simsx
import (
"context"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// SimMsgFactoryX is an interface for creating and handling fuzz test-like simulation messages in the system.
type SimMsgFactoryX interface {
// MsgType returns an empty instance of the concrete message type that the factory provides.
// This instance is primarily used for deduplication and reporting purposes.
// The result must not be nil
MsgType() sdk.Msg
// Create returns a FactoryMethod implementation which is responsible for constructing new instances of the message
// on each invocation.
Create() FactoryMethod
// DeliveryResultHandler returns a SimDeliveryResultHandler instance which processes the delivery
// response error object. While most simulation factories anticipate successful message delivery,
// certain factories employ this handler to validate execution errors, thereby covering negative
// test scenarios.
DeliveryResultHandler() SimDeliveryResultHandler
}
type (
// FactoryMethod is a method signature implemented by concrete message factories for SimMsgFactoryX
//
// This factory method is responsible for creating a new `sdk.Msg` instance and determining the
// proposed signers who are expected to successfully sign the message for delivery.
//
// Parameters:
// - ctx: The context for the operation
// - testData: A pointer to a `ChainDataSource` which provides helper methods and simple access to accounts
// and balances within the chain.
// - reporter: An instance of `SimulationReporter` used to report the results of the simulation.
// If no valid message can be provided, the factory method should call `reporter.Skip("some comment")`
// with both `signer` and `msg` set to nil.
//
// Returns:
// - signer: A slice of `SimAccount` representing the proposed signers.
// - msg: An instance of `sdk.Msg` representing the message to be delivered.
FactoryMethod func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg)
// FactoryMethodWithFutureOps extended message factory method for the gov module or others that have to schedule operations for a future block.
FactoryMethodWithFutureOps[T sdk.Msg] func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter, fOpsReg FutureOpsRegistry) ([]SimAccount, T)
// FactoryMethodWithDeliveryResultHandler extended factory method that can return a result handler, that is executed on the delivery tx error result.
// This is used in staking for example to validate negative execution results.
FactoryMethodWithDeliveryResultHandler[T sdk.Msg] func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg T, handler SimDeliveryResultHandler)
)
var _ SimMsgFactoryX = &ResultHandlingSimMsgFactory[sdk.Msg]{}
// ResultHandlingSimMsgFactory message factory with a delivery error result handler configured.
type ResultHandlingSimMsgFactory[T sdk.Msg] struct {
SimMsgFactoryFn[T]
resultHandler SimDeliveryResultHandler
}
// NewSimMsgFactoryWithDeliveryResultHandler constructor
func NewSimMsgFactoryWithDeliveryResultHandler[T sdk.Msg](f FactoryMethodWithDeliveryResultHandler[T]) *ResultHandlingSimMsgFactory[T] {
r := &ResultHandlingSimMsgFactory[T]{
resultHandler: expectNoError,
}
r.SimMsgFactoryFn = func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg T) {
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
}
var (
_ SimMsgFactoryX = &LazyStateSimMsgFactory[sdk.Msg]{}
_ HasFutureOpsRegistry = &LazyStateSimMsgFactory[sdk.Msg]{}
)
// LazyStateSimMsgFactory stateful message factory with weighted proposals and future operation
// registry initialized lazy before execution.
type LazyStateSimMsgFactory[T sdk.Msg] struct {
SimMsgFactoryFn[T]
fsOpsReg FutureOpsRegistry
}
func NewSimMsgFactoryWithFutureOps[T sdk.Msg](f FactoryMethodWithFutureOps[T]) *LazyStateSimMsgFactory[T] {
r := &LazyStateSimMsgFactory[T]{}
r.SimMsgFactoryFn = func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg T) {
signer, msg = f(ctx, testData, reporter, r.fsOpsReg)
return
}
return r
}
func (c *LazyStateSimMsgFactory[T]) SetFutureOpsRegistry(registry FutureOpsRegistry) {
c.fsOpsReg = registry
}
// pass errors through and don't handle them
func expectNoError(err error) error {
return err
}
var _ SimMsgFactoryX = SimMsgFactoryFn[sdk.Msg](nil)
// SimMsgFactoryFn is the default factory for most cases. It does not create future operations but ensures successful message delivery.
type SimMsgFactoryFn[T sdk.Msg] func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg T)
// MsgType returns an empty instance of type T, which implements `sdk.Msg`.
func (f SimMsgFactoryFn[T]) MsgType() sdk.Msg {
var x T
return x
}
func (f SimMsgFactoryFn[T]) Create() FactoryMethod {
// adapter to return sdk.Msg instead of typed result to match FactoryMethod signature
return func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) ([]SimAccount, sdk.Msg) {
return f(ctx, testData, reporter)
}
}
func (f SimMsgFactoryFn[T]) DeliveryResultHandler() SimDeliveryResultHandler {
return expectNoError
}
func (f SimMsgFactoryFn[T]) Cast(msg sdk.Msg) T {
return msg.(T)
}
type tuple struct {
signer []SimAccount
msg sdk.Msg
}
// SafeRunFactoryMethod runs the factory method on a separate goroutine to abort early when the context is canceled via reporter skip
func SafeRunFactoryMethod(
ctx context.Context,
data *ChainDataSource,
reporter SimulationReporter,
f FactoryMethod,
) (signer []SimAccount, msg sdk.Msg) {
r := make(chan tuple)
go func() {
defer recoverPanicForSkipped(reporter, r)
signer, msg := f(ctx, data, reporter)
r <- tuple{signer: signer, msg: msg}
}()
select {
case t, ok := <-r:
if !ok {
return nil, nil
}
return t.signer, t.msg
case <-ctx.Done():
reporter.Skip("context closed")
return nil, nil
}
}
func recoverPanicForSkipped(reporter SimulationReporter, resultChan chan tuple) {
if r := recover(); r != nil {
if !reporter.IsSkipped() {
panic(r)
}
close(resultChan)
}
}

97
simsx/msg_factory_test.go Normal file
View File

@ -0,0 +1,97 @@
package simsx
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func TestMsgFactories(t *testing.T) {
myMsg := testdata.NewTestMsg()
mySigners := []SimAccount{{}}
specs := map[string]struct {
src SimMsgFactoryX
expErrHandled bool
}{
"default": {
src: SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
return mySigners, myMsg
}),
},
"with delivery result handler": {
src: NewSimMsgFactoryWithDeliveryResultHandler[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg, handler SimDeliveryResultHandler) {
return mySigners, myMsg, func(err error) error { return nil }
}),
expErrHandled: true,
},
"with future ops": {
src: NewSimMsgFactoryWithFutureOps[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter, fOpsReg FutureOpsRegistry) ([]SimAccount, *testdata.TestMsg) {
return mySigners, myMsg
}),
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
assert.Equal(t, (*testdata.TestMsg)(nil), spec.src.MsgType())
factoryMethod := spec.src.Create()
require.NotNil(t, factoryMethod)
gotSigners, gotMsg := factoryMethod(context.Background(), nil, nil)
assert.Equal(t, mySigners, gotSigners)
assert.Equal(t, gotMsg, myMsg)
require.NotNil(t, spec.src.DeliveryResultHandler())
gotErr := spec.src.DeliveryResultHandler()(errors.New("testing"))
assert.Equal(t, spec.expErrHandled, gotErr == nil)
})
}
}
func TestRunWithFailFast(t *testing.T) {
myTestMsg := testdata.NewTestMsg()
mySigners := []SimAccount{SimAccountFixture()}
specs := map[string]struct {
factory FactoryMethod
expSigners []SimAccount
expMsg sdk.Msg
expSkipped bool
}{
"factory completes": {
factory: func(ctx context.Context, _ *ChainDataSource, reporter SimulationReporter) ([]SimAccount, sdk.Msg) {
return mySigners, myTestMsg
},
expSigners: mySigners,
expMsg: myTestMsg,
},
"factory skips": {
factory: func(ctx context.Context, _ *ChainDataSource, reporter SimulationReporter) ([]SimAccount, sdk.Msg) {
reporter.Skip("testing")
return nil, nil
},
expSkipped: true,
},
"factory skips and panics": {
factory: func(ctx context.Context, _ *ChainDataSource, reporter SimulationReporter) ([]SimAccount, sdk.Msg) {
reporter.Skip("testing")
panic("should be handled")
},
expSkipped: true,
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
ctx, done := context.WithCancel(context.Background())
reporter := NewBasicSimulationReporter().WithScope(&testdata.TestMsg{}, SkipHookFn(func(...any) { done() }))
gotSigners, gotMsg := SafeRunFactoryMethod(ctx, nil, reporter, spec.factory)
assert.Equal(t, spec.expSigners, gotSigners)
assert.Equal(t, spec.expMsg, gotMsg)
assert.Equal(t, spec.expSkipped, reporter.IsSkipped())
})
}
}

52
simsx/params.go Normal file
View File

@ -0,0 +1,52 @@
package simsx
import (
"math/rand"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
// WeightSource interface for retrieving weights based on a name and a default value.
type WeightSource interface {
Get(name string, defaultValue uint32) uint32
}
// WeightSourceFn function adapter that implements WeightSource.
// Example:
//
// weightSource := WeightSourceFn(func(name string, defaultValue uint32) uint32 {
// // implementation code...
// })
type WeightSourceFn func(name string, defaultValue uint32) uint32
func (f WeightSourceFn) Get(name string, defaultValue uint32) uint32 {
return f(name, defaultValue)
}
// ParamWeightSource is an adapter to the simtypes.AppParams object. This function returns a WeightSource
// implementation that retrieves weights
// based on a name and a default value. The implementation uses the provided AppParams
// to get or generate the weight value. If the weight value exists in the AppParams,
// it is decoded and returned. Otherwise, the provided ParamSimulator is used to generate
// a random value or default value.
//
// The WeightSource implementation is a WeightSourceFn function adapter that implements
// the WeightSource interface. It takes in a name string and a defaultValue uint32 as
// parameters and returns the weight value as a uint32.
//
// Example Usage:
//
// appParams := simtypes.AppParams{}
// // add parameters to appParams
//
// weightSource := ParamWeightSource(appParams)
// weightSource.Get("some_weight", 100)
func ParamWeightSource(p simtypes.AppParams) WeightSource {
return WeightSourceFn(func(name string, defaultValue uint32) uint32 {
var result uint32
p.GetOrGenerate("op_weight_"+name, &result, nil, func(_ *rand.Rand) {
result = defaultValue
})
return result
})
}

317
simsx/registry.go Normal file
View File

@ -0,0 +1,317 @@
package simsx
import (
"cmp"
"context"
"iter"
"maps"
"math/rand"
"slices"
"strings"
"time"
"cosmossdk.io/core/address"
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
type (
// Registry is an abstract entry point to register message factories with weights
Registry interface {
Add(weight uint32, f SimMsgFactoryX)
}
// FutureOpsRegistry register message factories for future blocks
FutureOpsRegistry interface {
Add(blockTime time.Time, f SimMsgFactoryX)
}
// AccountSourceX Account and Module account
AccountSourceX interface {
AccountSource
ModuleAccountSource
}
)
// WeightedProposalMsgIter iterator for weighted gov proposal payload messages
type WeightedProposalMsgIter = iter.Seq2[uint32, FactoryMethod]
var _ Registry = &WeightedOperationRegistryAdapter{}
// common types for abstract registry without generics
type regCommon struct {
reporter SimulationReporter
ak AccountSourceX
bk BalanceSource
addressCodec address.Codec
txConfig client.TxConfig
logger log.Logger
}
func (c regCommon) newChainDataSource(ctx context.Context, r *rand.Rand, accs ...simtypes.Account) *ChainDataSource {
return NewChainDataSource(ctx, r, c.ak, c.bk, c.addressCodec, accs...)
}
type AbstractRegistry[T any] struct {
regCommon
items []T
}
type operation func(
r *rand.Rand, app AppEntrypoint, ctx sdk.Context,
accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error)
// make api compatible to simtypes.Operation
func (o operation) toLegacyOp() simtypes.Operation {
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, chainID string) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) {
return o(r, app, ctx, accounts, chainID)
}
}
var _ simtypes.WeightedOperation = weightedOperation{}
type weightedOperation struct {
weight uint32
op operation
}
func (w weightedOperation) Weight() int {
return int(w.weight)
}
func (w weightedOperation) Op() simtypes.Operation {
return w.op.toLegacyOp()
}
// WeightedOperationRegistryAdapter is an implementation of the Registry interface that provides adapters to use the new message factories
// with the legacy simulation system
type WeightedOperationRegistryAdapter struct {
AbstractRegistry[weightedOperation]
}
// NewSimsMsgRegistryAdapter creates a new instance of SimsRegistryAdapter for WeightedOperation types.
func NewSimsMsgRegistryAdapter(
reporter SimulationReporter,
ak AccountSourceX,
bk BalanceSource,
txConfig client.TxConfig,
logger log.Logger,
) *WeightedOperationRegistryAdapter {
return &WeightedOperationRegistryAdapter{
AbstractRegistry: AbstractRegistry[weightedOperation]{
regCommon: regCommon{
reporter: reporter,
ak: ak,
bk: bk,
txConfig: txConfig,
addressCodec: txConfig.SigningContext().AddressCodec(),
logger: logger,
},
},
}
}
// Add adds a new weighted operation to the collection
func (l *WeightedOperationRegistryAdapter) Add(weight uint32, fx SimMsgFactoryX) {
if fx == nil {
panic("message factory must not be nil")
}
if weight == 0 {
return
}
obj := weightedOperation{weight: weight, op: legacyOperationAdapter(l.regCommon, fx)}
l.items = append(l.items, obj)
}
type HasFutureOpsRegistry interface {
SetFutureOpsRegistry(FutureOpsRegistry)
}
// msg factory to legacy Operation type
func legacyOperationAdapter(l regCommon, fx SimMsgFactoryX) operation {
return func(
r *rand.Rand, app AppEntrypoint, ctx sdk.Context,
accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
xCtx, done := context.WithCancel(ctx)
ctx = sdk.UnwrapSDKContext(xCtx)
testData := l.newChainDataSource(ctx, r, accs...)
reporter := l.reporter.WithScope(fx.MsgType(), SkipHookFn(func(args ...any) { done() }))
fOpsReg := NewFutureOpsRegistry(l)
if fx, ok := fx.(HasFutureOpsRegistry); ok {
fx.SetFutureOpsRegistry(fOpsReg)
}
from, msg := SafeRunFactoryMethod(ctx, testData, reporter, fx.Create())
futOps := fOpsReg.items
weightedOpsResult := DeliverSimsMsg(ctx, reporter, app, r, l.txConfig, l.ak, chainID, msg, fx.DeliveryResultHandler(), from...)
err := reporter.Close()
return weightedOpsResult, futOps, err
}
}
func NewFutureOpsRegistry(l regCommon) *FutureOperationRegistryAdapter {
return &FutureOperationRegistryAdapter{regCommon: l}
}
type FutureOperationRegistryAdapter AbstractRegistry[simtypes.FutureOperation]
func (l *FutureOperationRegistryAdapter) Add(blockTime time.Time, fx SimMsgFactoryX) {
if fx == nil {
panic("message factory must not be nil")
}
if blockTime.IsZero() {
return
}
obj := simtypes.FutureOperation{
BlockTime: blockTime,
Op: legacyOperationAdapter(l.regCommon, fx).toLegacyOp(),
}
l.items = append(l.items, obj)
}
var _ Registry = &UniqueTypeRegistry{}
type UniqueTypeRegistry map[string]WeightedFactory
func NewUniqueTypeRegistry() UniqueTypeRegistry {
return make(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 {
panic("type is already registered: " + msgTypeURL)
}
s[msgTypeURL] = WeightedFactory{Weight: weight, Factory: f}
}
// Iterator returns an iterator function for a Go for loop sorted by weight desc.
func (s UniqueTypeRegistry) Iterator() WeightedProposalMsgIter {
sortedWeightedFactory := slices.SortedFunc(maps.Values(s), func(a, b WeightedFactory) int {
return a.Compare(b)
})
return func(yield func(uint32, FactoryMethod) bool) {
for _, v := range sortedWeightedFactory {
if !yield(v.Weight, v.Factory.Create()) {
return
}
}
}
}
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
Factory SimMsgFactoryX
}
// Compare compares the WeightedFactory f with another WeightedFactory b.
// The comparison is done by comparing the weight of f with the weight of b.
// If the weight of f is greater than the weight of b, it returns 1.
// If the weight of f is less than the weight of b, it returns -1.
// If the weights are equal, it compares the TypeURL of the factories using strings.Compare.
// Returns an integer indicating the comparison result.
func (f WeightedFactory) Compare(b WeightedFactory) int {
switch {
case f.Weight > b.Weight:
return 1
case f.Weight < b.Weight:
return -1
default:
return strings.Compare(sdk.MsgTypeURL(f.Factory.MsgType()), sdk.MsgTypeURL(b.Factory.MsgType()))
}
}
// WeightedFactoryMethod is a data tuple used for registering legacy proposal operations
type WeightedFactoryMethod struct {
Weight uint32
Factory FactoryMethod
}
type WeightedFactoryMethods []WeightedFactoryMethod
// NewWeightedFactoryMethods constructor
func NewWeightedFactoryMethods() WeightedFactoryMethods {
return make(WeightedFactoryMethods, 0)
}
// Add adds a new WeightedFactoryMethod to the WeightedFactoryMethods slice.
// If weight is zero or f is nil, it returns without making any changes.
func (s *WeightedFactoryMethods) Add(weight uint32, f FactoryMethod) {
if weight == 0 {
return
}
if f == nil {
panic("message factory must not be nil")
}
*s = append(*s, WeightedFactoryMethod{Weight: weight, Factory: f})
}
// Iterator returns an iterator function for a Go for loop sorted by weight desc.
func (s WeightedFactoryMethods) Iterator() WeightedProposalMsgIter {
slices.SortFunc(s, func(e, e2 WeightedFactoryMethod) int {
return cmp.Compare(e.Weight, e2.Weight)
})
return func(yield func(uint32, FactoryMethod) bool) {
for _, v := range s {
if !yield(v.Weight, v.Factory) {
return
}
}
}
}
// legacy operation to Msg factory type
func legacyToMsgFactoryAdapter(fn simtypes.MsgSimulatorFn) FactoryMethod {
return func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
return []SimAccount{}, fn(testData.r, sdk.UnwrapSDKContext(ctx), testData.AllAccounts())
}
}
// AppendIterators takes multiple WeightedProposalMsgIter and returns a single iterator that sequentially yields items after each one.
func AppendIterators(iterators ...WeightedProposalMsgIter) WeightedProposalMsgIter {
return func(yield func(uint32, FactoryMethod) bool) {
for _, it := range iterators {
it(yield)
}
}
}

211
simsx/registry_test.go Normal file
View File

@ -0,0 +1,211 @@
package simsx
import (
"context"
"errors"
"math/rand"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
func TestSimsMsgRegistryAdapter(t *testing.T) {
senderAcc := SimAccountFixture()
accs := []simtypes.Account{senderAcc.Account}
ak := MockAccountSourceX{GetAccountFn: MemoryAccountSource(senderAcc).GetAccount}
myMsg := testdata.NewTestMsg(senderAcc.Address)
ctx := sdk.Context{}.WithContext(context.Background())
futureTime := time.Now().Add(time.Second)
specs := map[string]struct {
factory SimMsgFactoryX
expFactoryMsg sdk.Msg
expFactoryErr error
expDeliveryErr error
expFutureOpsCount int
}{
"successful execution": {
factory: SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
return []SimAccount{senderAcc}, myMsg
}),
expFactoryMsg: myMsg,
},
"skip execution": {
factory: SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
reporter.Skip("testing")
return nil, nil
}),
},
"future ops registration": {
factory: NewSimMsgFactoryWithFutureOps[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter, fOpsReg FutureOpsRegistry) (signer []SimAccount, msg *testdata.TestMsg) {
fOpsReg.Add(futureTime, SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
return []SimAccount{senderAcc}, myMsg
}))
return []SimAccount{senderAcc}, myMsg
}),
expFactoryMsg: myMsg,
expFutureOpsCount: 1,
},
"error in factory execution": {
factory: NewSimMsgFactoryWithFutureOps[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter, fOpsReg FutureOpsRegistry) (signer []SimAccount, msg *testdata.TestMsg) {
reporter.Fail(errors.New("testing"))
return nil, nil
}),
expFactoryErr: errors.New("testing"),
},
"missing senders": {
factory: SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
return nil, myMsg
}),
expDeliveryErr: errors.New("no senders"),
},
"error in delivery execution": {
factory: SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
return []SimAccount{senderAcc}, myMsg
}),
expDeliveryErr: errors.New("testing"),
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
r := NewBasicSimulationReporter()
reg := NewSimsMsgRegistryAdapter(r, ak, nil, txConfig(), log.NewNopLogger())
// when
reg.Add(100, spec.factory)
// then
gotOps := reg.items
require.Len(t, gotOps, 1)
assert.Equal(t, uint32(100), gotOps[0].weight)
// and when ops executed
var capturedTXs []sdk.Tx
captureTXApp := AppEntrypointFn(func(_txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
capturedTXs = append(capturedTXs, tx)
return sdk.GasInfo{}, &sdk.Result{}, spec.expDeliveryErr
})
fn := gotOps[0].op
gotOpsResult, gotFOps, gotErr := fn(rand.New(rand.NewSource(1)), captureTXApp, ctx, accs, "testchain")
// then
if spec.expFactoryErr != nil {
require.Equal(t, spec.expFactoryErr, gotErr)
assert.Empty(t, gotFOps)
assert.Equal(t, gotOpsResult.OK, spec.expFactoryErr == nil)
assert.Empty(t, gotOpsResult.Comment)
require.Empty(t, capturedTXs)
}
if spec.expDeliveryErr != nil {
require.Equal(t, spec.expDeliveryErr, gotErr)
}
// and verify TX delivery
if spec.expFactoryMsg != nil {
require.Len(t, capturedTXs, 1)
require.Len(t, capturedTXs[0].GetMsgs(), 1)
assert.Equal(t, spec.expFactoryMsg, capturedTXs[0].GetMsgs()[0])
}
assert.Len(t, gotFOps, spec.expFutureOpsCount)
})
}
}
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
exp []WeightedFactoryMethod
expErr bool
}{
"unique": {
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,
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
reg := NewUniqueTypeRegistry()
if spec.expErr {
require.Panics(t, func() {
for _, v := range spec.src {
reg.Add(v.Weight, v.Factory)
}
})
return
}
for _, v := range spec.src {
reg.Add(v.Weight, v.Factory)
}
// then
got := readAll(reg.Iterator())
require.Len(t, got, len(spec.exp))
})
}
}
func TestWeightedFactories(t *testing.T) {
r := NewWeightedFactoryMethods()
f1 := func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
panic("not implemented")
}
f2 := func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
panic("not implemented")
}
r.Add(1, f1)
r.Add(2, f2)
got := readAll(r.Iterator())
require.Len(t, got, 2)
assert.Equal(t, uint32(1), r[0].Weight)
assert.Equal(t, uint32(2), r[1].Weight)
}
func TestAppendIterators(t *testing.T) {
r1 := NewWeightedFactoryMethods()
r1.Add(2, func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
panic("not implemented")
})
r1.Add(2, func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
panic("not implemented")
})
r1.Add(3, func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
panic("not implemented")
})
r2 := NewUniqueTypeRegistry()
r2.Add(1, SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
panic("not implemented")
}))
// when
all := readAll(AppendIterators(r1.Iterator(), r2.Iterator()))
// then
require.Len(t, all, 4)
gotWeights := Collect(all, func(a WeightedFactoryMethod) uint32 { return a.Weight })
assert.Equal(t, []uint32{2, 2, 3, 1}, gotWeights)
}
func readAll(iterator WeightedProposalMsgIter) []WeightedFactoryMethod {
var ret []WeightedFactoryMethod
for w, f := range iterator {
ret = append(ret, WeightedFactoryMethod{Weight: w, Factory: f})
}
return ret
}

271
simsx/reporter.go Normal file
View File

@ -0,0 +1,271 @@
package simsx
import (
"errors"
"fmt"
"maps"
"slices"
"strings"
"sync"
"sync/atomic"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
// SimulationReporter is an interface for reporting the result of a simulation run.
type SimulationReporter interface {
WithScope(msg sdk.Msg, optionalSkipHook ...SkipHook) SimulationReporter
Skip(comment string)
Skipf(comment string, args ...any)
// IsSkipped returns true when skipped or completed
IsSkipped() bool
ToLegacyOperationMsg() simtypes.OperationMsg
// Fail complete with failure
Fail(err error, comments ...string)
// Success complete with success
Success(msg sdk.Msg, comments ...string)
// Close returns error captured on fail
Close() error
Comment() string
}
var _ SimulationReporter = &BasicSimulationReporter{}
type ReporterStatus uint8
const (
undefined ReporterStatus = iota
skipped ReporterStatus = iota
completed ReporterStatus = iota
)
func (s ReporterStatus) String() string {
switch s {
case skipped:
return "skipped"
case completed:
return "completed"
default:
return "undefined"
}
}
func reporterStatusFrom(s uint32) ReporterStatus {
switch s {
case uint32(skipped):
return skipped
case uint32(completed):
return completed
default:
return undefined
}
}
// SkipHook is an interface that represents a callback hook used triggered on skip operations.
// It provides a single method `Skip` that accepts variadic arguments. This interface is implemented
// by Go stdlib testing.T and testing.B
type SkipHook interface {
Skip(args ...any)
}
var _ SkipHook = SkipHookFn(nil)
type SkipHookFn func(args ...any)
func (s SkipHookFn) Skip(args ...any) {
s(args...)
}
type BasicSimulationReporter struct {
skipCallbacks []SkipHook
completedCallback func(reporter *BasicSimulationReporter)
module string
msgTypeURL string
status atomic.Uint32
cMX sync.RWMutex
comments []string
error error
summary *ExecutionSummary
}
// NewBasicSimulationReporter constructor that accepts an optional callback hook that is called on state transition to skipped status
// A typical implementation for this hook is testing.T or testing.B.
func NewBasicSimulationReporter(optionalSkipHook ...SkipHook) *BasicSimulationReporter {
r := &BasicSimulationReporter{
skipCallbacks: optionalSkipHook,
summary: NewExecutionSummary(),
}
r.completedCallback = func(child *BasicSimulationReporter) {
r.summary.Add(child.module, child.msgTypeURL, reporterStatusFrom(child.status.Load()), child.Comment())
}
return r
}
// WithScope is a method of the BasicSimulationReporter type that creates a new instance of SimulationReporter
// with an additional scope specified by the input `msg`. The msg is used to set type, module and binary data as
// context for the legacy operation.
// The WithScope method acts as a constructor to initialize state and has to be called before using the instance
// in DeliverSimsMsg.
//
// The method accepts an optional `optionalSkipHook` parameter
// that can be used to add a callback hook that is triggered on skip operations additional to any parent skip hook.
// This method returns the newly created
// SimulationReporter instance.
func (x *BasicSimulationReporter) WithScope(msg sdk.Msg, optionalSkipHook ...SkipHook) SimulationReporter {
typeURL := sdk.MsgTypeURL(msg)
r := &BasicSimulationReporter{
skipCallbacks: append(x.skipCallbacks, optionalSkipHook...),
completedCallback: x.completedCallback,
error: x.error,
msgTypeURL: typeURL,
module: sdk.GetModuleNameFromTypeURL(typeURL),
comments: slices.Clone(x.comments),
}
r.status.Store(x.status.Load())
return r
}
func (x *BasicSimulationReporter) Skip(comment string) {
x.toStatus(skipped, comment)
}
func (x *BasicSimulationReporter) Skipf(comment string, args ...any) {
x.Skip(fmt.Sprintf(comment, args...))
}
func (x *BasicSimulationReporter) IsSkipped() bool {
return reporterStatusFrom(x.status.Load()) > undefined
}
func (x *BasicSimulationReporter) ToLegacyOperationMsg() simtypes.OperationMsg {
switch reporterStatusFrom(x.status.Load()) {
case skipped:
return simtypes.NoOpMsg(x.module, x.msgTypeURL, x.Comment())
case completed:
x.cMX.RLock()
err := x.error
x.cMX.RUnlock()
if err == nil {
return simtypes.NewOperationMsgBasic(x.module, x.msgTypeURL, x.Comment(), true, []byte{})
} else {
return simtypes.NewOperationMsgBasic(x.module, x.msgTypeURL, x.Comment(), false, []byte{})
}
default:
x.Fail(errors.New("operation aborted before msg was executed"))
return x.ToLegacyOperationMsg()
}
}
func (x *BasicSimulationReporter) Fail(err error, comments ...string) {
if !x.toStatus(completed, comments...) {
return
}
x.cMX.Lock()
defer x.cMX.Unlock()
x.error = err
}
func (x *BasicSimulationReporter) Success(msg sdk.Msg, comments ...string) {
if !x.toStatus(completed, comments...) {
return
}
if msg == nil {
return
}
}
func (x *BasicSimulationReporter) Close() error {
x.completedCallback(x)
x.cMX.RLock()
defer x.cMX.RUnlock()
return x.error
}
func (x *BasicSimulationReporter) toStatus(next ReporterStatus, comments ...string) bool {
oldStatus := reporterStatusFrom(x.status.Load())
if oldStatus > next {
panic(fmt.Sprintf("can not switch from status %s to %s", oldStatus, next))
}
if !x.status.CompareAndSwap(uint32(oldStatus), uint32(next)) {
return false
}
x.cMX.Lock()
newComments := append(x.comments, comments...)
x.comments = newComments
x.cMX.Unlock()
if oldStatus != skipped && next == skipped {
prettyComments := strings.Join(newComments, ", ")
for _, hook := range x.skipCallbacks {
hook.Skip(prettyComments)
}
}
return true
}
func (x *BasicSimulationReporter) Comment() string {
x.cMX.RLock()
defer x.cMX.RUnlock()
return strings.Join(x.comments, ", ")
}
func (x *BasicSimulationReporter) Summary() *ExecutionSummary {
return x.summary
}
type ExecutionSummary struct {
mx sync.RWMutex
counts map[string]int // module to count
skipReasons map[string]map[string]int // msg type to reason->count
}
func NewExecutionSummary() *ExecutionSummary {
return &ExecutionSummary{counts: make(map[string]int), skipReasons: make(map[string]map[string]int)}
}
func (s *ExecutionSummary) Add(module, url string, status ReporterStatus, comment string) {
s.mx.Lock()
defer s.mx.Unlock()
combinedKey := fmt.Sprintf("%s_%s", module, status.String())
s.counts[combinedKey] += 1
if status == completed {
return
}
r, ok := s.skipReasons[url]
if !ok {
r = make(map[string]int)
s.skipReasons[url] = r
}
r[comment] += 1
}
func (s *ExecutionSummary) String() string {
s.mx.RLock()
defer s.mx.RUnlock()
keys := slices.Sorted(maps.Keys(s.counts))
var sb strings.Builder
for _, key := range keys {
sb.WriteString(fmt.Sprintf("%s: %d\n", key, s.counts[key]))
}
if len(s.skipReasons) != 0 {
sb.WriteString("\nSkip reasons:\n")
}
for m, c := range s.skipReasons {
values := maps.Values(c)
keys := maps.Keys(c)
sb.WriteString(fmt.Sprintf("%d\t%s: %q\n", sum(slices.Collect(values)), m, slices.Collect(keys)))
}
return sb.String()
}
func sum(values []int) int {
var r int
for _, v := range values {
r += v
}
return r
}

219
simsx/reporter_test.go Normal file
View File

@ -0,0 +1,219 @@
package simsx
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
func TestSimulationReporterToLegacy(t *testing.T) {
myErr := errors.New("my-error")
myMsg := testdata.NewTestMsg([]byte{1})
specs := map[string]struct {
setup func() SimulationReporter
expOp simtypes.OperationMsg
expErr error
}{
"init only": {
setup: func() SimulationReporter { return NewBasicSimulationReporter() },
expOp: simtypes.NewOperationMsgBasic("", "", "", false, []byte{}),
expErr: errors.New("operation aborted before msg was executed"),
},
"success result": {
setup: func() SimulationReporter {
r := NewBasicSimulationReporter().WithScope(myMsg)
r.Success(myMsg, "testing")
return r
},
expOp: simtypes.NewOperationMsgBasic("TestMsg", "/testpb.TestMsg", "testing", true, []byte{}),
},
"error result": {
setup: func() SimulationReporter {
r := NewBasicSimulationReporter().WithScope(myMsg)
r.Fail(myErr, "testing")
return r
},
expOp: simtypes.NewOperationMsgBasic("TestMsg", "/testpb.TestMsg", "testing", false, []byte{}),
expErr: myErr,
},
"last error wins": {
setup: func() SimulationReporter {
r := NewBasicSimulationReporter().WithScope(myMsg)
r.Fail(errors.New("other-err"), "testing1")
r.Fail(myErr, "testing2")
return r
},
expOp: simtypes.NewOperationMsgBasic("TestMsg", "/testpb.TestMsg", "testing1, testing2", false, []byte{}),
expErr: myErr,
},
"skipped ": {
setup: func() SimulationReporter {
r := NewBasicSimulationReporter().WithScope(myMsg)
r.Skip("testing")
return r
},
expOp: simtypes.NoOpMsg("TestMsg", "/testpb.TestMsg", "testing"),
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
r := spec.setup()
assert.Equal(t, spec.expOp, r.ToLegacyOperationMsg())
require.Equal(t, spec.expErr, r.Close())
})
}
}
func TestSimulationReporterTransitions(t *testing.T) {
specs := map[string]struct {
setup func(r SimulationReporter)
expStatus ReporterStatus
expPanic bool
}{
"undefined->skipped": {
setup: func(r SimulationReporter) {
r.Skip("testing")
},
expStatus: skipped,
},
"skipped->skipped": {
setup: func(r SimulationReporter) {
r.Skip("testing1")
r.Skip("testing2")
},
expStatus: skipped,
},
"skipped->completed": {
setup: func(r SimulationReporter) {
r.Skip("testing1")
r.Success(nil, "testing2")
},
expStatus: completed,
},
"completed->completed": {
setup: func(r SimulationReporter) {
r.Success(nil, "testing1")
r.Fail(nil, "testing2")
},
expStatus: completed,
},
"completed->completed2": {
setup: func(r SimulationReporter) {
r.Fail(nil, "testing1")
r.Success(nil, "testing2")
},
expStatus: completed,
},
"completed->skipped: rejected": {
setup: func(r SimulationReporter) {
r.Success(nil, "testing1")
r.Skip("testing2")
},
expPanic: true,
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
r := NewBasicSimulationReporter()
if !spec.expPanic {
spec.setup(r)
assert.Equal(t, uint32(spec.expStatus), r.status.Load())
return
}
require.Panics(t, func() {
spec.setup(r)
})
})
}
}
func TestSkipHook(t *testing.T) {
myHook := func() (SkipHookFn, *bool) {
var hookCalled bool
return func(args ...any) {
hookCalled = true
}, &hookCalled
}
fn, myHookCalled := myHook()
r := NewBasicSimulationReporter(fn)
r.Skip("testing")
require.True(t, *myHookCalled)
// and with nested reporter
fn, myHookCalled = myHook()
r = NewBasicSimulationReporter(fn)
fn2, myOtherHookCalled := myHook()
r2 := r.WithScope(testdata.NewTestMsg([]byte{1}), fn2)
r2.Skipf("testing %d", 2)
assert.True(t, *myHookCalled)
assert.True(t, *myOtherHookCalled)
}
func TestReporterSummary(t *testing.T) {
specs := map[string]struct {
do func(t *testing.T, r SimulationReporter)
expSummary map[string]int
expReasons map[string]map[string]int
}{
"skipped": {
do: func(t *testing.T, r SimulationReporter) { //nolint:thelper // not a helper
r2 := r.WithScope(testdata.NewTestMsg([]byte{1}))
r2.Skip("testing")
require.NoError(t, r2.Close())
},
expSummary: map[string]int{"TestMsg_skipped": 1},
expReasons: map[string]map[string]int{"/testpb.TestMsg": {"testing": 1}},
},
"success result": {
do: func(t *testing.T, r SimulationReporter) { //nolint:thelper // not a helper
msg := testdata.NewTestMsg([]byte{1})
r2 := r.WithScope(msg)
r2.Success(msg)
require.NoError(t, r2.Close())
},
expSummary: map[string]int{"TestMsg_completed": 1},
expReasons: map[string]map[string]int{},
},
"error result": {
do: func(t *testing.T, r SimulationReporter) { //nolint:thelper // not a helper
msg := testdata.NewTestMsg([]byte{1})
r2 := r.WithScope(msg)
r2.Fail(errors.New("testing"))
require.Error(t, r2.Close())
},
expSummary: map[string]int{"TestMsg_completed": 1},
expReasons: map[string]map[string]int{},
},
"multiple skipped": {
do: func(t *testing.T, r SimulationReporter) { //nolint:thelper // not a helper
r2 := r.WithScope(testdata.NewTestMsg([]byte{1}))
r2.Skip("testing1")
require.NoError(t, r2.Close())
r3 := r.WithScope(testdata.NewTestMsg([]byte{2}))
r3.Skip("testing2")
require.NoError(t, r3.Close())
},
expSummary: map[string]int{"TestMsg_skipped": 2},
expReasons: map[string]map[string]int{
"/testpb.TestMsg": {"testing1": 1, "testing2": 1},
},
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
r := NewBasicSimulationReporter()
// when
spec.do(t, r)
gotSummary := r.Summary()
// then
require.Equal(t, spec.expSummary, gotSummary.counts)
require.Equal(t, spec.expReasons, gotSummary.skipReasons)
})
}
}

417
simsx/runner.go Normal file
View File

@ -0,0 +1,417 @@
package simsx
import (
"encoding/json"
"fmt"
"io"
"math"
"os"
"path/filepath"
"strings"
"testing"
dbm "github.com/cosmos/cosmos-db"
"github.com/stretchr/testify/require"
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/runtime"
servertypes "github.com/cosmos/cosmos-sdk/server/types"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
"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"
)
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,
}
// SimStateFactory is a factory type that provides a convenient way to create a simulation state for testing.
// It contains the following fields:
// - Codec: a codec used for serializing other objects
// - AppStateFn: a function that returns the app state JSON bytes and the genesis accounts
// - BlockedAddr: a map of blocked addresses
// - AccountSource: an interface for retrieving accounts
// - BalanceSource: an interface for retrieving balance-related information
type SimStateFactory struct {
Codec codec.Codec
AppStateFn simtypes.AppStateFn
BlockedAddr map[string]bool
AccountSource AccountSourceX
BalanceSource BalanceSource
}
// SimulationApp abstract app that is used by sims
type SimulationApp interface {
runtime.AppI
SetNotSigverifyTx()
GetBaseApp() *baseapp.BaseApp
TxConfig() client.TxConfig
Close() error
}
// Run is a helper function that runs a simulation test with the given parameters.
// It calls the RunWithSeeds function with the default seeds and parameters.
//
// This is the entrypoint to run simulation tests that used to run with the runsim binary.
func Run[T SimulationApp](
t *testing.T,
appFactory func(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
appOpts servertypes.AppOptions,
baseAppOptions ...func(*baseapp.BaseApp),
) T,
setupStateFactory func(app T) SimStateFactory,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
t.Helper()
RunWithSeeds(t, appFactory, setupStateFactory, defaultSeeds, nil, postRunActions...)
}
// RunWithSeeds is a helper function that runs a simulation test with the given parameters.
// It iterates over the provided seeds and runs the simulation test for each seed in parallel.
//
// It sets up the environment, creates an instance of the simulation app,
// calls the simulation.SimulateFromSeed function to run the simulation, and performs post-run actions for each seed.
// The execution is deterministic and can be used for fuzz tests as well.
//
// The system under test is isolated for each run but unlike the old runsim command, there is no Process separation.
// This means, global caches may be reused for example. This implementation build upon the vanilla Go stdlib test framework.
func RunWithSeeds[T SimulationApp](
t *testing.T,
appFactory func(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
appOpts servertypes.AppOptions,
baseAppOptions ...func(*baseapp.BaseApp),
) T,
setupStateFactory func(app T) SimStateFactory,
seeds []int64,
fuzzSeed []byte,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
t.Helper()
RunWithSeedsAndRandAcc(t, appFactory, setupStateFactory, seeds, fuzzSeed, simtypes.RandomAccounts, postRunActions...)
}
// RunWithSeedsAndRandAcc calls RunWithSeeds with randAccFn
func RunWithSeedsAndRandAcc[T SimulationApp](
t *testing.T,
appFactory func(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
appOpts servertypes.AppOptions,
baseAppOptions ...func(*baseapp.BaseApp),
) T,
setupStateFactory func(app T) SimStateFactory,
seeds []int64,
fuzzSeed []byte,
randAccFn simtypes.RandomAccountFn,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
t.Helper()
if deprecatedParams := cli.GetDeprecatedFlagUsed(); len(deprecatedParams) != 0 {
fmt.Printf("Warning: Deprecated flag are used: %s", strings.Join(deprecatedParams, ","))
}
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, cfg, appFactory, setupStateFactory, seed, fuzzSeed, postRunActions...)
})
}
}
// RunWithSeed is a helper function that runs a simulation test with the given parameters.
// It iterates over the provided seeds and runs the simulation test for each seed in parallel.
//
// It sets up the environment, creates an instance of the simulation app,
// calls the simulation.SimulateFromSeed function to run the simulation, and performs post-run actions for the seed.
// The execution is deterministic and can be used for fuzz tests as well.
func RunWithSeed[T SimulationApp](
tb testing.TB,
cfg simtypes.Config,
appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T,
setupStateFactory func(app T) SimStateFactory,
seed int64,
fuzzSeed []byte,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
tb.Helper()
RunWithSeedAndRandAcc(tb, cfg, appFactory, setupStateFactory, seed, fuzzSeed, simtypes.RandomAccounts, postRunActions...)
}
// RunWithSeedAndRandAcc calls RunWithSeed with randAccFn
func RunWithSeedAndRandAcc[T SimulationApp](
tb testing.TB,
cfg simtypes.Config,
appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T,
setupStateFactory func(app T) SimStateFactory,
seed int64,
fuzzSeed []byte,
randAccFn simtypes.RandomAccountFn,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
tb.Helper()
// setup environment
tCfg := cfg.With(tb, seed, fuzzSeed)
testInstance := NewSimulationAppInstance(tb, tCfg, appFactory)
var runLogger log.Logger
if cli.FlagVerboseValue {
runLogger = log.NewTestLogger(tb)
} else {
runLogger = log.NewTestLoggerInfo(tb)
}
runLogger = runLogger.With("seed", tCfg.Seed)
app := testInstance.App
stateFactory := setupStateFactory(app)
ops, reporter := prepareWeightedOps(app.SimulationManager(), stateFactory, tCfg, testInstance.App.TxConfig(), runLogger)
simParams, accs, err := simulation.SimulateFromSeedX(
tb,
runLogger,
WriteToDebugLog(runLogger),
app.GetBaseApp(),
stateFactory.AppStateFn,
randAccFn,
ops,
stateFactory.BlockedAddr,
tCfg,
stateFactory.Codec,
testInstance.ExecLogWriter,
)
require.NoError(tb, err)
err = simtestutil.CheckExportSimulation(app, tCfg, simParams)
require.NoError(tb, err)
if tCfg.Commit {
simtestutil.PrintStats(testInstance.DB)
}
// not using tb.Log to always print the summary
fmt.Printf("+++ DONE (seed: %d): \n%s\n", seed, reporter.Summary().String())
for _, step := range postRunActions {
step(tb, testInstance, accs)
}
require.NoError(tb, app.Close())
}
type (
HasWeightedOperationsX interface {
WeightedOperationsX(weight WeightSource, reg Registry)
}
HasWeightedOperationsXWithProposals interface {
WeightedOperationsX(weights WeightSource, reg Registry, proposals WeightedProposalMsgIter,
legacyProposals []simtypes.WeightedProposalContent) //nolint: staticcheck // used for legacy proposal types
}
HasProposalMsgsX interface {
ProposalMsgsX(weights WeightSource, reg Registry)
}
)
type (
HasLegacyWeightedOperations interface {
// WeightedOperations simulation operations (i.e msgs) with their respective weight
WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation
}
// HasLegacyProposalMsgs defines the messages that can be used to simulate governance (v1) proposals
// Deprecated replaced by HasProposalMsgsX
HasLegacyProposalMsgs interface {
// ProposalMsgs msg fu nctions used to simulate governance proposals
ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg
}
// HasLegacyProposalContents defines the contents that can be used to simulate legacy governance (v1beta1) proposals
// Deprecated replaced by HasProposalMsgsX
HasLegacyProposalContents interface {
// ProposalContents content functions used to simulate governance proposals
ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent //nolint:staticcheck // legacy v1beta1 governance
}
)
// TestInstance is a generic type that represents an instance of a SimulationApp used for testing simulations.
// It contains the following fields:
// - App: The instance of the SimulationApp under test.
// - DB: The LevelDB database for the simulation app.
// - WorkDir: The temporary working directory for the simulation app.
// - Cfg: The configuration flags for the simulator.
// - AppLogger: The logger used for logging in the app during the simulation, with seed value attached.
// - ExecLogWriter: Captures block and operation data coming from the simulation
type TestInstance[T SimulationApp] struct {
App T
DB dbm.DB
WorkDir string
Cfg simtypes.Config
AppLogger log.Logger
ExecLogWriter simulation.LogWriter
}
// included to avoid cyclic dependency in testutils/sims
func prepareWeightedOps(
sm *module.SimulationManager,
stateFact SimStateFactory,
config simtypes.Config,
txConfig client.TxConfig,
logger log.Logger,
) (simulation.WeightedOperations, *BasicSimulationReporter) {
cdc := stateFact.Codec
simState := module.SimulationState{
AppParams: make(simtypes.AppParams),
Cdc: cdc,
TxConfig: txConfig,
BondDenom: sdk.DefaultBondDenom,
}
if config.ParamsFile != "" {
bz, err := os.ReadFile(config.ParamsFile)
if err != nil {
panic(err)
}
err = json.Unmarshal(bz, &simState.AppParams)
if err != nil {
panic(err)
}
}
weights := ParamWeightSource(simState.AppParams)
reporter := NewBasicSimulationReporter()
pReg := make(UniqueTypeRegistry)
wContent := make([]simtypes.WeightedProposalContent, 0) //nolint:staticcheck // required for legacy type
legacyPReg := NewWeightedFactoryMethods()
// add gov proposals types
for _, m := range sm.Modules {
switch xm := m.(type) {
case HasProposalMsgsX:
xm.ProposalMsgsX(weights, pReg)
case HasLegacyProposalMsgs:
for _, p := range xm.ProposalMsgs(simState) {
weight := weights.Get(p.AppParamsKey(), safeUint(p.DefaultWeight()))
legacyPReg.Add(weight, legacyToMsgFactoryAdapter(p.MsgSimulatorFn()))
}
case HasLegacyProposalContents:
wContent = append(wContent, xm.ProposalContents(simState)...)
}
}
oReg := NewSimsMsgRegistryAdapter(
reporter,
stateFact.AccountSource,
stateFact.BalanceSource,
txConfig,
logger,
)
wOps := make([]simtypes.WeightedOperation, 0, len(sm.Modules))
for _, m := range sm.Modules {
// add operations
switch xm := m.(type) {
case HasWeightedOperationsX:
xm.WeightedOperationsX(weights, oReg)
case HasWeightedOperationsXWithProposals:
xm.WeightedOperationsX(weights, oReg, AppendIterators(legacyPReg.Iterator(), pReg.Iterator()), wContent)
case HasLegacyWeightedOperations:
wOps = append(wOps, xm.WeightedOperations(simState)...)
}
}
return append(wOps, Collect(oReg.items, func(a weightedOperation) simtypes.WeightedOperation { return a })...), reporter
}
func safeUint(p int) uint32 {
if p < 0 || p > math.MaxUint32 {
panic(fmt.Sprintf("can not cast to uint32: %d", p))
}
return uint32(p)
}
// NewSimulationAppInstance initializes and returns a TestInstance of a SimulationApp.
// The function takes a testing.T instance, a simtypes.Config instance, and an appFactory function as parameters.
// It creates a temporary working directory and a LevelDB database for the simulation app.
// The function then initializes a logger based on the verbosity flag and sets the logger's seed to the test configuration's seed.
// The database is closed and cleaned up on test completion.
func NewSimulationAppInstance[T SimulationApp](
tb testing.TB,
tCfg simtypes.Config,
appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T,
) TestInstance[T] {
tb.Helper()
workDir := tb.TempDir()
require.NoError(tb, os.Mkdir(filepath.Join(workDir, "data"), 0o750))
dbDir := filepath.Join(workDir, "leveldb-app-sim")
var logger log.Logger
if cli.FlagVerboseValue {
logger = log.NewTestLogger(tb)
} else {
logger = log.NewTestLoggerError(tb)
}
logger = logger.With("seed", tCfg.Seed)
db, err := dbm.NewDB("Simulation", dbm.BackendType(tCfg.DBBackend), dbDir)
require.NoError(tb, err)
tb.Cleanup(func() {
_ = db.Close() // ensure db is closed
})
appOptions := make(simtestutil.AppOptionsMap)
appOptions[flags.FlagHome] = workDir
opts := []func(*baseapp.BaseApp){baseapp.SetChainID(tCfg.ChainID)}
if tCfg.FauxMerkle {
opts = append(opts, FauxMerkleModeOpt)
}
app := appFactory(logger, db, nil, true, appOptions, opts...)
if !cli.FlagSigverifyTxValue {
app.SetNotSigverifyTx()
}
return TestInstance[T]{
App: app,
DB: db,
WorkDir: workDir,
Cfg: tCfg,
AppLogger: logger,
ExecLogWriter: &simulation.StandardLogWriter{Seed: tCfg.Seed},
}
}
var _ io.Writer = writerFn(nil)
type writerFn func(p []byte) (n int, err error)
func (w writerFn) Write(p []byte) (n int, err error) {
return w(p)
}
// WriteToDebugLog is an adapter to io.Writer interface
func WriteToDebugLog(logger log.Logger) io.Writer {
return writerFn(func(p []byte) (n int, err error) {
logger.Debug(string(p))
return len(p), nil
})
}
// FauxMerkleModeOpt returns a BaseApp option to use a dbStoreAdapter instead of
// an IAVLStore for faster simulation speed.
func FauxMerkleModeOpt(bapp *baseapp.BaseApp) {
bapp.SetFauxMerkleMode()
}

38
simsx/slices.go Normal file
View File

@ -0,0 +1,38 @@
package simsx
// Collect applies the function f to each element in the source slice,
// returning a new slice containing the results.
//
// The source slice can contain elements of any type T, and the function f
// should take an element of type T as input and return a value of any type E.
//
// Example usage:
//
// source := []int{1, 2, 3, 4, 5}
// double := Collect(source, func(x int) int {
// return x * 2
// })
// // double is now []int{2, 4, 6, 8, 10}
func Collect[T, E any](source []T, f func(a T) E) []E {
r := make([]E, len(source))
for i, v := range source {
r[i] = f(v)
}
return r
}
// First returns the first element in the slice that matches the condition
func First[T any](source []T, f func(a T) bool) *T {
for i := 0; i < len(source); i++ {
if f(source[i]) {
return &source[i]
}
}
return nil
}
// OneOf returns a random element from the given slice using the provided random number generator.
// Panics for empty or nil slice
func OneOf[T any](r interface{ Intn(n int) int }, s []T) T {
return s[r.Intn(len(s))]
}

40
simsx/slices_test.go Normal file
View File

@ -0,0 +1,40 @@
package simsx
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCollect(t *testing.T) {
src := []int{1, 2, 3}
got := Collect(src, func(a int) int { return a * 2 })
assert.Equal(t, []int{2, 4, 6}, got)
gotStrings := Collect(src, strconv.Itoa)
assert.Equal(t, []string{"1", "2", "3"}, gotStrings)
}
func TestFirst(t *testing.T) {
src := []string{"a", "b"}
assert.Equal(t, &src[1], First(src, func(a string) bool { return a == "b" }))
assert.Nil(t, First(src, func(a string) bool { return false }))
}
func TestOneOf(t *testing.T) {
src := []string{"a", "b"}
got := OneOf(randMock{next: 1}, src)
assert.Equal(t, "b", got)
// test with other type
src2 := []int{1, 2, 3}
got2 := OneOf(randMock{next: 2}, src2)
assert.Equal(t, 3, got2)
}
type randMock struct {
next int
}
func (x randMock) Intn(n int) int {
return x.next
}

View File

@ -1,227 +0,0 @@
package sims
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
dbm "github.com/cosmos/cosmos-db"
"github.com/stretchr/testify/require"
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/runtime"
servertypes "github.com/cosmos/cosmos-sdk/server/types"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/cosmos/cosmos-sdk/x/simulation/client/cli"
)
const SimAppChainID = "simulation-app"
// this list of seeds was imported from the original simulation runner: https://github.com/cosmos/tools/blob/v1.0.0/cmd/runsim/main.go#L32
var defaultSeeds = []int64{
1, 2, 4, 7,
32, 123, 124, 582, 1893, 2989,
3012, 4728, 37827, 981928, 87821, 891823782,
989182, 89182391, 11, 22, 44, 77, 99, 2020,
3232, 123123, 124124, 582582, 18931893,
29892989, 30123012, 47284728, 7601778, 8090485,
977367484, 491163361, 424254581, 673398983,
}
type SimStateFactory struct {
Codec codec.Codec
AppStateFn simtypes.AppStateFn
BlockedAddr map[string]bool
}
// SimulationApp abstract app that is used by sims
type SimulationApp interface {
runtime.AppI
SetNotSigverifyTx()
GetBaseApp() *baseapp.BaseApp
TxConfig() client.TxConfig
Close() error
}
// Run is a helper function that runs a simulation test with the given parameters.
// It calls the RunWithSeeds function with the default seeds and parameters.
//
// This is the entrypoint to run simulation tests that used to run with the runsim binary.
func Run[T SimulationApp](
t *testing.T,
appFactory func(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
appOpts servertypes.AppOptions,
baseAppOptions ...func(*baseapp.BaseApp),
) T,
setupStateFactory func(app T) SimStateFactory,
postRunActions ...func(t *testing.T, app TestInstance[T]),
) {
t.Helper()
RunWithSeeds(t, appFactory, setupStateFactory, defaultSeeds, nil, postRunActions...)
}
// RunWithSeeds is a helper function that runs a simulation test with the given parameters.
// It iterates over the provided seeds and runs the simulation test for each seed in parallel.
//
// It sets up the environment, creates an instance of the simulation app,
// calls the simulation.SimulateFromSeed function to run the simulation, and performs post-run actions for each seed.
// The execution is deterministic and can be used for fuzz tests as well.
//
// The system under test is isolated for each run but unlike the old runsim command, there is no Process separation.
// This means, global caches may be reused for example. This implementation build upon the vanialla Go stdlib test framework.
func RunWithSeeds[T SimulationApp](
t *testing.T,
appFactory func(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
appOpts servertypes.AppOptions,
baseAppOptions ...func(*baseapp.BaseApp),
) T,
setupStateFactory func(app T) SimStateFactory,
seeds []int64,
fuzzSeed []byte,
postRunActions ...func(t *testing.T, app TestInstance[T]),
) {
t.Helper()
cfg := cli.NewConfigFromFlags()
cfg.ChainID = SimAppChainID
if deprecatedParams := cli.GetDeprecatedFlagUsed(); len(deprecatedParams) != 0 {
fmt.Printf("Warning: Deprecated flag are used: %s", strings.Join(deprecatedParams, ","))
}
for i := range seeds {
seed := seeds[i]
t.Run(fmt.Sprintf("seed: %d", seed), func(t *testing.T) {
t.Parallel()
// setup environment
tCfg := cfg.With(t, seed, fuzzSeed)
testInstance := NewSimulationAppInstance(t, tCfg, appFactory)
var runLogger log.Logger
if cli.FlagVerboseValue {
runLogger = log.NewTestLogger(t)
} else {
runLogger = log.NewTestLoggerInfo(t)
}
runLogger = runLogger.With("seed", tCfg.Seed)
app := testInstance.App
stateFactory := setupStateFactory(app)
simParams, err := simulation.SimulateFromSeedX(
t,
runLogger,
WriteToDebugLog(runLogger),
app.GetBaseApp(),
stateFactory.AppStateFn,
simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1
simtestutil.BuildSimulationOperations(app, stateFactory.Codec, tCfg, testInstance.App.TxConfig()),
stateFactory.BlockedAddr,
tCfg,
stateFactory.Codec,
testInstance.ExecLogWriter,
)
require.NoError(t, err)
err = simtestutil.CheckExportSimulation(app, tCfg, simParams)
require.NoError(t, err)
if tCfg.Commit {
simtestutil.PrintStats(testInstance.DB)
}
for _, step := range postRunActions {
step(t, testInstance)
}
require.NoError(t, app.Close())
})
}
}
// TestInstance is a generic type that represents an instance of a SimulationApp used for testing simulations.
// It contains the following fields:
// - App: The instance of the SimulationApp under test.
// - DB: The LevelDB database for the simulation app.
// - WorkDir: The temporary working directory for the simulation app.
// - Cfg: The configuration flags for the simulator.
// - AppLogger: The logger used for logging in the app during the simulation, with seed value attached.
// - ExecLogWriter: Captures block and operation data coming from the simulation
type TestInstance[T SimulationApp] struct {
App T
DB dbm.DB
WorkDir string
Cfg simtypes.Config
AppLogger log.Logger
ExecLogWriter simulation.LogWriter
}
// NewSimulationAppInstance initializes and returns a TestInstance of a SimulationApp.
// The function takes a testing.T instance, a simtypes.Config instance, and an appFactory function as parameters.
// It creates a temporary working directory and a LevelDB database for the simulation app.
// The function then initializes a logger based on the verbosity flag and sets the logger's seed to the test configuration's seed.
// The database is closed and cleaned up on test completion.
func NewSimulationAppInstance[T SimulationApp](
t *testing.T,
tCfg simtypes.Config,
appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T,
) TestInstance[T] {
t.Helper()
workDir := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(workDir, "data"), 0o755))
dbDir := filepath.Join(workDir, "leveldb-app-sim")
var logger log.Logger
if cli.FlagVerboseValue {
logger = log.NewTestLogger(t)
} else {
logger = log.NewTestLoggerError(t)
}
logger = logger.With("seed", tCfg.Seed)
db, err := dbm.NewDB("Simulation", dbm.BackendType(tCfg.DBBackend), dbDir)
require.NoError(t, err)
t.Cleanup(func() {
_ = db.Close() // ensure db is closed
})
appOptions := make(simtestutil.AppOptionsMap)
appOptions[flags.FlagHome] = workDir
app := appFactory(logger, db, nil, true, appOptions, baseapp.SetChainID(SimAppChainID))
if !cli.FlagSigverifyTxValue {
app.SetNotSigverifyTx()
}
return TestInstance[T]{
App: app,
DB: db,
WorkDir: workDir,
Cfg: tCfg,
AppLogger: logger,
ExecLogWriter: &simulation.StandardLogWriter{Seed: tCfg.Seed},
}
}
var _ io.Writer = writerFn(nil)
type writerFn func(p []byte) (n int, err error)
func (w writerFn) Write(p []byte) (n int, err error) {
return w(p)
}
// WriteToDebugLog is an adapter to io.Writer interface
func WriteToDebugLog(logger log.Logger) io.Writer {
return writerFn(func(p []byte) (n int, err error) {
logger.Debug(string(p))
return len(p), nil
})
}

View File

@ -1,7 +1,7 @@
package simulation
import (
"fmt"
"errors"
"math/rand"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
@ -14,10 +14,11 @@ import (
// eventually more useful data can be placed in here.
// (e.g. number of coins)
type Account struct {
PrivKey cryptotypes.PrivKey
PubKey cryptotypes.PubKey
Address sdk.AccAddress
ConsKey cryptotypes.PrivKey
PrivKey cryptotypes.PrivKey
PubKey cryptotypes.PubKey
Address sdk.AccAddress
ConsKey cryptotypes.PrivKey
AddressBech32 string
}
// Equals returns true if two accounts are equal
@ -50,10 +51,11 @@ func RandomAccounts(r *rand.Rand, n int) []Account {
}
idx[string(addr.Bytes())] = struct{}{}
accs[i] = Account{
Address: addr,
PrivKey: privKey,
PubKey: pubKey,
ConsKey: ed25519.GenPrivKeyFromSecret(privkeySeed),
Address: addr,
PrivKey: privKey,
PubKey: pubKey,
ConsKey: ed25519.GenPrivKeyFromSecret(privkeySeed),
AddressBech32: addr.String(),
}
i++
}
@ -75,7 +77,7 @@ func FindAccount(accs []Account, address sdk.Address) (Account, bool) {
// RandomFees returns a random fee by selecting a random coin denomination and
// amount from the account's available balance. If the user doesn't have enough
// funds for paying fees, it returns empty coins.
func RandomFees(r *rand.Rand, ctx sdk.Context, spendableCoins sdk.Coins) (sdk.Coins, error) {
func RandomFees(r *rand.Rand, _ sdk.Context, spendableCoins sdk.Coins) (sdk.Coins, error) {
if spendableCoins.Empty() {
return nil, nil
}
@ -90,7 +92,7 @@ func RandomFees(r *rand.Rand, ctx sdk.Context, spendableCoins sdk.Coins) (sdk.Co
}
if randCoin.Amount.IsZero() {
return nil, fmt.Errorf("no coins found for random fees")
return nil, errors.New("no coins found for random fees")
}
amt, err := RandPositiveInt(r, randCoin.Amount)

View File

@ -21,6 +21,7 @@ import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/simsx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
@ -172,6 +173,7 @@ func (AppModule) GenerateGenesisState(simState *module.SimulationState) {
}
// ProposalMsgs returns msgs used for governance proposals for simulations.
// migrate to ProposalMsgsX. This method is ignored when ProposalMsgsX exists and will be removed in the future.
func (AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg {
return simulation.ProposalMsgs()
}
@ -181,13 +183,25 @@ func (am AppModule) RegisterStoreDecoder(sdr simtypes.StoreDecoderRegistry) {
sdr[types.StoreKey] = simtypes.NewStoreDecoderFuncFromCollectionsSchema(am.keeper.(keeper.BaseKeeper).Schema)
}
// WeightedOperations returns the all the gov module operations with their respective weights.
// WeightedOperations returns the all the bank module operations with their respective weights.
// migrate to WeightedOperationsX. This method is ignored when WeightedOperationsX exists and will be removed in the future
func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation {
return simulation.WeightedOperations(
simState.AppParams, simState.Cdc, simState.TxConfig, am.accountKeeper, am.keeper,
)
}
// ProposalMsgsX registers governance proposal messages in the simulation registry.
func (AppModule) ProposalMsgsX(weights simsx.WeightSource, reg simsx.Registry) {
reg.Add(weights.Get("msg_update_params", 100), simulation.MsgUpdateParamsFactory())
}
// WeightedOperationsX registers weighted bank module operations for simulation.
func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Registry) {
reg.Add(weights.Get("msg_send", 100), simulation.MsgSendFactory())
reg.Add(weights.Get("msg_multisend", 10), simulation.MsgMultiSendFactory())
}
// App Wiring Setup
func init() {

View File

@ -0,0 +1,86 @@
package simulation
import (
"context"
"slices"
"github.com/cosmos/cosmos-sdk/simsx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank/types"
)
func MsgSendFactory() simsx.SimMsgFactoryFn[*types.MsgSend] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgSend) {
from := testData.AnyAccount(reporter, simsx.WithSpendableBalance())
to := testData.AnyAccount(reporter, simsx.ExcludeAccounts(from))
coins := from.LiquidBalance().RandSubsetCoins(reporter, simsx.WithSendEnabledCoins())
return []simsx.SimAccount{from}, types.NewMsgSend(from.Address, to.Address, coins)
}
}
func MsgMultiSendFactory() simsx.SimMsgFactoryFn[*types.MsgMultiSend] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgMultiSend) {
r := testData.Rand()
var (
sending = make([]types.Input, 1)
receiving = make([]types.Output, r.Intn(3)+1)
senderAcc = make([]simsx.SimAccount, len(sending))
totalSentCoins sdk.Coins
uniqueAccountsFilter = simsx.UniqueAccounts()
)
for i := range sending {
// generate random input fields, ignore to address
from := testData.AnyAccount(reporter, simsx.WithSpendableBalance(), uniqueAccountsFilter)
if reporter.IsSkipped() {
return nil, nil
}
coins := from.LiquidBalance().RandSubsetCoins(reporter, simsx.WithSendEnabledCoins())
// set signer privkey
senderAcc[i] = from
// set next input and accumulate total sent coins
sending[i] = types.NewInput(from.Address, coins)
totalSentCoins = totalSentCoins.Add(coins...)
}
for i := range receiving {
receiver := testData.AnyAccount(reporter)
if reporter.IsSkipped() {
return nil, nil
}
var outCoins sdk.Coins
// split total sent coins into random subsets for output
if i == len(receiving)-1 {
// last one receives remaining amount
outCoins = totalSentCoins
} else {
// take random subset of remaining coins for output
// and update remaining coins
outCoins = r.SubsetCoins(totalSentCoins)
totalSentCoins = totalSentCoins.Sub(outCoins...)
}
receiving[i] = types.NewOutput(receiver.Address, outCoins)
}
// remove any entries that have no coins
receiving = slices.DeleteFunc(receiving, func(o types.Output) bool {
return o.Address == "" || o.Coins.Empty()
})
return senderAcc, &types.MsgMultiSend{Inputs: sending, Outputs: receiving}
}
}
// MsgUpdateParamsFactory creates a gov proposal for param updates
func MsgUpdateParamsFactory() simsx.SimMsgFactoryFn[*types.MsgUpdateParams] {
return func(_ context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgUpdateParams) {
params := types.DefaultParams()
params.DefaultSendEnabled = testData.Rand().Intn(2) == 0
return nil, &types.MsgUpdateParams{
Authority: testData.ModuleAccountAddress(reporter, "gov"),
Params: params,
}
}
}

View File

@ -25,6 +25,7 @@ const (
)
// WeightedOperations returns all the operations from the module with their respective weights
// migrate to the msg factories instead, this method will be removed in the future
func WeightedOperations(
appParams simtypes.AppParams,
cdc codec.JSONCodec,
@ -55,6 +56,7 @@ func WeightedOperations(
// SimulateMsgSend tests and runs a single msg send where both
// accounts already exist.
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgSend(
txGen client.TxConfig,
ak types.AccountKeeper,
@ -94,6 +96,7 @@ func SimulateMsgSend(
// SimulateMsgSendToModuleAccount tests and runs a single msg send where both
// accounts already exist.
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgSendToModuleAccount(
txGen client.TxConfig,
ak types.AccountKeeper,
@ -183,6 +186,7 @@ func sendMsgSend(
// SimulateMsgMultiSend tests and runs a single msg multisend, with randomized, capped number of inputs/outputs.
// all accounts in msg fields exist in state
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgMultiSend(txGen client.TxConfig, ak types.AccountKeeper, bk keeper.Keeper) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
@ -273,6 +277,7 @@ func SimulateMsgMultiSend(txGen client.TxConfig, ak types.AccountKeeper, bk keep
}
// SimulateMsgMultiSendToModuleAccount sends coins to Module Accounts
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgMultiSendToModuleAccount(
txGen client.TxConfig,
ak types.AccountKeeper,

View File

@ -18,6 +18,7 @@ const (
)
// ProposalMsgs defines the module weighted proposals' contents
// Deprecated: migrate to MsgUpdateParamsFactory instead
func ProposalMsgs() []simtypes.WeightedProposalMsg {
return []simtypes.WeightedProposalMsg{
simulation.NewWeightedProposalMsg(

View File

@ -70,7 +70,7 @@ func SimulateFromSeed(
) (stopEarly bool, exportedParams Params, err error) {
tb.Helper()
mode, _, _ := getTestingMode(tb)
expParams, err := SimulateFromSeedX(tb, log.NewTestLogger(tb), w, app, appStateFn, randAccFn, ops, blockedAddrs, config, cdc, NewLogWriter(mode))
expParams, _, err := SimulateFromSeedX(tb, log.NewTestLogger(tb), w, app, appStateFn, randAccFn, ops, blockedAddrs, config, cdc, NewLogWriter(mode))
return false, expParams, err
}
@ -88,7 +88,7 @@ func SimulateFromSeedX(
config simulation.Config,
cdc codec.JSONCodec,
logWriter LogWriter,
) (exportedParams Params, err error) {
) (exportedParams Params, accs []simulation.Account, err error) {
tb.Helper()
// in case we have to end early, don't os.Exit so that we can run cleanup code.
testingMode, _, b := getTestingMode(tb)
@ -101,7 +101,7 @@ func SimulateFromSeedX(
logger.Debug("Randomized simulation setup", "params", mustMarshalJSONIndent(params))
timeDiff := maxTimePerBlock - minTimePerBlock
accs := randAccFn(r, params.NumKeys())
accs = randAccFn(r, params.NumKeys())
eventStats := NewEventStats()
// Second variable to keep pending validator set (delayed one block since
@ -110,7 +110,7 @@ func SimulateFromSeedX(
// At least 2 accounts must be added here, otherwise when executing SimulateMsgSend
// two accounts will be selected to meet the conditions from != to and it will fall into an infinite loop.
if len(accs) <= 1 {
return params, fmt.Errorf("at least two genesis accounts are required")
return params, accs, fmt.Errorf("at least two genesis accounts are required")
}
config.ChainID = chainID
@ -128,7 +128,7 @@ func SimulateFromSeedX(
nextValidators := validators
if len(nextValidators) == 0 {
tb.Skip("skipping: empty validator set in genesis")
return params, nil
return params, accs, nil
}
var (
@ -196,7 +196,7 @@ func SimulateFromSeedX(
res, err := app.FinalizeBlock(finalizeBlockReq)
if err != nil {
return params, fmt.Errorf("block finalization failed at height %d: %w", blockHeight, err)
return params, accs, fmt.Errorf("block finalization failed at height %d: %w", blockHeight, err)
}
ctx := app.NewContextLegacy(false, cmtproto.Header{
@ -245,7 +245,7 @@ func SimulateFromSeedX(
if config.Commit {
app.SimWriteState()
if _, err := app.Commit(); err != nil {
return params, fmt.Errorf("commit failed at height %d: %w", blockHeight, err)
return params, accs, fmt.Errorf("commit failed at height %d: %w", blockHeight, err)
}
}
@ -264,7 +264,7 @@ func SimulateFromSeedX(
nextValidators = updateValidators(tb, r, params, validators, res.ValidatorUpdates, eventStats.Tally)
if len(nextValidators) == 0 {
tb.Skip("skipping: empty validator set")
return params, nil
return params, accs, nil
}
// update the exported params
@ -282,7 +282,7 @@ func SimulateFromSeedX(
} else {
eventStats.Print(w)
}
return exportedParams, err
return exportedParams, accs, err
}
type blockSimFn func(