feat(sims): Integration with app v2 (#23013)

Co-authored-by: Alex | Interchain Labs <alex@skip.money>
This commit is contained in:
Alexander Peters 2025-01-09 09:19:35 +01:00 committed by GitHub
parent 39a55ed045
commit cf721a6540
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1217 additions and 48 deletions

View File

@ -41,7 +41,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
Every module contains its own CHANGELOG.md. Please refer to the module you are interested in.
### Features
* (sims) [#23013](https://github.com/cosmos/cosmos-sdk/pull/23013) Integration with app v2
* (baseapp) [#20291](https://github.com/cosmos/cosmos-sdk/pull/20291) Simulate nested messages.
* (client/keys) [#21829](https://github.com/cosmos/cosmos-sdk/pull/21829) Add support for importing hex key using standard input.
* (x/auth/ante) [#23128](https://github.com/cosmos/cosmos-sdk/pull/23128) Allow custom verifyIsOnCurve when validate tx for public key like ethsecp256k1.

View File

@ -106,3 +106,8 @@ func (a *App[T]) SchemaDecoderResolver() decoding.DecoderResolver {
func (a *App[T]) Close() error {
return nil
}
// GetApp return self
func (a *App[T]) GetApp() *App[T] {
return a
}

View File

@ -47,10 +47,16 @@ test-sim-multi-seed-long:
# @cd ${CURRENT_DIR}/simapp && go test -failfast -mod=readonly -timeout=2h -tags='sims' -run TestFullAppSimulation \
# -NumBlocks=150 -Period=50
test-sim-multi-seed-short:
test-sim-multi-seed-short: test-v2-sim
# @echo "Running short multi-seed application simulation. This may take awhile!"
# @cd ${CURRENT_DIR}/simapp && go test -failfast -mod=readonly -timeout 30m -tags='sims' -run TestFullAppSimulation \
# -NumBlocks=50 -Period=10 -FauxMerkle=true
# -NumBlocks=50 -Period=10 -FauxMerkle=true
.Phony: test-v2-sim
test-v2-sim:
@echo "Running short multi-seed application simulation. This may take awhile!"
@cd ${CURRENT_DIR}/simapp/v2 && go test -failfast -mod=readonly -timeout 30m -tags='sims' -run TestSimsAppV2 \
# -NumBlocks=50 -Period=10 -FauxMerkle=true
test-sim-benchmark-invariants:
# @echo "Running simulation invariant benchmarks..."

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"iter"
"cosmossdk.io/core/server"
corestore "cosmossdk.io/core/store"
@ -16,6 +17,8 @@ import (
// It is responsible for interacting with stf and store.
// Runtime/v2 is an extension of this interface.
type AppManager[T transaction.Tx] interface {
TransactionFuzzer[T]
// InitGenesis initializes the genesis state of the application.
InitGenesis(
ctx context.Context,
@ -55,6 +58,17 @@ type AppManager[T transaction.Tx] interface {
QueryWithState(ctx context.Context, state corestore.ReaderMap, request transaction.Msg) (transaction.Msg, error)
}
// TransactionFuzzer defines an interface for processing simulated transactions and generating responses with state changes.
type TransactionFuzzer[T transaction.Tx] interface {
// DeliverSims processes simulated transactions for a block and generates a response with potential state changes.
// The simsBuilder generates simulated transactions.
DeliverSims(
ctx context.Context,
block *server.BlockRequest[T],
simsBuilder func(ctx context.Context) iter.Seq[T],
) (*server.BlockResponse, corestore.WriterMap, error)
}
// Store defines the underlying storage behavior needed by AppManager.
type Store interface {
// StateLatest returns a readonly view over the latest
@ -187,6 +201,29 @@ func (a appManager[T]) DeliverBlock(
return blockResponse, newState, nil
}
// DeliverSims same as DeliverBlock for sims only.
func (a appManager[T]) DeliverSims(
ctx context.Context,
block *server.BlockRequest[T],
simsBuilder func(ctx context.Context) iter.Seq[T],
) (*server.BlockResponse, corestore.WriterMap, error) {
latestVersion, currentState, err := a.db.StateLatest()
if err != nil {
return nil, nil, fmt.Errorf("unable to create new state for height %d: %w", block.Height, err)
}
if latestVersion+1 != block.Height {
return nil, nil, fmt.Errorf("invalid DeliverSims height wanted %d, got %d", latestVersion+1, block.Height)
}
blockResponse, newState, err := a.stf.DeliverSims(ctx, block, currentState, simsBuilder)
if err != nil {
return nil, nil, fmt.Errorf("sims delivery failed: %w", err)
}
return blockResponse, newState, nil
}
// ValidateTx will validate the tx against the latest storage state. This means that
// only the stateful validation will be run, not the execution portion of the tx.
// If full execution is needed, Simulate must be used.

View File

@ -2,6 +2,7 @@ package appmanager
import (
"context"
"iter"
"cosmossdk.io/core/server"
"cosmossdk.io/core/store"
@ -40,4 +41,12 @@ type StateTransitionFunction[T transaction.Tx] interface {
gasLimit uint64,
req transaction.Msg,
) (transaction.Msg, error)
// DeliverSims provides an interface for state transitions by sims.
DeliverSims(
ctx context.Context,
block *server.BlockRequest[T],
state store.ReaderMap,
simsBuilder func(ctx context.Context) iter.Seq[T],
) (blockResult *server.BlockResponse, newState store.WriterMap, err error)
}

View File

@ -0,0 +1,34 @@
package stf
import (
"context"
"iter"
"cosmossdk.io/core/header"
"cosmossdk.io/core/server"
"cosmossdk.io/core/store"
"cosmossdk.io/core/transaction"
)
// doSimsTXs constructs a function to simulate transactions in a block execution context using the provided simsBuilder.
func (s STF[T]) doSimsTXs(simsBuilder func(ctx context.Context) iter.Seq[T]) doInBlockDeliveryFn[T] {
return func(
exCtx context.Context,
_ []T,
newState store.WriterMap,
headerInfo header.Info,
) ([]server.TxResult, error) {
const key = "sims.header.time"
simsCtx := context.WithValue(exCtx, key, headerInfo.Time) //nolint: staticcheck // using string key to decouple
var results []server.TxResult
var i int32
for tx := range simsBuilder(simsCtx) {
if err := isCtxCancelled(simsCtx); err != nil {
return nil, err
}
results = append(results, s.deliverTx(simsCtx, newState, tx, transaction.ExecModeFinalize, headerInfo, i+1))
i++
}
return results, nil
}
}

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"iter"
"strings"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
@ -84,6 +85,16 @@ func New[T transaction.Tx](
}, nil
}
// DeliverSims entrypoint to processes sims transactions similar to DeliverBlock.
func (s STF[T]) DeliverSims(
ctx context.Context,
block *server.BlockRequest[T],
state store.ReaderMap,
simsBuilder func(ctx context.Context) iter.Seq[T],
) (blockResult *server.BlockResponse, newState store.WriterMap, err error) {
return s.deliverBlock(ctx, block, state, s.doSimsTXs(simsBuilder))
}
// DeliverBlock is our state transition function.
// It takes a read only view of the state to apply the block to,
// executes the block and returns the block results and the new state.
@ -91,6 +102,23 @@ func (s STF[T]) DeliverBlock(
ctx context.Context,
block *server.BlockRequest[T],
state store.ReaderMap,
) (blockResult *server.BlockResponse, newState store.WriterMap, err error) {
return s.deliverBlock(ctx, block, state, s.doDeliverTXs)
}
// common code path for DeliverSims and DeliverBlock
type doInBlockDeliveryFn[T transaction.Tx] func(
ctx context.Context,
txs []T,
newState store.WriterMap,
hi header.Info,
) ([]server.TxResult, error)
func (s STF[T]) deliverBlock(
ctx context.Context,
block *server.BlockRequest[T],
state store.ReaderMap,
doInBlockDelivery doInBlockDeliveryFn[T],
) (blockResult *server.BlockResponse, newState store.WriterMap, err error) {
// creates a new branchFn state, from the readonly view of the state
// that can be written to.
@ -141,14 +169,9 @@ func (s STF[T]) DeliverBlock(
}
// execute txs
txResults := make([]server.TxResult, len(block.Txs))
// TODO: skip first tx if vote extensions are enabled (marko)
for i, txBytes := range block.Txs {
// check if we need to return early or continue delivering txs
if err = isCtxCancelled(ctx); err != nil {
return nil, nil, err
}
txResults[i] = s.deliverTx(exCtx, newState, txBytes, transaction.ExecModeFinalize, hi, int32(i+1))
txResults, err := doInBlockDelivery(exCtx, block.Txs, newState, hi)
if err != nil {
return nil, nil, err
}
// reset events
exCtx.events = make([]event.Event, 0)
@ -167,6 +190,25 @@ func (s STF[T]) DeliverBlock(
}, newState, nil
}
func (s STF[T]) doDeliverTXs(
exCtx context.Context,
txs []T,
newState store.WriterMap,
hi header.Info,
) ([]server.TxResult, error) {
// execute txs
txResults := make([]server.TxResult, len(txs))
// TODO: skip first tx if vote extensions are enabled (marko)
for i, txBytes := range txs {
// check if we need to return early or continue delivering txs
if err := isCtxCancelled(exCtx); err != nil {
return nil, err
}
txResults[i] = s.deliverTx(exCtx, newState, txBytes, transaction.ExecModeFinalize, hi, int32(i+1))
}
return txResults, nil
}
// deliverTx executes a TX and returns the result.
func (s STF[T]) deliverTx(
ctx context.Context,

View File

@ -6,8 +6,9 @@ import (
"os"
"path/filepath"
"cosmossdk.io/server/v2/streaming"
"github.com/hashicorp/go-plugin"
"cosmossdk.io/server/v2/streaming"
)
// FilePlugin is the implementation of the baseapp.ABCIListener interface

View File

@ -50,6 +50,11 @@ require (
require github.com/cosmos/iavl/v2 v2.0.0-alpha.4 // indirect
require (
cosmossdk.io/server/v2/appmanager v1.0.0-beta.1
github.com/cometbft/cometbft/api v1.0.0
)
require (
buf.build/gen/go/cometbft/cometbft/protocolbuffers/go v1.36.1-20241120201313-68e42a58b301.1 // indirect
buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.36.1-20240130113600-88ef6483f90f.1 // indirect
@ -64,7 +69,6 @@ require (
cosmossdk.io/errors v1.0.1 // indirect
cosmossdk.io/errors/v2 v2.0.0 // indirect
cosmossdk.io/schema v1.0.0 // indirect
cosmossdk.io/server/v2/appmanager v1.0.0-beta.1 // indirect
cosmossdk.io/server/v2/stf v1.0.0-beta.1 // indirect
cosmossdk.io/store v1.10.0-rc.1.0.20241218084712-ca559989da43 // indirect
cosmossdk.io/x/tx v1.0.0 // indirect
@ -96,7 +100,6 @@ require (
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/cometbft/cometbft-db v1.0.1 // indirect
github.com/cometbft/cometbft/api v1.0.0 // indirect
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-db v1.1.1 // indirect
github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect

492
simapp/v2/sim_runner.go Normal file
View File

@ -0,0 +1,492 @@
package simapp
import (
"context"
"encoding/json"
"fmt"
"iter"
"maps"
"math/rand"
"slices"
"testing"
"time"
cmtproto "github.com/cometbft/cometbft/api/cometbft/types/v1"
cmttypes "github.com/cometbft/cometbft/types"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"cosmossdk.io/core/appmodule"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
"cosmossdk.io/core/comet"
corecontext "cosmossdk.io/core/context"
"cosmossdk.io/core/server"
"cosmossdk.io/core/store"
"cosmossdk.io/core/transaction"
"cosmossdk.io/depinject"
"cosmossdk.io/log"
"cosmossdk.io/runtime/v2"
"cosmossdk.io/server/v2/appmanager"
cometbfttypes "cosmossdk.io/server/v2/cometbft/types"
storev2 "cosmossdk.io/store/v2"
consensustypes "cosmossdk.io/x/consensus/types"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simsx"
simsxv2 "github.com/cosmos/cosmos-sdk/simsx/v2"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/cosmos/cosmos-sdk/x/simulation/client/cli"
)
type Tx = transaction.Tx
type (
HasWeightedOperationsX = simsx.HasWeightedOperationsX
HasWeightedOperationsXWithProposals = simsx.HasWeightedOperationsXWithProposals
HasProposalMsgsX = simsx.HasProposalMsgsX
)
const SimAppChainID = "simulation-app"
// this list of seeds was imported from the original simulation runner: https://github.com/cosmos/tools/blob/v1.0.0/cmd/runsim/main.go#L32
var defaultSeeds = []int64{
1, 2, 4, 7,
32, 123, 124, 582, 1893, 2989,
3012, 4728, 37827, 981928, 87821, 891823782,
989182, 89182391, 11, 22, 44, 77, 99, 2020,
3232, 123123, 124124, 582582, 18931893,
29892989, 30123012, 47284728, 7601778, 8090485,
977367484, 491163361, 424254581, 673398983,
}
const (
maxTimePerBlock = 10_000 * time.Second
minTimePerBlock = maxTimePerBlock / 2
timeRangePerBlock = maxTimePerBlock - minTimePerBlock
)
type (
AuthKeeper interface {
simsx.ModuleAccountSource
simsx.AccountSource
}
BankKeeper interface {
simsx.BalanceSource
GetBlockedAddresses() map[string]bool
}
StakingKeeper interface {
UnbondingTime(ctx context.Context) (time.Duration, error)
}
ModuleManager interface {
Modules() map[string]appmodulev2.AppModule
}
// SimulationApp abstract blockchain app
SimulationApp[T Tx] interface {
GetApp() *runtime.App[T]
TxConfig() client.TxConfig
AppCodec() codec.Codec
DefaultGenesis() map[string]json.RawMessage
Store() storev2.RootStore
Close() error
}
// TestInstance system under test
TestInstance[T Tx] struct {
App SimulationApp[T]
TxDecoder transaction.Codec[T]
BankKeeper BankKeeper
AuthKeeper AuthKeeper
StakingKeeper StakingKeeper
TXBuilder simsxv2.TXBuilder[T]
AppManager appmanager.AppManager[T]
ModuleManager ModuleManager
}
AppFactory[T Tx, V SimulationApp[T]] func(config depinject.Config, outputs ...any) (V, error)
)
// SetupTestInstance initializes and returns the system under test.
func SetupTestInstance[T Tx, V SimulationApp[T]](t *testing.T, factory AppFactory[T, V], appConfig depinject.Config) TestInstance[T] {
t.Helper()
vp := viper.New()
vp.Set("store.app-db-backend", "memdb")
vp.Set("home", t.TempDir())
depInjCfg := depinject.Configs(
depinject.Supply(log.NewNopLogger(), runtime.GlobalConfig(vp.AllSettings())),
appConfig,
)
var (
bankKeeper BankKeeper
authKeeper AuthKeeper
stKeeper StakingKeeper
)
err := depinject.Inject(depInjCfg,
&authKeeper,
&bankKeeper,
&stKeeper,
)
require.NoError(t, err)
xapp, err := factory(depinject.Configs(depinject.Supply(log.NewNopLogger(), runtime.GlobalConfig(vp.AllSettings()))))
require.NoError(t, err)
return TestInstance[T]{
App: xapp,
BankKeeper: bankKeeper,
AuthKeeper: authKeeper,
StakingKeeper: stKeeper,
AppManager: xapp.GetApp(),
ModuleManager: xapp.GetApp().ModuleManager(),
TxDecoder: simsxv2.NewGenericTxDecoder[T](xapp.TxConfig()),
TXBuilder: simsxv2.NewSDKTXBuilder[T](xapp.TxConfig(), simsxv2.DefaultGenTxGas),
}
}
// RunWithSeeds runs a series of subtests using the default set of random seeds for deterministic simulation testing.
func RunWithSeeds[T Tx](
t *testing.T,
seeds []int64,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
t.Helper()
cfg := cli.NewConfigFromFlags()
cfg.ChainID = SimAppChainID
for i := range seeds {
seed := seeds[i]
t.Run(fmt.Sprintf("seed: %d", seed), func(t *testing.T) {
t.Parallel()
RunWithSeed(t, NewSimApp[T], AppConfig(), cfg, seed, postRunActions...)
})
}
}
// RunWithSeed initializes and executes a simulation run with the given seed, generating blocks and transactions.
func RunWithSeed[T Tx, V SimulationApp[T]](
t *testing.T,
appFactory AppFactory[T, V],
appConfig depinject.Config,
tCfg simtypes.Config,
seed int64,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
t.Helper()
r := rand.New(rand.NewSource(seed))
testInstance := SetupTestInstance[T, V](t, appFactory, appConfig)
accounts, genesisAppState, chainID, genesisTimestamp := prepareInitialGenesisState(testInstance.App, r, testInstance.BankKeeper, tCfg, testInstance.ModuleManager)
appManager := testInstance.AppManager
appStore := testInstance.App.Store()
txConfig := testInstance.App.TxConfig()
rootCtx, done := context.WithCancel(context.Background())
defer done()
initRsp, stateRoot := doChainInitWithGenesis(t, rootCtx, chainID, genesisTimestamp, appManager, testInstance.TxDecoder, genesisAppState, appStore)
activeValidatorSet := simsxv2.NewValSet().Update(initRsp.ValidatorUpdates)
valsetHistory := simsxv2.NewValSetHistory(1)
valsetHistory.Add(genesisTimestamp, activeValidatorSet)
emptySimParams := make(map[string]json.RawMessage) // todo read sims params from disk as before
modules := testInstance.ModuleManager.Modules()
msgFactoriesFn := prepareSimsMsgFactories(r, modules, simsx.ParamWeightSource(emptySimParams))
cs := chainState[T]{
chainID: chainID,
blockTime: genesisTimestamp,
activeValidatorSet: activeValidatorSet,
valsetHistory: valsetHistory,
stateRoot: stateRoot,
app: appManager,
appStore: appStore,
txConfig: txConfig,
}
doMainLoop(
t,
rootCtx,
cs,
msgFactoriesFn,
r,
testInstance.AuthKeeper,
testInstance.BankKeeper,
accounts,
testInstance.TXBuilder,
testInstance.StakingKeeper,
)
require.NoError(t, testInstance.App.Close(), "closing app")
for _, step := range postRunActions {
step(t, testInstance, accounts)
}
}
// prepareInitialGenesisState initializes the genesis state for simulation by generating accounts, app state, chain ID, and timestamp.
// It uses a random seed, configuration parameters, and module manager to customize the state.
// Blocked accounts are removed from the simulation accounts list based on the bank keeper's configuration.
func prepareInitialGenesisState[T Tx](
app SimulationApp[T],
r *rand.Rand,
bankKeeper BankKeeper,
tCfg simtypes.Config,
moduleManager ModuleManager,
) ([]simtypes.Account, json.RawMessage, string, time.Time) {
txConfig := app.TxConfig()
// todo: replace legacy testdata functions ?
appStateFn := simtestutil.AppStateFn(
app.AppCodec(),
txConfig.SigningContext().AddressCodec(),
txConfig.SigningContext().ValidatorAddressCodec(),
toLegacySimsModule(moduleManager.Modules()),
app.DefaultGenesis(),
)
params := simulation.RandomParams(r)
accounts := slices.DeleteFunc(simtypes.RandomAccounts(r, params.NumKeys()),
func(acc simtypes.Account) bool { // remove blocked accounts
return bankKeeper.GetBlockedAddresses()[acc.AddressBech32]
})
appState, accounts, chainID, genesisTimestamp := appStateFn(r, accounts, tCfg)
return accounts, appState, chainID, genesisTimestamp
}
// doChainInitWithGenesis initializes the blockchain state with the provided genesis data and returns the initial block response and state root.
func doChainInitWithGenesis[T Tx](
t *testing.T,
ctx context.Context,
chainID string,
genesisTimestamp time.Time,
app appmanager.AppManager[T],
txDecoder transaction.Codec[T],
genesisAppState json.RawMessage,
appStore cometbfttypes.Store,
) (*server.BlockResponse, store.Hash) {
t.Helper()
genesisReq := &server.BlockRequest[T]{
Height: 0,
Time: genesisTimestamp,
Hash: make([]byte, 32),
ChainId: chainID,
AppHash: make([]byte, 32),
IsGenesis: true,
}
initialConsensusParams := &consensustypes.MsgUpdateParams{
Block: &cmtproto.BlockParams{
MaxBytes: 200000,
MaxGas: 100_000_000,
},
Evidence: &cmtproto.EvidenceParams{
MaxAgeNumBlocks: 302400,
MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration
MaxBytes: 10000,
},
Validator: &cmtproto.ValidatorParams{PubKeyTypes: []string{cmttypes.ABCIPubKeyTypeEd25519, cmttypes.ABCIPubKeyTypeSecp256k1}},
}
genesisCtx := context.WithValue(ctx, corecontext.CometParamsInitInfoKey, initialConsensusParams)
initRsp, genesisStateChanges, err := app.InitGenesis(genesisCtx, genesisReq, genesisAppState, txDecoder)
require.NoError(t, err)
require.NoError(t, appStore.SetInitialVersion(0))
changeSet, err := genesisStateChanges.GetStateChanges()
require.NoError(t, err)
stateRoot, err := appStore.Commit(&store.Changeset{Changes: changeSet})
require.NoError(t, err)
return initRsp, stateRoot
}
// chainState represents the state of a blockchain during a simulation run.
type chainState[T Tx] struct {
chainID string
blockTime time.Time
activeValidatorSet simsxv2.WeightedValidators
valsetHistory *simsxv2.ValSetHistory
stateRoot store.Hash
app appmanager.TransactionFuzzer[T]
appStore storev2.RootStore
txConfig client.TxConfig
}
// doMainLoop executes the main simulation loop after chain setup with genesis block.
// Based on the initial seed and configurations, a deterministic set of messages is generated
// and executed. Events like validators missing votes or double signing are included in this
// process. The runtime tracks the validator's state and history.
func doMainLoop[T Tx](
t *testing.T,
rootCtx context.Context,
cs chainState[T],
nextMsgFactory func() simsx.SimMsgFactoryX,
r *rand.Rand,
authKeeper AuthKeeper,
bankKeeper simsx.BalanceSource,
accounts []simtypes.Account,
txBuilder simsxv2.TXBuilder[T],
stakingKeeper StakingKeeper,
) {
t.Helper()
blockTime := cs.blockTime
activeValidatorSet := cs.activeValidatorSet
if len(activeValidatorSet) == 0 {
t.Fatal("no active validators in chain setup")
return
}
valsetHistory := cs.valsetHistory
stateRoot := cs.stateRoot
chainID := cs.chainID
app := cs.app
appStore := cs.appStore
const ( // todo: read from CLI instead
numBlocks = 100 // 500 default
maxTXPerBlock = 200 // 200 default
)
var (
txSkippedCounter int
txTotalCounter int
)
rootReporter := simsx.NewBasicSimulationReporter()
futureOpsReg := simsxv2.NewFutureOpsRegistry()
for i := 0; i < numBlocks; i++ {
if len(activeValidatorSet) == 0 {
t.Skipf("run out of validators in block: %d\n", i+1)
return
}
blockTime = blockTime.Add(minTimePerBlock)
blockTime = blockTime.Add(time.Duration(int64(r.Intn(int(timeRangePerBlock/time.Second)))) * time.Second)
valsetHistory.Add(blockTime, activeValidatorSet)
blockReqN := &server.BlockRequest[T]{
Height: uint64(1 + i),
Time: blockTime,
Hash: stateRoot,
AppHash: stateRoot,
ChainId: chainID,
}
cometInfo := comet.Info{
ValidatorsHash: nil,
Evidence: valsetHistory.MissBehaviour(r),
ProposerAddress: activeValidatorSet[0].Address,
LastCommit: activeValidatorSet.NewCommitInfo(r),
}
fOps, pos := futureOpsReg.PopScheduledFor(blockTime), 0
addressCodec := cs.txConfig.SigningContext().AddressCodec()
simsCtx := context.WithValue(rootCtx, corecontext.CometInfoKey, cometInfo) // required for ContextAwareCometInfoService
resultHandlers := make([]simsx.SimDeliveryResultHandler, 0, maxTXPerBlock)
var txPerBlockCounter int
blockRsp, updates, err := app.DeliverSims(simsCtx, blockReqN, func(ctx context.Context) iter.Seq[T] {
return func(yield func(T) bool) {
unbondingTime, err := stakingKeeper.UnbondingTime(ctx)
require.NoError(t, err)
valsetHistory.SetMaxHistory(minBlocksInUnbondingPeriod(unbondingTime))
testData := simsx.NewChainDataSource(ctx, r, authKeeper, bankKeeper, addressCodec, accounts...)
for txPerBlockCounter < maxTXPerBlock {
txPerBlockCounter++
mergedMsgFactory := func() simsx.SimMsgFactoryX {
if pos < len(fOps) {
pos++
return fOps[pos-1]
}
return nextMsgFactory()
}()
reporter := rootReporter.WithScope(mergedMsgFactory.MsgType())
if fx, ok := mergedMsgFactory.(simsx.HasFutureOpsRegistry); ok {
fx.SetFutureOpsRegistry(futureOpsReg)
continue
}
// the stf context is required to access state via keepers
signers, msg := mergedMsgFactory.Create()(ctx, testData, reporter)
if reporter.IsSkipped() {
txSkippedCounter++
require.NoError(t, reporter.Close())
continue
}
resultHandlers = append(resultHandlers, mergedMsgFactory.DeliveryResultHandler())
reporter.Success(msg)
require.NoError(t, reporter.Close())
tx, err := txBuilder.Build(ctx, authKeeper, signers, msg, r, chainID)
require.NoError(t, err)
if !yield(tx) {
return
}
}
}
})
require.NoError(t, err, "%d, %s", blockReqN.Height, blockReqN.Time)
changeSet, err := updates.GetStateChanges()
require.NoError(t, err)
stateRoot, err = appStore.Commit(&store.Changeset{
Version: blockReqN.Height,
Changes: changeSet,
})
require.NoError(t, err)
require.Equal(t, len(resultHandlers), len(blockRsp.TxResults), "txPerBlockCounter: %d, totalSkipped: %d", txPerBlockCounter, txSkippedCounter)
for i, v := range blockRsp.TxResults {
require.NoError(t, resultHandlers[i](v.Error))
}
txTotalCounter += txPerBlockCounter
activeValidatorSet = activeValidatorSet.Update(blockRsp.ValidatorUpdates)
}
fmt.Println("+++ reporter:\n" + rootReporter.Summary().String())
fmt.Printf("Tx total: %d skipped: %d\n", txTotalCounter, txSkippedCounter)
}
// prepareSimsMsgFactories constructs and returns a function to retrieve simulation message factories for all modules.
// It initializes proposal and factory registries, registers proposals and weighted operations, and sorts deterministically.
func prepareSimsMsgFactories(
r *rand.Rand,
modules map[string]appmodulev2.AppModule,
weights simsx.WeightSource,
) func() simsx.SimMsgFactoryX {
moduleNames := slices.Collect(maps.Keys(modules))
slices.Sort(moduleNames) // make deterministic
// get all proposal types
proposalRegistry := simsx.NewUniqueTypeRegistry()
for _, n := range moduleNames {
switch xm := modules[n].(type) { // nolint: gocritic // extended in the future
case HasProposalMsgsX:
xm.ProposalMsgsX(weights, proposalRegistry)
// todo: register legacy and v1 msg proposals
}
}
// register all msg factories
factoryRegistry := simsx.NewUnorderedRegistry()
for _, n := range moduleNames {
switch xm := modules[n].(type) {
case HasWeightedOperationsX:
xm.WeightedOperationsX(weights, factoryRegistry)
case HasWeightedOperationsXWithProposals:
xm.WeightedOperationsX(weights, factoryRegistry, proposalRegistry.Iterator(), nil)
}
}
return simsxv2.NextFactoryFn(factoryRegistry.Elements(), r)
}
func toLegacySimsModule(modules map[string]appmodule.AppModule) []module.AppModuleSimulation {
r := make([]module.AppModuleSimulation, 0, len(modules))
names := slices.Collect(maps.Keys(modules))
slices.Sort(names) // make deterministic
for _, v := range names {
if m, ok := modules[v].(module.AppModuleSimulation); ok {
r = append(r, m)
}
}
return r
}
func minBlocksInUnbondingPeriod(unbondingTime time.Duration) int {
maxblocks := unbondingTime / maxTimePerBlock
return max(int(maxblocks)-1, 1)
}

7
simapp/v2/sim_test.go Normal file
View File

@ -0,0 +1,7 @@
package simapp
import "testing"
func TestSimsAppV2(t *testing.T) {
RunWithSeeds[Tx](t, defaultSeeds)
}

View File

@ -61,24 +61,20 @@ type ResultHandlingSimMsgFactory[T sdk.Msg] struct {
// NewSimMsgFactoryWithDeliveryResultHandler constructor
func NewSimMsgFactoryWithDeliveryResultHandler[T sdk.Msg](f FactoryMethodWithDeliveryResultHandler[T]) *ResultHandlingSimMsgFactory[T] {
// the result handler is always called after the factory. so we initialize it lazy for syntactic sugar and simplicity
// in the message factory function that is implemented by the users
var lazyResultHandler SimDeliveryResultHandler
r := &ResultHandlingSimMsgFactory[T]{
resultHandler: func(err error) error {
return lazyResultHandler(err)
},
resultHandler: expectNoError,
}
r.SimMsgFactoryFn = func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg T) {
signer, msg, lazyResultHandler = f(ctx, testData, reporter)
if lazyResultHandler == nil {
lazyResultHandler = expectNoError
signer, msg, r.resultHandler = f(ctx, testData, reporter)
if r.resultHandler == nil {
r.resultHandler = expectNoError
}
return
}
return r
}
// DeliveryResultHandler result handler of the last msg factory invocation
func (f ResultHandlingSimMsgFactory[T]) DeliveryResultHandler() SimDeliveryResultHandler {
return f.resultHandler
}

View File

@ -160,6 +160,12 @@ func NewUniqueTypeRegistry() UniqueTypeRegistry {
}
func (s UniqueTypeRegistry) Add(weight uint32, f SimMsgFactoryX) {
if weight == 0 {
return
}
if f == nil {
panic("message factory must not be nil")
}
msgType := f.MsgType()
msgTypeURL := sdk.MsgTypeURL(msgType)
if _, exists := s[msgTypeURL]; exists {
@ -170,8 +176,7 @@ func (s UniqueTypeRegistry) Add(weight uint32, f SimMsgFactoryX) {
// Iterator returns an iterator function for a Go for loop sorted by weight desc.
func (s UniqueTypeRegistry) Iterator() WeightedProposalMsgIter {
x := maps.Values(s)
sortedWeightedFactory := slices.SortedFunc(x, func(a, b WeightedFactory) int {
sortedWeightedFactory := slices.SortedFunc(maps.Values(s), func(a, b WeightedFactory) int {
return a.Compare(b)
})
@ -184,6 +189,33 @@ func (s UniqueTypeRegistry) Iterator() WeightedProposalMsgIter {
}
}
var _ Registry = &UnorderedRegistry{}
// UnorderedRegistry represents a collection of WeightedFactory elements without guaranteed order.
// It is used to maintain factories coupled with their associated weights for simulation purposes.
type UnorderedRegistry []WeightedFactory
func NewUnorderedRegistry() *UnorderedRegistry {
r := make(UnorderedRegistry, 0)
return &r
}
// Add appends a new WeightedFactory with the provided weight and factory to the UnorderedRegistry.
func (x *UnorderedRegistry) Add(weight uint32, f SimMsgFactoryX) {
if weight == 0 {
return
}
if f == nil {
panic("message factory must not be nil")
}
*x = append(*x, WeightedFactory{Weight: weight, Factory: f})
}
// Elements returns all collected elements
func (x *UnorderedRegistry) Elements() []WeightedFactory {
return *x
}
// WeightedFactory is a Weight Factory tuple
type WeightedFactory struct {
Weight uint32

View File

@ -119,6 +119,9 @@ func TestUniqueTypeRegistry(t *testing.T) {
exampleFactory := SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
return []SimAccount{}, nil
})
exampleFactory2 := SimMsgFactoryFn[*testdata.MsgCreateDog](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.MsgCreateDog) {
return []SimAccount{}, nil
})
specs := map[string]struct {
src []WeightedFactory
@ -129,6 +132,10 @@ func TestUniqueTypeRegistry(t *testing.T) {
src: []WeightedFactory{{Weight: 1, Factory: exampleFactory}},
exp: []WeightedFactoryMethod{{Weight: 1, Factory: exampleFactory.Create()}},
},
"sorted": {
src: []WeightedFactory{{Weight: 2, Factory: exampleFactory2}, {Weight: 1, Factory: exampleFactory}},
exp: []WeightedFactoryMethod{{Weight: 1, Factory: exampleFactory.Create()}, {Weight: 2, Factory: exampleFactory2.Create()}},
},
"duplicate": {
src: []WeightedFactory{{Weight: 1, Factory: exampleFactory}, {Weight: 2, Factory: exampleFactory}},
expErr: true,

31
simsx/v2/msg_factory.go Normal file
View File

@ -0,0 +1,31 @@
package v2
import (
"math/rand"
"github.com/cosmos/cosmos-sdk/simsx"
)
// NextFactoryFn shuffles and processes a list of weighted factories, returning a selection function for factory objects.
func NextFactoryFn(factories []simsx.WeightedFactory, r *rand.Rand) func() simsx.SimMsgFactoryX {
factCount := len(factories)
r.Shuffle(factCount, func(i, j int) {
factories[i], factories[j] = factories[j], factories[i]
})
var totalWeight int
for _, f := range factories {
totalWeight += int(f.Weight)
}
return func() simsx.SimMsgFactoryX {
// this is copied from old sims WeightedOperations.getSelectOpFn
x := r.Intn(totalWeight)
for i := 0; i < factCount; i++ {
if x <= int(factories[i].Weight) {
return factories[i].Factory
}
x -= int(factories[i].Weight)
}
// shouldn't happen
return factories[0].Factory
}
}

65
simsx/v2/registry.go Normal file
View File

@ -0,0 +1,65 @@
package v2
import (
"math/rand"
"time"
"github.com/huandu/skiplist"
"github.com/cosmos/cosmos-sdk/simsx"
)
// FutureOpsRegistry is a registry for scheduling and retrieving operations mapped to future block times.
type FutureOpsRegistry struct {
list *skiplist.SkipList
}
// NewFutureOpsRegistry constructor
func NewFutureOpsRegistry() *FutureOpsRegistry {
list := skiplist.New(timeComparator{})
list.SetRandSource(rand.NewSource(1))
return &FutureOpsRegistry{list: list}
}
// Add schedules a new operation for the given block time
func (l *FutureOpsRegistry) Add(blockTime time.Time, fx simsx.SimMsgFactoryX) {
if fx == nil {
panic("message factory must not be nil")
}
if blockTime.IsZero() {
return
}
var scheduledOps []simsx.SimMsgFactoryX
if e := l.list.Get(blockTime); e != nil {
scheduledOps = e.Value.([]simsx.SimMsgFactoryX)
}
scheduledOps = append(scheduledOps, fx)
l.list.Set(blockTime, scheduledOps)
}
// PopScheduledFor retrieves and removes all scheduled operations up to the specified block time from the registry.
func (l *FutureOpsRegistry) PopScheduledFor(blockTime time.Time) []simsx.SimMsgFactoryX {
var r []simsx.SimMsgFactoryX
for {
e := l.list.Front()
if e == nil || e.Key().(time.Time).After(blockTime) {
break
}
r = append(r, e.Value.([]simsx.SimMsgFactoryX)...)
l.list.RemoveFront()
}
return r
}
var _ skiplist.Comparable = timeComparator{}
// used for SkipList
type timeComparator struct{}
func (t timeComparator) Compare(lhs, rhs interface{}) int {
return lhs.(time.Time).Compare(rhs.(time.Time))
}
func (t timeComparator) CalcScore(key interface{}) float64 {
return float64(key.(time.Time).UnixNano())
}

198
simsx/v2/txutils.go Normal file
View File

@ -0,0 +1,198 @@
package v2
import (
"context"
"errors"
"fmt"
"math/rand"
"cosmossdk.io/core/transaction"
"github.com/cosmos/cosmos-sdk/client"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/simsx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authsign "github.com/cosmos/cosmos-sdk/x/auth/signing"
)
const DefaultGenTxGas = 10_000_000
type Tx = transaction.Tx
// TXBuilder abstract transaction builder
type TXBuilder[T Tx] interface {
// Build creates a signed transaction
Build(ctx context.Context,
ak simsx.AccountSource,
senders []simsx.SimAccount,
msg sdk.Msg,
r *rand.Rand,
chainID string,
) (T, error)
}
var _ TXBuilder[Tx] = TXBuilderFn[Tx](nil)
// TXBuilderFn adapter that implements the TXBuilder interface
type TXBuilderFn[T Tx] func(ctx context.Context, ak simsx.AccountSource, senders []simsx.SimAccount, msg sdk.Msg, r *rand.Rand, chainID string) (T, error)
func (b TXBuilderFn[T]) Build(ctx context.Context, ak simsx.AccountSource, senders []simsx.SimAccount, msg sdk.Msg, r *rand.Rand, chainID string) (T, error) {
return b(ctx, ak, senders, msg, r, chainID)
}
// NewSDKTXBuilder constructor to create a signed transaction builder for sdk.Tx type.
func NewSDKTXBuilder[T Tx](txConfig client.TxConfig, defaultGas uint64) TXBuilder[T] {
return TXBuilderFn[T](func(ctx context.Context, ak simsx.AccountSource, senders []simsx.SimAccount, msg sdk.Msg, r *rand.Rand, chainID string) (tx T, err error) {
accountNumbers := make([]uint64, len(senders))
sequenceNumbers := make([]uint64, len(senders))
for i := 0; i < len(senders); i++ {
acc := ak.GetAccount(ctx, senders[i].Address)
accountNumbers[i] = acc.GetAccountNumber()
sequenceNumbers[i] = acc.GetSequence()
}
fees := senders[0].LiquidBalance().RandFees()
sdkTx, err := GenSignedMockTx(
r,
txConfig,
[]sdk.Msg{msg},
fees,
defaultGas,
chainID,
accountNumbers,
sequenceNumbers,
simsx.Collect(senders, func(a simsx.SimAccount) cryptotypes.PrivKey { return a.PrivKey })...,
)
if err != nil {
return tx, err
}
out, ok := sdkTx.(T)
if !ok {
return out, errors.New("unexpected Tx type")
}
return out, nil
})
}
// GenSignedMockTx generates a signed mock transaction.
func GenSignedMockTx(
r *rand.Rand,
txConfig client.TxConfig,
msgs []sdk.Msg,
feeAmt sdk.Coins,
gas uint64,
chainID string,
accNums, accSeqs []uint64,
priv ...cryptotypes.PrivKey,
) (sdk.Tx, error) {
sigs := make([]signing.SignatureV2, len(priv))
// create a random length memo
memo := simulation.RandStringOfLength(r, simulation.RandIntBetween(r, 0, 100))
signMode, err := authsign.APISignModeToInternal(txConfig.SignModeHandler().DefaultMode())
if err != nil {
return nil, err
}
// 1st round: set SignatureV2 with empty signatures, to set correct
// signer infos.
for i, p := range priv {
sigs[i] = signing.SignatureV2{
PubKey: p.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: signMode,
},
Sequence: accSeqs[i],
}
}
tx := txConfig.NewTxBuilder()
err = tx.SetMsgs(msgs...)
if err != nil {
return nil, err
}
err = tx.SetSignatures(sigs...)
if err != nil {
return nil, err
}
tx.SetMemo(memo)
tx.SetFeeAmount(feeAmt)
tx.SetGasLimit(gas)
// 2nd round: once all signer infos are set, every signer can sign.
for i, p := range priv {
signerData := authsign.SignerData{
Address: sdk.AccAddress(p.PubKey().Address()).String(),
ChainID: chainID,
AccountNumber: accNums[i],
Sequence: accSeqs[i],
PubKey: p.PubKey(),
}
signBytes, err := authsign.GetSignBytesAdapter(
context.Background(), txConfig.SignModeHandler(), signMode, signerData,
tx.GetTx())
if err != nil {
return nil, fmt.Errorf("sign bytes: %w", err)
}
sig, err := p.Sign(signBytes)
if err != nil {
return nil, fmt.Errorf("sign: %w", err)
}
sigs[i].Data.(*signing.SingleSignatureData).Signature = sig
}
if err = tx.SetSignatures(sigs...); err != nil {
return nil, fmt.Errorf("signature: %w", err)
}
return tx.GetTx(), nil
}
var _ transaction.Codec[Tx] = &GenericTxDecoder[Tx]{}
// GenericTxDecoder Encoder type that implements transaction.Codec
type GenericTxDecoder[T Tx] struct {
txConfig client.TxConfig
}
// NewGenericTxDecoder constructor
func NewGenericTxDecoder[T Tx](txConfig client.TxConfig) *GenericTxDecoder[T] {
return &GenericTxDecoder[T]{txConfig: txConfig}
}
// Decode implements transaction.Codec.
func (t *GenericTxDecoder[T]) Decode(bz []byte) (T, error) {
var out T
tx, err := t.txConfig.TxDecoder()(bz)
if err != nil {
return out, err
}
var ok bool
out, ok = tx.(T)
if !ok {
return out, errors.New("unexpected Tx type")
}
return out, nil
}
// DecodeJSON implements transaction.Codec.
func (t *GenericTxDecoder[T]) DecodeJSON(bz []byte) (T, error) {
var out T
tx, err := t.txConfig.TxJSONDecoder()(bz)
if err != nil {
return out, err
}
var ok bool
out, ok = tx.(T)
if !ok {
return out, errors.New("unexpected Tx type")
}
return out, nil
}

108
simsx/v2/valset.go Normal file
View File

@ -0,0 +1,108 @@
package v2
import (
"bytes"
"crypto/sha256"
"math/rand"
"slices"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
"cosmossdk.io/core/comet"
"github.com/cosmos/cosmos-sdk/simsx"
)
// WeightedValidator represents a validator for usage in the sims runner.
type WeightedValidator struct {
Power int64
Address []byte
Offline bool
}
// Compare determines the order between two WeightedValidator instances.
// Returns -1 if the caller has higher Power, 1 if it has lower Power, and defaults to comparing their Address bytes.
func (a WeightedValidator) Compare(b WeightedValidator) int {
switch {
case a.Power < b.Power:
return 1
case a.Power > b.Power:
return -1
default:
return bytes.Compare(a.Address, b.Address)
}
}
// NewValSet constructor
func NewValSet() WeightedValidators {
return make(WeightedValidators, 0)
}
// WeightedValidators represents a slice of WeightedValidator, used for managing and processing validator sets.
type WeightedValidators []WeightedValidator
func (v WeightedValidators) Update(updates []appmodulev2.ValidatorUpdate) WeightedValidators {
if len(updates) == 0 {
return v
}
const truncatedSize = 20
valUpdates := simsx.Collect(updates, func(u appmodulev2.ValidatorUpdate) WeightedValidator {
hash := sha256.Sum256(u.PubKey)
return WeightedValidator{Power: u.Power, Address: hash[:truncatedSize]}
})
newValset := slices.Clone(v)
for _, u := range valUpdates {
pos := slices.IndexFunc(newValset, func(val WeightedValidator) bool {
return bytes.Equal(u.Address, val.Address)
})
if pos == -1 { // new address
if u.Power > 0 {
newValset = append(newValset, u)
}
continue
}
if u.Power == 0 {
newValset = append(newValset[0:pos], newValset[pos+1:]...)
continue
}
newValset[pos].Power = u.Power
}
newValset = slices.DeleteFunc(newValset, func(validator WeightedValidator) bool {
return validator.Power == 0
})
// sort vals by Power
slices.SortFunc(newValset, func(a, b WeightedValidator) int {
return a.Compare(b)
})
return newValset
}
// NewCommitInfo build Comet commit info for the validator set
func (v WeightedValidators) NewCommitInfo(r *rand.Rand) comet.CommitInfo {
if len(v) == 0 {
return comet.CommitInfo{Votes: make([]comet.VoteInfo, 0)}
}
if r.Intn(10) == 0 {
v[r.Intn(len(v))].Offline = r.Intn(2) == 0
}
votes := make([]comet.VoteInfo, 0, len(v))
for i := range v {
if v[i].Offline {
continue
}
votes = append(votes, comet.VoteInfo{
Validator: comet.Validator{Address: v[i].Address, Power: v[i].Power},
BlockIDFlag: comet.BlockIDFlagCommit,
})
}
return comet.CommitInfo{Round: int32(r.Uint32()), Votes: votes}
}
func (v WeightedValidators) TotalPower() int64 {
var r int64
for _, val := range v {
r += val.Power
}
return r
}

View File

@ -0,0 +1,78 @@
package v2
import (
"math/rand"
"slices"
"time"
"cosmossdk.io/core/comet"
"github.com/cosmos/cosmos-sdk/simsx"
)
type historicValSet struct {
blockTime time.Time
vals WeightedValidators
}
type ValSetHistory struct {
maxElements int
blockOffset int
vals []historicValSet
}
func NewValSetHistory(maxElements int) *ValSetHistory {
return &ValSetHistory{
maxElements: maxElements,
blockOffset: 1, // start at height 1
vals: make([]historicValSet, 0, maxElements),
}
}
func (h *ValSetHistory) Add(blockTime time.Time, vals WeightedValidators) {
vals = slices.DeleteFunc(vals, func(validator WeightedValidator) bool {
return validator.Power == 0
})
slices.SortFunc(vals, func(a, b WeightedValidator) int {
return b.Compare(a)
})
newEntry := historicValSet{blockTime: blockTime, vals: vals}
if len(h.vals) >= h.maxElements {
h.vals = append(h.vals[1:], newEntry)
h.blockOffset++
return
}
h.vals = append(h.vals, newEntry)
}
// MissBehaviour determines if a random validator misbehaves, creating and returning evidence for duplicate voting.
// Returns a slice of comet.Evidence if misbehavior is detected; otherwise, returns nil.
// Has a 1% chance of generating evidence for a validator's misbehavior.
// Recursively checks for other misbehavior instances and combines their evidence if any.
// Utilizes a random generator to select a validator and evidence-related attributes.
func (h *ValSetHistory) MissBehaviour(r *rand.Rand) []comet.Evidence {
if r.Intn(100) != 0 { // 1% chance
return nil
}
n := r.Intn(len(h.vals))
badVal := simsx.OneOf(r, h.vals[n].vals)
evidence := comet.Evidence{
Type: comet.DuplicateVote,
Validator: comet.Validator{Address: badVal.Address, Power: badVal.Power},
Height: int64(h.blockOffset + n),
Time: h.vals[n].blockTime,
TotalVotingPower: h.vals[n].vals.TotalPower(),
}
if otherEvidence := h.MissBehaviour(r); otherEvidence != nil {
return append([]comet.Evidence{evidence}, otherEvidence...)
}
return []comet.Evidence{evidence}
}
func (h *ValSetHistory) SetMaxHistory(v int) {
h.maxElements = v
if len(h.vals) > h.maxElements {
diff := len(h.vals) - h.maxElements
h.vals = h.vals[diff:]
h.blockOffset += diff
}
}

View File

@ -14,6 +14,8 @@ const (
DBTypePebbleDB DBType = "pebbledb"
DBTypePrefixDB DBType = "prefixdb"
DBTypeMemDB DBType = "memdb" // used for sims
DBFileSuffix string = ".db"
)
@ -24,6 +26,8 @@ func NewDB(dbType DBType, name, dataDir string, opts coreserver.DynamicConfig) (
case DBTypePebbleDB:
return NewPebbleDB(name, dataDir)
case DBTypeMemDB:
return NewMemDB(), nil
}
return nil, fmt.Errorf("unsupported db type: %s", dbType)

View File

@ -1118,7 +1118,7 @@ func TestSimulateTx_GasImprovements(t *testing.T) {
testCases := []struct {
name string
simulateArgs []string
txArgs []string
txArgs []string
}{
{
"simulate without fees",
@ -1152,17 +1152,17 @@ func TestSimulateTx_GasImprovements(t *testing.T) {
// create unsign tx
res := cli.RunCommandWithArgs(tc.simulateArgs...)
txFile := systest.StoreTempFile(t, []byte(res))
res = cli.RunCommandWithArgs("tx", "sign", txFile.Name(), "--from="+valAddr, "--chain-id="+cli.ChainID(), "--keyring-backend=test", "--home="+systest.Sut.NodeDir(0))
signedTxFile := systest.StoreTempFile(t, []byte(res))
res = cli.RunCommandWithArgs("tx", "encode", signedTxFile.Name())
txBz, err := base64.StdEncoding.DecodeString(res)
require.NoError(t, err)
reqBz, err := json.Marshal(&tx.SimulateRequest{TxBytes: txBz})
require.NoError(t, err)
resBz, err := testutil.PostRequest(fmt.Sprintf("%s/cosmos/tx/v1beta1/simulate", baseURL), "application/json", reqBz)
require.NoError(t, err)
gasUsed := gjson.Get(string(resBz), "gas_info.gas_used").Int()
@ -1184,7 +1184,7 @@ func TestSimulateTx_GasImprovements(t *testing.T) {
fmt.Println("gasAdjustment", i, gasAdjustment[i])
}
}
// Calculate average adjustments
total := 0.0
for i := 0; i < txlen; i++ {

View File

@ -212,6 +212,7 @@ func AppStateRandomizedFn(
// AppStateFromGenesisFileFn util function to generate the genesis AppState
// from a genesis.json file.
// Deprecated: the private keys are not matching the accounts read from app state
func AppStateFromGenesisFileFn(_ io.Reader, cdc codec.JSONCodec, genesisFile string) (genutiltypes.AppGenesis, []simtypes.Account, error) {
file, err := os.Open(filepath.Clean(genesisFile))
if err != nil {
@ -233,6 +234,8 @@ func AppStateFromGenesisFileFn(_ io.Reader, cdc codec.JSONCodec, genesisFile str
return *genesis, newAccs, nil
}
// AccountsFromAppState
// Deprecated: the private keys are not matching the accounts read from app state
func AccountsFromAppState(cdc codec.JSONCodec, appStateJSON json.RawMessage) ([]simtypes.Account, error) {
var appState map[string]json.RawMessage
if err := json.Unmarshal(appStateJSON, &appState); err != nil {

View File

@ -22,7 +22,16 @@ import (
)
// GenSignedMockTx generates a signed mock transaction.
func GenSignedMockTx(r *rand.Rand, txConfig client.TxConfig, msgs []sdk.Msg, feeAmt sdk.Coins, gas uint64, chainID string, accNums, accSeqs []uint64, priv ...cryptotypes.PrivKey) (sdk.Tx, error) {
func GenSignedMockTx(
r *rand.Rand,
txConfig client.TxConfig,
msgs []sdk.Msg,
feeAmt sdk.Coins,
gas uint64,
chainID string,
accNums, accSeqs []uint64,
priv ...cryptotypes.PrivKey,
) (sdk.Tx, error) {
sigs := make([]signing.SignatureV2, len(priv))
// create a random length memo

View File

@ -1,12 +1,13 @@
package testdata
import (
"cosmossdk.io/math"
"testing"
"gotest.tools/v3/assert"
"pgregory.net/rapid"
"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256r1"

View File

@ -131,8 +131,7 @@ func submitProposalWithVotesScheduled(
proposalMsgs ...sdk.Msg,
) ([]simsx.SimAccount, *v1.MsgSubmitProposal) {
r := testData.Rand()
expedited := true
// expedited := r.Bool()
expedited := r.Bool()
params := must(k.Params.Get(ctx))
minDeposits := params.MinDeposit
if expedited {

View File

@ -14,7 +14,9 @@ func MsgSendFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*nft.MsgSend] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *nft.MsgSend) {
from := testData.AnyAccount(reporter, simsx.WithSpendableBalance())
to := testData.AnyAccount(reporter, simsx.ExcludeAccounts(from))
if reporter.IsSkipped() {
return nil, nil
}
n, err := randNFT(ctx, testData.Rand(), k, from.Address)
if err != nil {
reporter.Skip(err.Error())

View File

@ -321,21 +321,11 @@ func MsgRotateConsPubKeyFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.M
return nil, nil
}
// check whether the new cons key associated with another validator
newConsAddr := sdk.ConsAddress(otherAccount.ConsKey.PubKey().Address())
if _, err := k.GetValidatorByConsAddr(ctx, newConsAddr); err == nil {
reporter.Skip("cons key already used")
assertKeyUnused(ctx, reporter, k, otherAccount.ConsKey.PubKey())
if reporter.IsSkipped() {
return nil, nil
}
msg := must(types.NewMsgRotateConsPubKey(val.GetOperator(), otherAccount.ConsKey.PubKey()))
// check if there's another key rotation for this same key in the same block
for _, r := range must(k.GetBlockConsPubKeyRotationHistory(ctx)) {
if r.NewConsPubkey.Compare(msg.NewPubkey) == 0 {
reporter.Skip("cons key already used in this block")
return nil, nil
}
}
return []simsx.SimAccount{valOper}, msg
}
}
@ -371,6 +361,16 @@ func randomValidator(ctx context.Context, reporter simsx.SimulationReporter, k *
// skips execution if there's another key rotation for the same key in the same block
func assertKeyUnused(ctx context.Context, reporter simsx.SimulationReporter, k *keeper.Keeper, newPubKey cryptotypes.PubKey) {
newConsAddr := sdk.ConsAddress(newPubKey.Address())
if rotatedTo, _ := k.ConsAddrToValidatorIdentifierMap.Get(ctx, newConsAddr); rotatedTo != nil {
reporter.Skip("consensus key already used")
return
}
if _, err := k.GetValidatorByConsAddr(ctx, newConsAddr); err == nil {
reporter.Skip("cons key already used")
return
}
allRotations, err := k.GetBlockConsPubKeyRotationHistory(ctx)
if err != nil {
reporter.Skipf("cannot get block cons key rotation history: %s", err.Error())