mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-20 15:05:21 +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
@ -8,9 +8,10 @@ const (
|
||||
|
||||
// Default simulation operation weights for messages and gov proposals
|
||||
const (
|
||||
DefaultWeightMsgPlaceBid int = 75
|
||||
DefaultWeightMsgCreateAtomicSwap int = 50
|
||||
DefaultWeightMsgUpdatePrices int = 50
|
||||
DefaultWeightMsgCdp int = 100
|
||||
DefaultWeightMsgClaimReward int = 50
|
||||
DefaultWeightMsgPlaceBid int = 75
|
||||
DefaultWeightMsgCreateAtomicSwap int = 50
|
||||
DefaultWeightMsgUpdatePrices int = 50
|
||||
DefaultWeightMsgCdp int = 100
|
||||
DefaultWeightMsgClaimReward int = 50
|
||||
OpWeightSubmitCommitteeChangeProposal int = 50
|
||||
)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
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
|
||||
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)
|
||||
}
|
||||
|
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
|
||||
|
||||
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]))
|
||||
}
|
||||
}
|
||||
|
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 (
|
||||
"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
|
||||
}
|
||||
|
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 {
|
||||
// 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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user