Add committee simulations (#431)

* first pass at genesis and msgs

* add proposal generation

* add permission generation

* add decoder

* add invariants

* add committee change proposal generator

* improve committee change proposal generation

* fix error formatting

* update sims to v0.38

* Update x/committee/keeper/invariants.go

Co-Authored-By: Denali Marsh <denali@kava.io>

* Update x/committee/keeper/invariants.go

Co-Authored-By: Denali Marsh <denali@kava.io>

* tidy up comments

* tidy up random helpers

* add committee to ImportExport test

* add member check to vote invariant

* fix comment wording

Co-authored-by: Kevin Davis <karzak@users.noreply.github.com>

Co-authored-by: Denali Marsh <denali@kava.io>
Co-authored-by: Kevin Davis <karzak@users.noreply.github.com>
This commit is contained in:
Ruaridh 2020-05-04 22:17:20 +01:00 committed by GitHub
parent 471565e360
commit 23a5c7b969
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 836 additions and 46 deletions

View File

@ -13,4 +13,5 @@ const (
DefaultWeightMsgUpdatePrices int = 50
DefaultWeightMsgCdp int = 100
DefaultWeightMsgClaimReward int = 50
OpWeightSubmitCommitteeChangeProposal int = 50
)

View File

@ -8,6 +8,10 @@ import (
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
@ -23,13 +27,10 @@ import (
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/cosmos-sdk/x/supply"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
dbm "github.com/tendermint/tm-db"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
@ -178,6 +179,7 @@ func TestAppImportExport(t *testing.T) {
{app.keys[kavadist.StoreKey], newApp.keys[kavadist.StoreKey], [][]byte{}},
{app.keys[pricefeed.StoreKey], newApp.keys[pricefeed.StoreKey], [][]byte{}},
{app.keys[validatorvesting.StoreKey], newApp.keys[validatorvesting.StoreKey], [][]byte{}},
{app.keys[committee.StoreKey], newApp.keys[committee.StoreKey], [][]byte{}},
}
for _, skp := range storeKeysPrefixes {

View File

@ -43,6 +43,10 @@ var (
// function aliases
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
RegisterInvariants = keeper.RegisterInvariants
ValidCommitteesInvariant = keeper.ValidCommitteesInvariant
ValidProposalsInvariant = keeper.ValidProposalsInvariant
ValidVotesInvariant = keeper.ValidVotesInvariant
DefaultGenesisState = types.DefaultGenesisState
GetKeyFromID = types.GetKeyFromID
GetVoteKey = types.GetVoteKey

View File

@ -0,0 +1,145 @@
package keeper
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/committee/types"
)
// RegisterInvariants registers all committee invariants
func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) {
ir.RegisterRoute(types.ModuleName, "valid-committees",
ValidCommitteesInvariant(k))
ir.RegisterRoute(types.ModuleName, "valid-proposals",
ValidProposalsInvariant(k))
ir.RegisterRoute(types.ModuleName, "valid-votes",
ValidVotesInvariant(k))
}
// ValidCommitteesInvariant verifies that all committees in the store are independently valid
func ValidCommitteesInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var validationErr error
var invalidCommittee types.Committee
k.IterateCommittees(ctx, func(com types.Committee) bool {
if err := com.Validate(); err != nil {
validationErr = err
invalidCommittee = com
return true
}
return false
})
broken := validationErr != nil
invariantMessage := sdk.FormatInvariant(
types.ModuleName,
"valid committees",
fmt.Sprintf(
"\tfound invalid committee, reason: %s\n"+
"\tcommittee:\n\t%+v\n",
validationErr, invalidCommittee),
)
return invariantMessage, broken
}
}
// ValidProposalsInvariant verifies that all proposals in the store are valid
func ValidProposalsInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var validationErr error
var invalidProposal types.Proposal
k.IterateProposals(ctx, func(proposal types.Proposal) bool {
invalidProposal = proposal
if err := proposal.PubProposal.ValidateBasic(); err != nil {
validationErr = err
return true
}
currentTime := ctx.BlockTime()
if !currentTime.Equal(time.Time{}) { // this avoids a simulator bug where app.InitGenesis is called with blockTime=0 instead of the correct time
if proposal.Deadline.Before(currentTime) {
validationErr = fmt.Errorf("deadline after current block time %s", currentTime)
return true
}
}
com, found := k.GetCommittee(ctx, proposal.CommitteeID)
if !found {
validationErr = fmt.Errorf("proposal has no committee %d", proposal.CommitteeID)
return true
}
if !com.HasPermissionsFor(proposal.PubProposal) {
validationErr = fmt.Errorf("proposal not permitted for committee %+v", com)
return true
}
return false
})
broken := validationErr != nil
invariantMessage := sdk.FormatInvariant(
types.ModuleName,
"valid proposals",
fmt.Sprintf(
"\tfound invalid proposal, reason: %s\n"+
"\tproposal:\n\t%s\n",
validationErr, invalidProposal),
)
return invariantMessage, broken
}
}
// ValidVotesInvariant verifies that all votes in the store are valid
func ValidVotesInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var validationErr error
var invalidVote types.Vote
k.IterateVotes(ctx, func(vote types.Vote) bool {
invalidVote = vote
if vote.Voter.Empty() {
validationErr = fmt.Errorf("empty voter address")
return true
}
proposal, found := k.GetProposal(ctx, vote.ProposalID)
if !found {
validationErr = fmt.Errorf("vote has no proposal %d", vote.ProposalID)
return true
}
com, found := k.GetCommittee(ctx, proposal.CommitteeID)
if !found {
validationErr = fmt.Errorf("vote's proposal has no committee %d", proposal.CommitteeID)
return true
}
if !com.HasMember(vote.Voter) {
validationErr = fmt.Errorf("voter is not a member of committee %+v", com)
return true
}
return false
})
broken := validationErr != nil
invariantMessage := sdk.FormatInvariant(
types.ModuleName,
"valid votes",
fmt.Sprintf(
"\tfound invalid vote, reason: %s\n"+
"\tvote:\n\t%+v\n",
validationErr, invalidVote),
)
return invariantMessage, broken
}
}

