cosmos-sdk/x/staking/simulation/msg_factory.go
crStiv 64348183e7
fix: multiple typos of different importance (#24987)
Co-authored-by: User <user@example.com>
Co-authored-by: Alex | Interchain Labs <alex@interchainlabs.io>
2025-07-24 21:22:04 +00:00

335 lines
12 KiB
Go

package simulation
import (
"context"
"slices"
"time"
"cosmossdk.io/math"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/testutil/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 unbondings")
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
}