mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 15:55:18 +00:00
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:
parent
471565e360
commit
23a5c7b969
@ -13,4 +13,5 @@ const (
|
|||||||
DefaultWeightMsgUpdatePrices int = 50
|
DefaultWeightMsgUpdatePrices int = 50
|
||||||
DefaultWeightMsgCdp int = 100
|
DefaultWeightMsgCdp int = 100
|
||||||
DefaultWeightMsgClaimReward int = 50
|
DefaultWeightMsgClaimReward int = 50
|
||||||
|
OpWeightSubmitCommitteeChangeProposal int = 50
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,10 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"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/baseapp"
|
||||||
"github.com/cosmos/cosmos-sdk/simapp"
|
"github.com/cosmos/cosmos-sdk/simapp"
|
||||||
"github.com/cosmos/cosmos-sdk/simapp/helpers"
|
"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/staking"
|
||||||
"github.com/cosmos/cosmos-sdk/x/supply"
|
"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/auction"
|
||||||
"github.com/kava-labs/kava/x/bep3"
|
"github.com/kava-labs/kava/x/bep3"
|
||||||
"github.com/kava-labs/kava/x/cdp"
|
"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/incentive"
|
||||||
"github.com/kava-labs/kava/x/kavadist"
|
"github.com/kava-labs/kava/x/kavadist"
|
||||||
"github.com/kava-labs/kava/x/pricefeed"
|
"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[kavadist.StoreKey], newApp.keys[kavadist.StoreKey], [][]byte{}},
|
||||||
{app.keys[pricefeed.StoreKey], newApp.keys[pricefeed.StoreKey], [][]byte{}},
|
{app.keys[pricefeed.StoreKey], newApp.keys[pricefeed.StoreKey], [][]byte{}},
|
||||||
{app.keys[validatorvesting.StoreKey], newApp.keys[validatorvesting.StoreKey], [][]byte{}},
|
{app.keys[validatorvesting.StoreKey], newApp.keys[validatorvesting.StoreKey], [][]byte{}},
|
||||||
|
{app.keys[committee.StoreKey], newApp.keys[committee.StoreKey], [][]byte{}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, skp := range storeKeysPrefixes {
|
for _, skp := range storeKeysPrefixes {
|
||||||
|
@ -43,6 +43,10 @@ var (
|
|||||||
// function aliases
|
// function aliases
|
||||||
NewKeeper = keeper.NewKeeper
|
NewKeeper = keeper.NewKeeper
|
||||||
NewQuerier = keeper.NewQuerier
|
NewQuerier = keeper.NewQuerier
|
||||||
|
RegisterInvariants = keeper.RegisterInvariants
|
||||||
|
ValidCommitteesInvariant = keeper.ValidCommitteesInvariant
|
||||||
|
ValidProposalsInvariant = keeper.ValidProposalsInvariant
|
||||||
|
ValidVotesInvariant = keeper.ValidVotesInvariant
|
||||||
DefaultGenesisState = types.DefaultGenesisState
|
DefaultGenesisState = types.DefaultGenesisState
|
||||||
GetKeyFromID = types.GetKeyFromID
|
GetKeyFromID = types.GetKeyFromID
|
||||||
GetVoteKey = types.GetVoteKey
|
GetVoteKey = types.GetVoteKey
|
||||||
|
145
x/committee/keeper/invariants.go
Normal file
145
x/committee/keeper/invariants.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -95,7 +95,9 @@ func (AppModule) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RegisterInvariants register module invariants
|
// 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
|
// Route module message route name
|
||||||
func (AppModule) Route() string {
|
func (AppModule) Route() string {
|
||||||
@ -149,12 +151,12 @@ func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
|
|||||||
simulation.RandomizedGenState(simState)
|
simulation.RandomizedGenState(simState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// ProposalContents returns functions that generate gov proposals for the module
|
||||||
func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
|
func (am AppModule) ProposalContents(simState module.SimulationState) []sim.WeightedProposalContent {
|
||||||
return nil
|
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 {
|
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -164,7 +166,7 @@ func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
|
|||||||
sdr[StoreKey] = simulation.DecodeStore
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
|
71
x/committee/simulation/common.go
Normal file
71
x/committee/simulation/common.go
Normal 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
|
||||||
|
}
|
@ -1,13 +1,43 @@
|
|||||||
package simulation
|
package simulation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/cosmos/cosmos-sdk/codec"
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
|
||||||
"github.com/tendermint/tendermint/libs/kv"
|
"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
|
// DecodeStore unmarshals the KVPair's Value to the corresponding module type
|
||||||
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
|
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
|
||||||
// TODO implement this
|
switch {
|
||||||
return ""
|
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]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
77
x/committee/simulation/decoder_test.go
Normal file
77
x/committee/simulation/decoder_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -2,21 +2,122 @@ package simulation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"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/types/module"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/simulation"
|
||||||
|
|
||||||
"github.com/kava-labs/kava/x/committee/types"
|
"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
|
// RandomizedGenState generates a random GenesisState for the module
|
||||||
func RandomizedGenState(simState *module.SimulationState) {
|
func RandomizedGenState(simState *module.SimulationState) {
|
||||||
|
r := simState.Rand
|
||||||
|
|
||||||
// TODO implement this fully
|
// Create an always present committee with god permissions to ensure any randomly generated proposal can always be submitted.
|
||||||
// - randomly generating the genesis params
|
// Without this, proposals can often not be submitted as there aren't any committees with the right set of permissions available.
|
||||||
// - overwriting with genesis provided to simulation
|
// It provides more control over how often different proposal types happen during simulation.
|
||||||
genesisState := types.DefaultGenesisState()
|
// 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)
|
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
|
||||||
|
}
|
||||||
|
199
x/committee/simulation/operations.go
Normal file
199
x/committee/simulation/operations.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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{}
|
|
||||||
}
|
|
173
x/committee/simulation/proposals.go
Normal file
173
x/committee/simulation/proposals.go
Normal 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]
|
||||||
|
}
|
@ -63,31 +63,30 @@ func (c Committee) Validate() error {
|
|||||||
for _, m := range c.Members {
|
for _, m := range c.Members {
|
||||||
// check there are no duplicate members
|
// check there are no duplicate members
|
||||||
if _, ok := addressMap[m.String()]; ok {
|
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
|
// check for valid addresses
|
||||||
if m.Empty() {
|
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
|
addressMap[m.String()] = true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.Members) == 0 {
|
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 {
|
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]
|
// threshold must be in the range (0,1]
|
||||||
if c.VoteThreshold.IsNil() || c.VoteThreshold.LTE(sdk.ZeroDec()) || c.VoteThreshold.GT(sdk.NewDec(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 {
|
if c.ProposalDuration < 0 {
|
||||||
return fmt.Errorf("invalid proposal duration")
|
return fmt.Errorf("invalid proposal duration: %s", c.ProposalDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -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))
|
proposalMap := make(map[uint64]bool, len(gs.Proposals))
|
||||||
for _, p := range gs.Proposals {
|
for _, p := range gs.Proposals {
|
||||||
// check there are no duplicate IDs
|
// check there are no duplicate IDs
|
||||||
|
Loading…
Reference in New Issue
Block a user