View File

@ -95,7 +95,9 @@ func (AppModule) Name() string {
}
// RegisterInvariants register module invariants
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
RegisterInvariants(ir, am.keeper)
}
// Route module message route name
func (AppModule) Route() string {
@ -149,12 +151,12 @@ func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
simulation.RandomizedGenState(simState)
}
// TODO
func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
return nil
// ProposalContents returns functions that generate gov proposals for the module
func (am AppModule) ProposalContents(simState module.SimulationState) []sim.WeightedProposalContent {
return simulation.ProposalContents(am.keeper, simState.ParamChanges)
}
// RandomizedParams returns functions that generate params for the module.
// RandomizedParams returns functions that generate params for the module
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
return nil
}
@ -164,7 +166,7 @@ func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
sdr[StoreKey] = simulation.DecodeStore
}
// WeightedOperations returns the all the auction module operations with their respective weights.
// WeightedOperations returns the module operations for use in simulations
func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
return nil // TODO simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper)
return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper, simState.Contents)
}

View File

@ -0,0 +1,71 @@
package simulation
import (
"fmt"
"math/big"
"math/rand"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/simulation"
)
func RandomAddresses(r *rand.Rand, accs []simulation.Account) []sdk.AccAddress {
r.Shuffle(len(accs), func(i, j int) {
accs[i], accs[j] = accs[j], accs[i]
})
var addresses []sdk.AccAddress
numAddresses := r.Intn(len(accs) + 1)
for i := 0; i < numAddresses; i++ {
addresses = append(addresses, accs[i].Address)
}
return addresses
}
func RandomPositiveDuration(r *rand.Rand, inclusiveMin, exclusiveMax time.Duration) (time.Duration, error) {
min := int64(inclusiveMin)
max := int64(exclusiveMax)
if min < 0 || max < 0 {
return 0, fmt.Errorf("min and max must be positive")
}
if min >= max {
return 0, fmt.Errorf("max must be < min")
}
randPositiveInt64 := r.Int63n(max-min) + min
return time.Duration(randPositiveInt64), nil
}
func RandomTime(r *rand.Rand, inclusiveMin, exclusiveMax time.Time) (time.Time, error) {
if exclusiveMax.Before(inclusiveMin) {
return time.Time{}, fmt.Errorf("max must be > min")
}
period := exclusiveMax.Sub(inclusiveMin)
subPeriod, err := RandomPositiveDuration(r, 0, period)
if err != nil {
return time.Time{}, err
}
return inclusiveMin.Add(subPeriod), nil
}
// RandInt randomly generates an sdk.Int in the range [inclusiveMin, inclusiveMax]. It works for negative and positive integers.
func RandIntInclusive(r *rand.Rand, inclusiveMin, inclusiveMax sdk.Int) (sdk.Int, error) {
if inclusiveMin.GT(inclusiveMax) {
return sdk.Int{}, fmt.Errorf("min larger than max")
}
return RandInt(r, inclusiveMin, inclusiveMax.Add(sdk.OneInt()))
}
// RandInt randomly generates an sdk.Int in the range [inclusiveMin, exclusiveMax). It works for negative and positive integers.
func RandInt(r *rand.Rand, inclusiveMin, exclusiveMax sdk.Int) (sdk.Int, error) {
// validate input
if inclusiveMin.GTE(exclusiveMax) {
return sdk.Int{}, fmt.Errorf("min larger or equal to max")
}
// shift the range to start at 0
shiftedRange := exclusiveMax.Sub(inclusiveMin) // should always be positive given the check above
// randomly pick from the shifted range
shiftedRandInt := sdk.NewIntFromBigInt(new(big.Int).Rand(r, shiftedRange.BigInt()))
// shift back to the original range
return shiftedRandInt.Add(inclusiveMin), nil
}

