feat(sims): Add simsx support to modules (#24145)

Co-authored-by: Alex | Interchain Labs <alex@interchainlabs.io>
This commit is contained in:
Alexander Peters
2025-04-02 13:23:36 +00:00
committed by GitHub
co-authored by Alex | Interchain Labs
parent 99da3279be
commit ea908cef68
53 changed files with 2025 additions and 241 deletions
-8
View File
@@ -1,8 +1,6 @@
package simulation
import (
"encoding/json"
"fmt"
"math/rand"
"time"
@@ -91,11 +89,5 @@ func RandomizedGenState(simState *module.SimulationState) {
}
stakingGenesis := types.NewGenesisState(params, validators, delegations)
bz, err := json.MarshalIndent(&stakingGenesis.Params, "", " ")
if err != nil {
panic(err)
}
fmt.Printf("Selected randomly generated staking parameters:\n%s\n", bz)
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(stakingGenesis)
}
+334
View File
@@ -0,0 +1,334 @@
package simulation
import (
"context"
"slices"
"time"
"cosmossdk.io/math"
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/x/staking/keeper"
"github.com/cosmos/cosmos-sdk/x/staking/types"
)
func MsgCreateValidatorFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgCreateValidator] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgCreateValidator) {
r := testData.Rand()
withoutValidators := simsx.SimAccountFilterFn(func(a simsx.SimAccount) bool {
_, err := k.GetValidator(ctx, sdk.ValAddress(a.Address))
return err != nil
})
withoutConsAddrUsed := simsx.SimAccountFilterFn(func(a simsx.SimAccount) bool {
consPubKey := sdk.GetConsAddress(a.ConsKey.PubKey())
_, err := k.GetValidatorByConsAddr(ctx, consPubKey)
return err != nil
})
bondDenom := must(k.BondDenom(ctx))
valOper := testData.AnyAccount(reporter, withoutValidators, withoutConsAddrUsed, simsx.WithDenomBalance(bondDenom))
if reporter.IsSkipped() {
return nil, nil
}
newPubKey := valOper.ConsKey.PubKey()
assertKeyUnused(ctx, reporter, k, newPubKey)
if reporter.IsSkipped() {
return nil, nil
}
selfDelegation := valOper.LiquidBalance().RandSubsetCoin(reporter, bondDenom)
description := types.NewDescription(
r.StringN(10),
r.StringN(10),
r.StringN(10),
r.StringN(10),
r.StringN(10),
)
maxCommission := math.LegacyNewDecWithPrec(int64(r.IntInRange(0, 100)), 2)
commission := types.NewCommissionRates(
r.DecN(maxCommission),
maxCommission,
r.DecN(maxCommission),
)
addr := must(k.ValidatorAddressCodec().BytesToString(valOper.Address))
msg, err := types.NewMsgCreateValidator(addr, newPubKey, selfDelegation, description, commission, math.OneInt())
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
return []simsx.SimAccount{valOper}, msg
}
}
func MsgDelegateFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgDelegate] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgDelegate) {
r := testData.Rand()
bondDenom := must(k.BondDenom(ctx))
val := randomValidator(ctx, reporter, k, r)
if reporter.IsSkipped() {
return nil, nil
}
if val.InvalidExRate() {
reporter.Skip("validator's invalid exchange rate")
return nil, nil
}
sender := testData.AnyAccount(reporter)
delegation := sender.LiquidBalance().RandSubsetCoin(reporter, bondDenom)
return []simsx.SimAccount{sender}, types.NewMsgDelegate(sender.AddressBech32, val.GetOperator(), delegation)
}
}
func MsgUndelegateFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgUndelegate] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgUndelegate) {
r := testData.Rand()
bondDenom := must(k.BondDenom(ctx))
val := randomValidator(ctx, reporter, k, r)
if reporter.IsSkipped() {
return nil, nil
}
// select delegator and amount for undelegate
valAddr := must(k.ValidatorAddressCodec().StringToBytes(val.GetOperator()))
delegations := must(k.GetValidatorDelegations(ctx, valAddr))
if delegations == nil {
reporter.Skip("no delegation entries")
return nil, nil
}
// get random delegator from validator
delegation := delegations[r.Intn(len(delegations))]
delAddr := delegation.GetDelegatorAddr()
delegator := testData.GetAccount(reporter, delAddr)
if hasMaxUD := must(k.HasMaxUnbondingDelegationEntries(ctx, delegator.Address, valAddr)); hasMaxUD {
reporter.Skipf("max unbodings")
return nil, nil
}
totalBond := val.TokensFromShares(delegation.GetShares()).TruncateInt()
if !totalBond.IsPositive() {
reporter.Skip("total bond is negative")
return nil, nil
}
unbondAmt := must(r.PositiveSDKIntn(totalBond))
msg := types.NewMsgUndelegate(delAddr, val.GetOperator(), sdk.NewCoin(bondDenom, unbondAmt))
return []simsx.SimAccount{delegator}, msg
}
}
func MsgEditValidatorFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgEditValidator] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgEditValidator) {
r := testData.Rand()
val := randomValidator(ctx, reporter, k, r)
if reporter.IsSkipped() {
return nil, nil
}
newCommissionRate := r.DecN(val.Commission.MaxRate)
if err := val.Commission.ValidateNewRate(newCommissionRate, simsx.BlockTime(ctx)); err != nil {
// skip as the commission is invalid
reporter.Skip("invalid commission rate")
return nil, nil
}
valOpAddrBz := must(k.ValidatorAddressCodec().StringToBytes(val.GetOperator()))
valOper := testData.GetAccountbyAccAddr(reporter, valOpAddrBz)
d := types.NewDescription(r.StringN(10), r.StringN(10), r.StringN(10), r.StringN(10), r.StringN(10))
msg := types.NewMsgEditValidator(val.GetOperator(), d, &newCommissionRate, nil)
return []simsx.SimAccount{valOper}, msg
}
}
func MsgBeginRedelegateFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgBeginRedelegate] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgBeginRedelegate) {
bondDenom := must(k.BondDenom(ctx))
if !testData.IsSendEnabledDenom(bondDenom) {
reporter.Skip("bond denom send not enabled")
return nil, nil
}
r := testData.Rand()
// select random validator as src
vals := must(k.GetAllValidators(ctx))
if len(vals) < 2 {
reporter.Skip("insufficient number of validators")
return nil, nil
}
srcVal := simsx.OneOf(r, vals)
srcValOpAddrBz := must(k.ValidatorAddressCodec().StringToBytes(srcVal.GetOperator()))
delegations := must(k.GetValidatorDelegations(ctx, srcValOpAddrBz))
if delegations == nil {
reporter.Skip("no delegations")
return nil, nil
}
// get random delegator from src validator
delegation := simsx.OneOf(r, delegations)
totalBond := srcVal.TokensFromShares(delegation.GetShares()).TruncateInt()
if !totalBond.IsPositive() {
reporter.Skip("total bond is negative")
return nil, nil
}
redAmount, err := r.PositiveSDKIntn(totalBond)
if err != nil || redAmount.IsZero() {
reporter.Skip("unable to generate positive amount")
return nil, nil
}
if totalBond.Sub(redAmount).IsZero() {
reporter.Skip("can not redelegate all")
return nil, nil
}
// check if the shares truncate to zero
shares := must(srcVal.SharesFromTokens(redAmount))
if srcVal.TokensFromShares(shares).TruncateInt().IsZero() {
reporter.Skip("shares truncate to zero")
return nil, nil
}
// pick a random delegator
delAddr := delegation.GetDelegatorAddr()
delAddrBz := must(testData.AddressCodec().StringToBytes(delAddr))
if hasRecRedel := must(k.HasReceivingRedelegation(ctx, delAddrBz, srcValOpAddrBz)); hasRecRedel {
reporter.Skip("receiving redelegation is not allowed")
return nil, nil
}
delegator := testData.GetAccountbyAccAddr(reporter, delAddrBz)
if reporter.IsSkipped() {
return nil, nil
}
// get random destination validator
destVal := simsx.OneOf(r, vals)
if srcVal.Equal(&destVal) {
destVal = simsx.OneOf(r, slices.DeleteFunc(vals, func(v types.Validator) bool { return srcVal.Equal(&v) }))
}
if destVal.InvalidExRate() {
reporter.Skip("invalid delegation rate")
return nil, nil
}
destAddrBz := must(k.ValidatorAddressCodec().StringToBytes(destVal.GetOperator()))
if hasMaxRedel := must(k.HasMaxRedelegationEntries(ctx, delAddrBz, srcValOpAddrBz, destAddrBz)); hasMaxRedel {
reporter.Skip("maximum redelegation entries reached")
return nil, nil
}
msg := types.NewMsgBeginRedelegate(
delAddr, srcVal.GetOperator(), destVal.GetOperator(),
sdk.NewCoin(bondDenom, redAmount),
)
return []simsx.SimAccount{delegator}, msg
}
}
func MsgCancelUnbondingDelegationFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.MsgCancelUnbondingDelegation] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *types.MsgCancelUnbondingDelegation) {
r := testData.Rand()
val := randomValidator(ctx, reporter, k, r)
if reporter.IsSkipped() {
return nil, nil
}
if val.IsJailed() || val.InvalidExRate() {
reporter.Skip("validator is jailed")
return nil, nil
}
valOpAddrBz := must(k.ValidatorAddressCodec().StringToBytes(val.GetOperator()))
valOper := testData.GetAccountbyAccAddr(reporter, valOpAddrBz)
unbondingDelegation, err := k.GetUnbondingDelegation(ctx, valOper.Address, valOpAddrBz)
if err != nil {
reporter.Skip("no unbonding delegation")
return nil, nil
}
// This is a temporary fix to make staking simulation pass. We should fetch
// the first unbondingDelegationEntry that matches the creationHeight, because
// currently the staking msgServer chooses the first unbondingDelegationEntry
// with the matching creationHeight.
//
// ref: https://github.com/cosmos/cosmos-sdk/issues/12932
creationHeight := unbondingDelegation.Entries[r.Intn(len(unbondingDelegation.Entries))].CreationHeight
var unbondingDelegationEntry types.UnbondingDelegationEntry
for _, entry := range unbondingDelegation.Entries {
if entry.CreationHeight == creationHeight {
unbondingDelegationEntry = entry
break
}
}
if unbondingDelegationEntry.CompletionTime.Before(simsx.BlockTime(ctx)) {
reporter.Skip("unbonding delegation is already processed")
return nil, nil
}
if !unbondingDelegationEntry.Balance.IsPositive() {
reporter.Skip("delegator receiving balance is negative")
return nil, nil
}
cancelBondAmt := r.Amount(unbondingDelegationEntry.Balance)
if cancelBondAmt.IsZero() {
reporter.Skip("cancelBondAmt amount is zero")
return nil, nil
}
msg := types.NewMsgCancelUnbondingDelegation(
valOper.AddressBech32,
val.GetOperator(),
unbondingDelegationEntry.CreationHeight,
sdk.NewCoin(must(k.BondDenom(ctx)), cancelBondAmt),
)
return []simsx.SimAccount{valOper}, msg
}
}
// 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) {
r := testData.Rand()
params := types.DefaultParams()
// do not modify denom or staking will break
params.HistoricalEntries = r.Uint32InRange(0, 1000)
params.MaxEntries = r.Uint32InRange(1, 1000)
params.MaxValidators = r.Uint32InRange(1, 1000)
params.UnbondingTime = time.Duration(r.Timestamp().UnixNano())
// modifying commission rate can cause issues for proposals within the same block
// params.MinCommissionRate = r.DecN(sdkmath.LegacyNewDec(1))
return nil, &types.MsgUpdateParams{
Authority: testData.ModuleAccountAddress(reporter, "gov"),
Params: params,
}
}
}
func randomValidator(ctx context.Context, reporter simsx.SimulationReporter, k *keeper.Keeper, r *simsx.XRand) types.Validator {
vals, err := k.GetAllValidators(ctx)
if err != nil || len(vals) == 0 {
reporter.Skipf("unable to get validators or empty list: %s", err)
return types.Validator{}
}
return simsx.OneOf(r, vals)
}
// 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 _, err := k.GetValidatorByConsAddr(ctx, newConsAddr); err == nil {
reporter.Skip("cons key already used")
return
}
}
func must[T any](r T, err error) T {
if err != nil {
panic(err)
}
return r
}
+8
View File
@@ -19,6 +19,7 @@ import (
)
// Simulation operation weights constants
// will be removed in the future
const (
DefaultWeightMsgCreateValidator int = 100
DefaultWeightMsgEditValidator int = 5
@@ -36,6 +37,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,
@@ -106,6 +108,7 @@ func WeightedOperations(
}
// SimulateMsgCreateValidator generates a MsgCreateValidator with random values
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgCreateValidator(
txGen client.TxConfig,
ak types.AccountKeeper,
@@ -193,6 +196,7 @@ func SimulateMsgCreateValidator(
}
// SimulateMsgEditValidator generates a MsgEditValidator with random values
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgEditValidator(
txGen client.TxConfig,
ak types.AccountKeeper,
@@ -268,6 +272,7 @@ func SimulateMsgEditValidator(
}
// SimulateMsgDelegate generates a MsgDelegate with random values
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgDelegate(
txGen client.TxConfig,
ak types.AccountKeeper,
@@ -346,6 +351,7 @@ func SimulateMsgDelegate(
}
// SimulateMsgUndelegate generates a MsgUndelegate with random values
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgUndelegate(
txGen client.TxConfig,
ak types.AccountKeeper,
@@ -461,6 +467,7 @@ func SimulateMsgUndelegate(
}
// SimulateMsgCancelUnbondingDelegate generates a MsgCancelUnbondingDelegate with random values
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgCancelUnbondingDelegate(
txGen client.TxConfig,
ak types.AccountKeeper,
@@ -561,6 +568,7 @@ func SimulateMsgCancelUnbondingDelegate(
}
// SimulateMsgBeginRedelegate generates a MsgBeginRedelegate with random values
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgBeginRedelegate(
txGen client.TxConfig,
ak types.AccountKeeper,
+3
View File
@@ -14,6 +14,7 @@ import (
)
// Simulation operation weights constants
// will be removed in the future
const (
DefaultWeightMsgUpdateParams int = 100
@@ -21,6 +22,7 @@ const (
)
// ProposalMsgs defines the module weighted proposals' contents
// migrate to the msg factories instead, this method will be removed in the future
func ProposalMsgs() []simtypes.WeightedProposalMsg {
return []simtypes.WeightedProposalMsg{
simulation.NewWeightedProposalMsg(
@@ -32,6 +34,7 @@ func ProposalMsgs() []simtypes.WeightedProposalMsg {
}
// SimulateMsgUpdateParams returns a random MsgUpdateParams
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgUpdateParams(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg {
// use the default gov module account address as authority
var authority sdk.AccAddress = address.Module("gov")