cosmos-sdk/x/staking/simulation/operations.go
MD Aleem 9094794478
feat!: Ensure InitGenesis returns with non-empty validator set (#9697)
<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->

## Description

Closes: #8961 

SDK allows InitGenesis to return with an empty validator set. In practice, the error for an empty validator set gets thrown in tendermint. To fix this,

* Add non-empty validator set check to the `mm.InitGenesis` function. This will break `simapp.Setup` because it relies on an empty validator set [#comment](https://github.com/cosmos/cosmos-sdk/pull/8909#issuecomment-804850834).
* Update `simapp.Setup` to use a single validator.
* Fix failing tests (Most of them are keeper tests).

<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->

---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [x] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [x] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [x] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
2021-10-05 12:02:51 +00:00

474 lines
16 KiB
Go

package simulation
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
simappparams "github.com/cosmos/cosmos-sdk/simapp/params"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/cosmos/cosmos-sdk/x/staking/keeper"
"github.com/cosmos/cosmos-sdk/x/staking/types"
)
// Simulation operation weights constants
const (
OpWeightMsgCreateValidator = "op_weight_msg_create_validator"
OpWeightMsgEditValidator = "op_weight_msg_edit_validator"
OpWeightMsgDelegate = "op_weight_msg_delegate"
OpWeightMsgUndelegate = "op_weight_msg_undelegate"
OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate"
)
// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
appParams simtypes.AppParams, cdc codec.JSONCodec, ak types.AccountKeeper,
bk types.BankKeeper, k keeper.Keeper,
) simulation.WeightedOperations {
var (
weightMsgCreateValidator int
weightMsgEditValidator int
weightMsgDelegate int
weightMsgUndelegate int
weightMsgBeginRedelegate int
)
appParams.GetOrGenerate(cdc, OpWeightMsgCreateValidator, &weightMsgCreateValidator, nil,
func(_ *rand.Rand) {
weightMsgCreateValidator = simappparams.DefaultWeightMsgCreateValidator
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgEditValidator, &weightMsgEditValidator, nil,
func(_ *rand.Rand) {
weightMsgEditValidator = simappparams.DefaultWeightMsgEditValidator
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgDelegate, &weightMsgDelegate, nil,
func(_ *rand.Rand) {
weightMsgDelegate = simappparams.DefaultWeightMsgDelegate
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgUndelegate, &weightMsgUndelegate, nil,
func(_ *rand.Rand) {
weightMsgUndelegate = simappparams.DefaultWeightMsgUndelegate
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgBeginRedelegate, &weightMsgBeginRedelegate, nil,
func(_ *rand.Rand) {
weightMsgBeginRedelegate = simappparams.DefaultWeightMsgBeginRedelegate
},
)
return simulation.WeightedOperations{
simulation.NewWeightedOperation(
weightMsgCreateValidator,
SimulateMsgCreateValidator(ak, bk, k),
),
simulation.NewWeightedOperation(
weightMsgEditValidator,
SimulateMsgEditValidator(ak, bk, k),
),
simulation.NewWeightedOperation(
weightMsgDelegate,
SimulateMsgDelegate(ak, bk, k),
),
simulation.NewWeightedOperation(
weightMsgUndelegate,
SimulateMsgUndelegate(ak, bk, k),
),
simulation.NewWeightedOperation(
weightMsgBeginRedelegate,
SimulateMsgBeginRedelegate(ak, bk, k),
),
}
}
// SimulateMsgCreateValidator generates a MsgCreateValidator with random values
func SimulateMsgCreateValidator(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
simAccount, _ := simtypes.RandomAcc(r, accs)
address := sdk.ValAddress(simAccount.Address)
// ensure the validator doesn't exist already
_, found := k.GetValidator(ctx, address)
if found {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgCreateValidator, "validator already exists"), nil, nil
}
denom := k.GetParams(ctx).BondDenom
balance := bk.GetBalance(ctx, simAccount.Address, denom).Amount
if !balance.IsPositive() {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgCreateValidator, "balance is negative"), nil, nil
}
amount, err := simtypes.RandPositiveInt(r, balance)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgCreateValidator, "unable to generate positive amount"), nil, err
}
selfDelegation := sdk.NewCoin(denom, amount)
account := ak.GetAccount(ctx, simAccount.Address)
spendable := bk.SpendableCoins(ctx, account.GetAddress())
var fees sdk.Coins
coins, hasNeg := spendable.SafeSub(sdk.Coins{selfDelegation})
if !hasNeg {
fees, err = simtypes.RandomFees(r, ctx, coins)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgCreateValidator, "unable to generate fees"), nil, err
}
}
description := types.NewDescription(
simtypes.RandStringOfLength(r, 10),
simtypes.RandStringOfLength(r, 10),
simtypes.RandStringOfLength(r, 10),
simtypes.RandStringOfLength(r, 10),
simtypes.RandStringOfLength(r, 10),
)
maxCommission := sdk.NewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 100)), 2)
commission := types.NewCommissionRates(
simtypes.RandomDecAmount(r, maxCommission),
maxCommission,
simtypes.RandomDecAmount(r, maxCommission),
)
msg, err := types.NewMsgCreateValidator(address, simAccount.ConsKey.PubKey(), selfDelegation, description, commission, sdk.OneInt())
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to create CreateValidator message"), nil, err
}
txCtx := simulation.OperationInput{
App: app,
TxGen: simappparams.MakeTestEncodingConfig().TxConfig,
Cdc: nil,
Msg: msg,
MsgType: msg.Type(),
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
ModuleName: types.ModuleName,
}
return simulation.GenAndDeliverTx(txCtx, fees)
}
}
// SimulateMsgEditValidator generates a MsgEditValidator with random values
func SimulateMsgEditValidator(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
if len(k.GetAllValidators(ctx)) == 0 {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEditValidator, "number of validators equal zero"), nil, nil
}
val, ok := keeper.RandomValidator(r, k, ctx)
if !ok {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEditValidator, "unable to pick a validator"), nil, nil
}
address := val.GetOperator()
newCommissionRate := simtypes.RandomDecAmount(r, val.Commission.MaxRate)
if err := val.Commission.ValidateNewRate(newCommissionRate, ctx.BlockHeader().Time); err != nil {
// skip as the commission is invalid
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEditValidator, "invalid commission rate"), nil, nil
}
simAccount, found := simtypes.FindAccount(accs, sdk.AccAddress(val.GetOperator()))
if !found {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEditValidator, "unable to find account"), nil, fmt.Errorf("validator %s not found", val.GetOperator())
}
account := ak.GetAccount(ctx, simAccount.Address)
spendable := bk.SpendableCoins(ctx, account.GetAddress())
description := types.NewDescription(
simtypes.RandStringOfLength(r, 10),
simtypes.RandStringOfLength(r, 10),
simtypes.RandStringOfLength(r, 10),
simtypes.RandStringOfLength(r, 10),
simtypes.RandStringOfLength(r, 10),
)
msg := types.NewMsgEditValidator(address, description, &newCommissionRate, nil)
txCtx := simulation.OperationInput{
R: r,
App: app,
TxGen: simappparams.MakeTestEncodingConfig().TxConfig,
Cdc: nil,
Msg: msg,
MsgType: msg.Type(),
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
Bankkeeper: bk,
ModuleName: types.ModuleName,
CoinsSpentInMsg: spendable,
}
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
// SimulateMsgDelegate generates a MsgDelegate with random values
func SimulateMsgDelegate(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
denom := k.GetParams(ctx).BondDenom
if len(k.GetAllValidators(ctx)) == 0 {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDelegate, "number of validators equal zero"), nil, nil
}
simAccount, _ := simtypes.RandomAcc(r, accs)
val, ok := keeper.RandomValidator(r, k, ctx)
if !ok {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDelegate, "unable to pick a validator"), nil, nil
}
if val.InvalidExRate() {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDelegate, "validator's invalid echange rate"), nil, nil
}
amount := bk.GetBalance(ctx, simAccount.Address, denom).Amount
if !amount.IsPositive() {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDelegate, "balance is negative"), nil, nil
}
amount, err := simtypes.RandPositiveInt(r, amount)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDelegate, "unable to generate positive amount"), nil, err
}
bondAmt := sdk.NewCoin(denom, amount)
account := ak.GetAccount(ctx, simAccount.Address)
spendable := bk.SpendableCoins(ctx, account.GetAddress())
var fees sdk.Coins
coins, hasNeg := spendable.SafeSub(sdk.Coins{bondAmt})
if !hasNeg {
fees, err = simtypes.RandomFees(r, ctx, coins)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDelegate, "unable to generate fees"), nil, err
}
}
msg := types.NewMsgDelegate(simAccount.Address, val.GetOperator(), bondAmt)
txCtx := simulation.OperationInput{
App: app,
TxGen: simappparams.MakeTestEncodingConfig().TxConfig,
Cdc: nil,
Msg: msg,
MsgType: msg.Type(),
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
ModuleName: types.ModuleName,
}
return simulation.GenAndDeliverTx(txCtx, fees)
}
}
// SimulateMsgUndelegate generates a MsgUndelegate with random values
func SimulateMsgUndelegate(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
// get random validator
validator, ok := keeper.RandomValidator(r, k, ctx)
if !ok {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUndelegate, "validator is not ok"), nil, nil
}
valAddr := validator.GetOperator()
delegations := k.GetValidatorDelegations(ctx, validator.GetOperator())
if delegations == nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUndelegate, "keeper does have any delegation entries"), nil, nil
}
// get random delegator from validator
delegation := delegations[r.Intn(len(delegations))]
delAddr := delegation.GetDelegatorAddr()
if k.HasMaxUnbondingDelegationEntries(ctx, delAddr, valAddr) {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUndelegate, "keeper does have a max unbonding delegation entries"), nil, nil
}
totalBond := validator.TokensFromShares(delegation.GetShares()).TruncateInt()
if !totalBond.IsPositive() {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUndelegate, "total bond is negative"), nil, nil
}
unbondAmt, err := simtypes.RandPositiveInt(r, totalBond)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUndelegate, "invalid unbond amount"), nil, err
}
if unbondAmt.IsZero() {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUndelegate, "unbond amount is zero"), nil, nil
}
msg := types.NewMsgUndelegate(
delAddr, valAddr, sdk.NewCoin(k.BondDenom(ctx), unbondAmt),
)
// need to retrieve the simulation account associated with delegation to retrieve PrivKey
var simAccount simtypes.Account
for _, simAcc := range accs {
if simAcc.Address.Equals(delAddr) {
simAccount = simAcc
break
}
}
// if simaccount.PrivKey == nil, delegation address does not exist in accs. Return error
if simAccount.PrivKey == nil {
return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "account private key is nil"), nil, fmt.Errorf("delegation addr: %s does not exist in simulation accounts", delAddr)
}
account := ak.GetAccount(ctx, delAddr)
spendable := bk.SpendableCoins(ctx, account.GetAddress())
txCtx := simulation.OperationInput{
R: r,
App: app,
TxGen: simappparams.MakeTestEncodingConfig().TxConfig,
Cdc: nil,
Msg: msg,
MsgType: msg.Type(),
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
Bankkeeper: bk,
ModuleName: types.ModuleName,
CoinsSpentInMsg: spendable,
}
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
// SimulateMsgBeginRedelegate generates a MsgBeginRedelegate with random values
func SimulateMsgBeginRedelegate(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
// get random source validator
srcVal, ok := keeper.RandomValidator(r, k, ctx)
if !ok {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "unable to pick validator"), nil, nil
}
srcAddr := srcVal.GetOperator()
delegations := k.GetValidatorDelegations(ctx, srcAddr)
if delegations == nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "keeper does have any delegation entries"), nil, nil
}
// get random delegator from src validator
delegation := delegations[r.Intn(len(delegations))]
delAddr := delegation.GetDelegatorAddr()
if k.HasReceivingRedelegation(ctx, delAddr, srcAddr) {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "receveing redelegation is not allowed"), nil, nil // skip
}
// get random destination validator
destVal, ok := keeper.RandomValidator(r, k, ctx)
if !ok {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "unable to pick validator"), nil, nil
}
destAddr := destVal.GetOperator()
if srcAddr.Equals(destAddr) || destVal.InvalidExRate() || k.HasMaxRedelegationEntries(ctx, delAddr, srcAddr, destAddr) {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "checks failed"), nil, nil
}
totalBond := srcVal.TokensFromShares(delegation.GetShares()).TruncateInt()
if !totalBond.IsPositive() {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "total bond is negative"), nil, nil
}
redAmt, err := simtypes.RandPositiveInt(r, totalBond)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "unable to generate positive amount"), nil, err
}
if redAmt.IsZero() {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "amount is zero"), nil, nil
}
// check if the shares truncate to zero
shares, err := srcVal.SharesFromTokens(redAmt)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "invalid shares"), nil, err
}
if srcVal.TokensFromShares(shares).TruncateInt().IsZero() {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "shares truncate to zero"), nil, nil // skip
}
// need to retrieve the simulation account associated with delegation to retrieve PrivKey
var simAccount simtypes.Account
for _, simAcc := range accs {
if simAcc.Address.Equals(delAddr) {
simAccount = simAcc
break
}
}
// if simaccount.PrivKey == nil, delegation address does not exist in accs. Return error
if simAccount.PrivKey == nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgBeginRedelegate, "account private key is nil"), nil, fmt.Errorf("delegation addr: %s does not exist in simulation accounts", delAddr)
}
account := ak.GetAccount(ctx, delAddr)
spendable := bk.SpendableCoins(ctx, account.GetAddress())
msg := types.NewMsgBeginRedelegate(
delAddr, srcAddr, destAddr,
sdk.NewCoin(k.BondDenom(ctx), redAmt),
)
txCtx := simulation.OperationInput{
R: r,
App: app,
TxGen: simappparams.MakeTestEncodingConfig().TxConfig,
Cdc: nil,
Msg: msg,
MsgType: msg.Type(),
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
Bankkeeper: bk,
ModuleName: types.ModuleName,
CoinsSpentInMsg: spendable,
}
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}