View File

@ -1,13 +1,43 @@
package simulation
import (
"bytes"
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/tendermint/tendermint/libs/kv"
"github.com/kava-labs/kava/x/committee/types"
)
// DecodeStore unmarshals the KVPair's Value to the corresponding module type
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
// TODO implement this
return ""
switch {
case bytes.Equal(kvA.Key[:1], types.CommitteeKeyPrefix):
var committeeA, committeeB types.Committee
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &committeeA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &committeeB)
return fmt.Sprintf("%v\n%v", committeeA, committeeB)
case bytes.Equal(kvA.Key[:1], types.ProposalKeyPrefix):
var proposalA, proposalB types.Proposal
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &proposalA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &proposalB)
return fmt.Sprintf("%v\n%v", proposalA, proposalB)
case bytes.Equal(kvA.Key[:1], types.VoteKeyPrefix):
var voteA, voteB types.Vote
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &voteA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &voteB)
return fmt.Sprintf("%v\n%v", voteA, voteB)
case bytes.Equal(kvA.Key[:1], types.NextProposalIDKey):
proposalIDA := types.Uint64FromBytes(kvA.Value)
proposalIDB := types.Uint64FromBytes(kvB.Value)
return fmt.Sprintf("%d\n%d", proposalIDA, proposalIDB)
default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
}
}

View File

@ -0,0 +1,77 @@
package simulation
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/libs/kv"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/kava-labs/kava/x/committee/types"
)
func makeTestCodec() (cdc *codec.Codec) {
cdc = codec.New()
sdk.RegisterCodec(cdc)
govtypes.RegisterCodec(cdc)
types.RegisterCodec(cdc)
return cdc
}
func TestDecodeStore(t *testing.T) {
cdc := makeTestCodec()
committee := types.NewCommittee(
12,
"This committee is for testing.",
nil,
[]types.Permission{types.TextPermission{}},
sdk.MustNewDecFromStr("0.667"),
time.Hour*24*7,
)
proposal := types.Proposal{
ID: 34,
CommitteeID: 12,
Deadline: time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC),
PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."),
}
vote := types.Vote{
ProposalID: 9,
Voter: nil,
}
kvPairs := kv.Pairs{
kv.Pair{Key: types.CommitteeKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&committee)},
kv.Pair{Key: types.ProposalKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&proposal)},
kv.Pair{Key: types.VoteKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&vote)},
kv.Pair{Key: types.NextProposalIDKey, Value: sdk.Uint64ToBigEndian(10)},
kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
}
tests := []struct {
name string
expectedLog string
}{
{"Committee", fmt.Sprintf("%v\n%v", committee, committee)},
{"Proposal", fmt.Sprintf("%v\n%v", proposal, proposal)},
{"Vote", fmt.Sprintf("%v\n%v", vote, vote)},
{"NextProposalID", "10\n10"},
{"other", ""},
}
for i, tt := range tests {
i, tt := i, tt
t.Run(tt.name, func(t *testing.T) {
switch i {
case len(tests) - 1:
require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
default:
require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
}
})
}
}

View File

@ -2,21 +2,122 @@ package simulation
import (
"fmt"
"math/rand"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/committee/types"
)
const (
// Block time params are un-exported constants in cosmos-sdk/x/simulation.
// Copy them here in lieu of importing them.
minTimePerBlock time.Duration = (10000 / 2) * time.Second
maxTimePerBlock time.Duration = 10000 * time.Second
// Calculate the average block time
AverageBlockTime time.Duration = (maxTimePerBlock - minTimePerBlock) / 2
FallbackCommitteeID uint64 = 0
)
// RandomizedGenState generates a random GenesisState for the module
func RandomizedGenState(simState *module.SimulationState) {
r := simState.Rand
// TODO implement this fully
// - randomly generating the genesis params
// - overwriting with genesis provided to simulation
genesisState := types.DefaultGenesisState()
// Create an always present committee with god permissions to ensure any randomly generated proposal can always be submitted.
// Without this, proposals can often not be submitted as there aren't any committees with the right set of permissions available.
// It provides more control over how often different proposal types happen during simulation.
// It also makes the code simpler--proposals can just be randomly generated and submitted without having to comply to permissions that happen to be available at the time.
fallbackCommittee := types.NewCommittee(
FallbackCommitteeID,
"A committee with god permissions that will always be in state and not deleted. It ensures any generated proposal can always be submitted and passed.",
RandomAddresses(r, simState.Accounts),
[]types.Permission{types.GodPermission{}},
sdk.MustNewDecFromStr("0.5"),
AverageBlockTime*10,
)
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, genesisState))
// Create other committees
numCommittees := r.Intn(100)
committees := []types.Committee{fallbackCommittee}
for i := 0; i < numCommittees; i++ {
com, err := RandomCommittee(r, firstNAccounts(25, simState.Accounts), paramChangeToAllowedParams(simState.ParamChanges))
if err != nil {
panic(err)
}
committees = append(committees, com)
}
// Add genesis state to simState
genesisState := types.NewGenesisState(
types.DefaultNextProposalID,
committees,
[]types.Proposal{},
[]types.Vote{},
)
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, []byte{})
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesisState)
}
func RandomCommittee(r *rand.Rand, availableAccs []simulation.Account, allowedParams []types.AllowedParam) (types.Committee, error) {
// pick committee members
if len(availableAccs) < 1 {
return types.Committee{}, fmt.Errorf("must be ≥ 1 addresses")
}
var members []sdk.AccAddress
for len(members) < 1 {
members = RandomAddresses(r, availableAccs)
}
// pick proposal duration
dur, err := RandomPositiveDuration(r, 0, AverageBlockTime*10)
if err != nil {
return types.Committee{}, err
}
// pick committee vote threshold, must be in interval (0,1]
threshold := simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1").Sub(sdk.SmallestDec())).Add(sdk.SmallestDec())
return types.NewCommittee(
r.Uint64(), // could collide with other committees, but unlikely
simulation.RandStringOfLength(r, r.Intn(types.MaxCommitteeDescriptionLength+1)),
members,
RandomPermissions(r, allowedParams),
threshold,
dur,
), nil
}
func RandomPermissions(r *rand.Rand, allowedParams []types.AllowedParam) []types.Permission {
var permissions []types.Permission
if r.Intn(100) < 50 {
permissions = append(permissions, types.TextPermission{})
}
if r.Intn(100) < 50 {
r.Shuffle(len(allowedParams), func(i, j int) {
allowedParams[i], allowedParams[j] = allowedParams[j], allowedParams[i]
})
permissions = append(permissions,
types.ParamChangePermission{
AllowedParams: allowedParams[:r.Intn(len(allowedParams)+1)],
})
}
return permissions
}
func paramChangeToAllowedParams(paramChanges []simulation.ParamChange) []types.AllowedParam {
var allowedParams []types.AllowedParam
for _, pc := range paramChanges {
allowedParams = append(
allowedParams,
types.AllowedParam{
Subspace: pc.Subspace,
Key: pc.Key,
},
)
}
return allowedParams
}

View File

@ -0,0 +1,199 @@
package simulation
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/committee/keeper"
"github.com/kava-labs/kava/x/committee/types"
)
var (
proposalPassPercentage = 0.9
)
type AccountKeeper interface {
GetAccount(sdk.Context, sdk.AccAddress) authexported.Account
}
// WeightedOperations creates an operation (with weight) for each type of proposal generator.
// Custom proposal generators can be added for more control over types of proposal submitted, eg to increase likelyhood of particular cdp param changes.
func WeightedOperations(appParams simulation.AppParams, cdc *codec.Codec, ak AccountKeeper,
k keeper.Keeper, wContents []simulation.WeightedProposalContent) simulation.WeightedOperations {
var wops simulation.WeightedOperations
for _, wContent := range wContents {
wContent := wContent // pin variable
if wContent.AppParamsKey == OpWeightSubmitCommitteeChangeProposal {
// don't include committee change/delete proposals as they're not enabled for submission to committees
continue
}
var weight int
// TODO this doesn't allow weights to be different from what they are in the gov module
appParams.GetOrGenerate(cdc, wContent.AppParamsKey, &weight, nil,
func(_ *rand.Rand) { weight = wContent.DefaultWeight })
wops = append(
wops,
simulation.NewWeightedOperation(
weight,
SimulateMsgSubmitProposal(ak, k, wContent.ContentSimulatorFn),
),
)
}
return wops
}
// SimulateMsgSubmitProposal creates a proposal using the passed contentSimulatorFn and tries to find a committee that has permissions for it. If it can't then it uses the fallback committee.
// If the fallback committee isn't there (eg when using an non-generated genesis) and no committee can be found this emits a no-op msg and doesn't do anything.
// For each submit proposal msg, future ops for the vote messages are generated. Sometimes it doesn't run enough votes to allow the proposal to timeout - the likelihood of this happening is controlled by a parameter.
func SimulateMsgSubmitProposal(ak AccountKeeper, k keeper.Keeper, contentSim simulation.ContentSimulatorFn) simulation.Operation {
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string) (simulation.OperationMsg, []simulation.FutureOperation, error) {
// 1) Send a submit proposal msg
committees := k.GetCommittees(ctx)
// shuffle committees to ensure proposals are distributed across them evenly
r.Shuffle(len(committees), func(i, j int) {
committees[i], committees[j] = committees[j], committees[i]
})
// move fallback committee to the end of slice
for i, c := range committees {
if c.ID == FallbackCommitteeID {
// switch places with last element
committees[i], committees[len(committees)-1] = committees[len(committees)-1], committees[i]
}
}
// pick a committee that has permissions for proposal
pp := types.PubProposal(contentSim(r, ctx, accs))
var selectedCommittee types.Committee
var found bool
for _, c := range committees {
if c.HasPermissionsFor(pp) {
selectedCommittee = c
found = true
break
}
}
if !found {
// fallback committee was not present, this should only happen if not using the generated genesis state
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation (no committee has permissions for proposal)", "", false, nil), nil, nil
}
// create the msg and tx
proposer := selectedCommittee.Members[r.Intn(len(selectedCommittee.Members))] // won't panic as committees must have ≥ 1 members
msg := types.NewMsgSubmitProposal(
pp,
proposer,
selectedCommittee.ID,
)
account := ak.GetAccount(ctx, proposer)
fees, err := simulation.RandomFees(r, ctx, account.SpendableCoins(ctx.BlockTime()))
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
proposerAcc, found := simulation.FindAccount(accs, proposer)
if !found {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("address not in account list")
}
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{account.GetAccountNumber()},
[]uint64{account.GetSequence()},
proposerAcc.PrivKey,
)
// submit tx
_, result, err := app.Deliver(tx)
if err != nil {
// to aid debugging, add the stack trace to the comment field of the returned opMsg
return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
}
// to aid debugging, add the result log to the comment field
submitOpMsg := simulation.NewOperationMsg(msg, true, result.Log)
// 2) Schedule vote operations
// get submitted proposal
proposalID := types.Uint64FromBytes(result.Data)
proposal, found := k.GetProposal(ctx, proposalID)
if !found {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("can't find proposal with ID '%d'", proposalID)
}
// pick the voters
// num voters determined by whether the proposal should pass or not
numMembers := int64(len(selectedCommittee.Members))
majority := selectedCommittee.VoteThreshold.Mul(sdk.NewInt(numMembers).ToDec()).Ceil().TruncateInt64()
numVoters := r.Int63n(majority) // in interval [0, majority)
shouldPass := r.Float64() < proposalPassPercentage
if shouldPass {
numVoters = majority + r.Int63n(numMembers-majority+1) // in interval [majority, numMembers]
}
voters := selectedCommittee.Members[:numVoters]
// schedule vote operations
var futureOps []simulation.FutureOperation
for _, v := range voters {
voteTime, err := RandomTime(r, ctx.BlockTime(), proposal.Deadline)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("random time generation failed: %w", err)
}
fop := simulation.FutureOperation{
BlockTime: voteTime,
Op: SimulateMsgVote(k, ak, v, proposal.ID),
}
futureOps = append(futureOps, fop)
}
return submitOpMsg, futureOps, nil
}
}
func SimulateMsgVote(k keeper.Keeper, ak AccountKeeper, voter sdk.AccAddress, proposalID uint64) simulation.Operation {
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string) (
opMsg simulation.OperationMsg, fOps []simulation.FutureOperation, err error) {
msg := types.NewMsgVote(voter, proposalID)
account := ak.GetAccount(ctx, voter)
fees, err := simulation.RandomFees(r, ctx, account.SpendableCoins(ctx.BlockTime()))
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
voterAcc, found := simulation.FindAccount(accs, voter)
if !found {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("address not in account list")
}
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{account.GetAccountNumber()},
[]uint64{account.GetSequence()},
voterAcc.PrivKey,
)
_, result, err := app.Deliver(tx)
if err != nil {
// to aid debugging, add the stack trace to the comment field of the returned opMsg
return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
}
// to aid debugging, add the result log to the comment field
return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
}
}

View File

@ -1,14 +0,0 @@
package simulation
import (
"math/rand"
"github.com/cosmos/cosmos-sdk/x/simulation"
)
// ParamChanges defines the parameters that can be modified by param change proposals
// on the simulation
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
// TODO implement this
return []simulation.ParamChange{}
}

View File

@ -0,0 +1,173 @@
package simulation
import (
"fmt"
"math/rand"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/cosmos/cosmos-sdk/x/simulation"
appparams "github.com/kava-labs/kava/app/params"
"github.com/kava-labs/kava/x/committee/keeper"
"github.com/kava-labs/kava/x/committee/types"
)
const OpWeightSubmitCommitteeChangeProposal = "op_weight_submit_committee_change_proposal"
// ProposalContents defines the module weighted proposals' contents
func ProposalContents(k keeper.Keeper, paramChanges []simulation.ParamChange) []simulation.WeightedProposalContent {
return []simulation.WeightedProposalContent{
{
AppParamsKey: OpWeightSubmitCommitteeChangeProposal,
DefaultWeight: appparams.OpWeightSubmitCommitteeChangeProposal,
ContentSimulatorFn: SimulateCommitteeChangeProposalContent(k, paramChanges),
},
}
}
// SimulateCommitteeChangeProposalContent generates gov proposal contents that either:
// - create new committees
// - change existing committees
// - delete committees
// It does not alter the fallback committee.
func SimulateCommitteeChangeProposalContent(k keeper.Keeper, paramChanges []simulation.ParamChange) simulation.ContentSimulatorFn {
return func(r *rand.Rand, ctx sdk.Context, accs []simulation.Account) govtypes.Content {
allowedParams := paramChangeToAllowedParams(paramChanges)
// get current committees, ignoring the fallback committee
var committees []types.Committee
k.IterateCommittees(ctx, func(com types.Committee) bool {
if com.ID != FallbackCommitteeID {
committees = append(committees, com)
}
return false
})
if len(committees) < 1 { // create a committee if none exist
com, err := RandomCommittee(r, firstNAccounts(25, accs), allowedParams) // limit num members to avoid overflowing hardcoded gov ops gas limit
if err != nil {
panic(err)
}
return types.NewCommitteeChangeProposal(
simulation.RandStringOfLength(r, 10),
simulation.RandStringOfLength(r, 100),
com,
)
}
// create a proposal content
var content govtypes.Content
switch choice := r.Intn(100); {
// create committee
case choice < 20:
com, err := RandomCommittee(r, firstNAccounts(25, accs), allowedParams) // limit num members to avoid overflowing hardcoded gov ops gas limit
if err != nil {
panic(err)
}
content = types.NewCommitteeChangeProposal(
simulation.RandStringOfLength(r, 10),
simulation.RandStringOfLength(r, 100),
com,
)
// update committee
case choice < 80:
com := committees[r.Intn(len(committees))]
// update members
if r.Intn(100) < 50 {
if len(accs) == 0 {
panic("must have at least one account availabel to use as committee member")
}
var members []sdk.AccAddress
for len(members) < 1 {
members = RandomAddresses(r, firstNAccounts(25, accs)) // limit num members to avoid overflowing hardcoded gov ops gas limit
}
com.Members = members
}
// update permissions
if r.Intn(100) < 50 {
com.Permissions = RandomPermissions(r, allowedParams)
}
// update proposal duration
if r.Intn(100) < 50 {
dur, err := RandomPositiveDuration(r, 0, AverageBlockTime*100)
if err != nil {
panic(err)
}
com.ProposalDuration = dur
}
// update vote threshold
if r.Intn(100) < 50 {
// VoteThreshold must be in interval (0,1]
com.VoteThreshold = simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1").Sub(sdk.SmallestDec())).Add(sdk.SmallestDec())
}
content = types.NewCommitteeChangeProposal(
simulation.RandStringOfLength(r, 10),
simulation.RandStringOfLength(r, 100),
com,
)
// delete committee
default:
com := committees[r.Intn(len(committees))]
content = types.NewCommitteeDeleteProposal(
simulation.RandStringOfLength(r, 10),
simulation.RandStringOfLength(r, 100),
com.ID,
)
}
return content
}
}
/*
// Example custom ParamChangeProposal generator to only generate change to interesting cdp params.
// This allows more control over what params are changed within a simulation.
func SimulateCDPParamChangeProposalContent(cdpKeeper cdpkeeper.Keeper, paramChangePool []simulation.ParamChange) simulation.ContentSimulatorFn {
return func(r *rand.Rand, ctx sdk.Context, _ []simulation.Account) govtypes.Content {
var paramChanges []paramstypes.ParamChange
// alter sub params
cp := cdpKeeper.GetParams(ctx).CollateralParams
if len(cp) == 0 {
return nil
}
cp[0].StabilityFee = sdk.MustNewDecFromStr("0.000001") // TODO generate
paramChanges = append(
paramChanges,
paramstypes.NewParamChange(cdptypes.ModuleName, "?", string(cdptypes.ModuleCdc.MustMarshalJSON(cp))),
)
// alter normal param
for _, pc := range paramChangePool {
if pc.Subspace == cdptypes.ModuleName && pc.Key == string(cdptypes.KeyGlobalDebtLimit) {
paramChanges = append(
paramChanges,
paramstypes.NewParamChange(pc.Subspace, pc.Key, pc.SimValue(r)),
)
}
}
return paramstypes.NewParameterChangeProposal(
simulation.RandStringOfLength(r, 140), // title
simulation.RandStringOfLength(r, 5000), // description
paramChanges, // set of changes
)
}
}
*/
func firstNAccounts(n int, accs []simulation.Account) []simulation.Account {
if n < 0 {
panic(fmt.Sprintf("n must be ≥ 0"))
}
if n > len(accs) {
return accs
}
return accs[:n]
}

View File

@ -63,31 +63,30 @@ func (c Committee) Validate() error {
for _, m := range c.Members {
// check there are no duplicate members
if _, ok := addressMap[m.String()]; ok {
return fmt.Errorf("duplicate member found in committee, %s", m)
return fmt.Errorf("committe cannot have duplicate members, %s", m)
}
// check for valid addresses
if m.Empty() {
return fmt.Errorf("committee %d invalid: found empty member address", c.ID)
return fmt.Errorf("committee cannot have empty member address")
}
addressMap[m.String()] = true
}
if len(c.Members) == 0 {
return fmt.Errorf("committee %d invalid: cannot have zero members", c.ID)
return fmt.Errorf("committee cannot have zero members")
}
if len(c.Description) > MaxCommitteeDescriptionLength {
return fmt.Errorf("invalid description")
return fmt.Errorf("description length %d longer than max allowed %d", len(c.Description), MaxCommitteeDescriptionLength)
}
// threshold must be in the range (0,1]
if c.VoteThreshold.IsNil() || c.VoteThreshold.LTE(sdk.ZeroDec()) || c.VoteThreshold.GT(sdk.NewDec(1)) {
return fmt.Errorf("invalid threshold")
return fmt.Errorf("invalid threshold: %s", c.VoteThreshold)
}
if c.ProposalDuration < 0 {
return fmt.Errorf("invalid proposal duration")
return fmt.Errorf("invalid proposal duration: %s", c.ProposalDuration)
}
return nil

View File

@ -65,7 +65,7 @@ func (gs GenesisState) Validate() error {
}
}
// validate proposals - pp.Val, no duplicate IDs, no ids >= nextID, committee needs to exist
// validate proposals
proposalMap := make(map[uint64]bool, len(gs.Proposals))
for _, p := range gs.Proposals {
// check there are no duplicate IDs