add committee change gov proposals

This commit is contained in:
rhuairahrighairigh 2020-03-21 18:06:58 +00:00
parent 18dfcd2a3d
commit fbf67b4527
9 changed files with 522 additions and 51 deletions

View File

@ -8,40 +8,45 @@ import (
) )
const ( const (
AttributeKeyProposalID = types.AttributeKeyProposalID AttributeKeyProposalID = types.AttributeKeyProposalID
DefaultNextProposalID = types.DefaultNextProposalID DefaultCodespace = types.DefaultCodespace
DefaultParamspace = types.DefaultParamspace DefaultNextProposalID = types.DefaultNextProposalID
EventTypeSubmitProposal = types.EventTypeSubmitProposal DefaultParamspace = types.DefaultParamspace
ModuleName = types.ModuleName EventTypeSubmitProposal = types.EventTypeSubmitProposal
QuerierRoute = types.QuerierRoute ModuleName = types.ModuleName
QueryCommittee = types.QueryCommittee ProposalTypeCommitteeChange = types.ProposalTypeCommitteeChange
QueryCommittees = types.QueryCommittees ProposalTypeCommitteeDelete = types.ProposalTypeCommitteeDelete
QueryProposal = types.QueryProposal QuerierRoute = types.QuerierRoute
QueryProposals = types.QueryProposals QueryCommittee = types.QueryCommittee
QueryTally = types.QueryTally QueryCommittees = types.QueryCommittees
QueryVote = types.QueryVote QueryProposal = types.QueryProposal
QueryVotes = types.QueryVotes QueryProposals = types.QueryProposals
RouterKey = types.RouterKey QueryTally = types.QueryTally
StoreKey = types.StoreKey QueryVote = types.QueryVote
TypeMsgSubmitProposal = types.TypeMsgSubmitProposal QueryVotes = types.QueryVotes
TypeMsgVote = types.TypeMsgVote RouterKey = types.RouterKey
StoreKey = types.StoreKey
TypeMsgSubmitProposal = types.TypeMsgSubmitProposal
TypeMsgVote = types.TypeMsgVote
) )
var ( var (
// function aliases // function aliases
NewKeeper = keeper.NewKeeper NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier NewQuerier = keeper.NewQuerier
DefaultGenesisState = types.DefaultGenesisState DefaultGenesisState = types.DefaultGenesisState
GetKeyFromID = types.GetKeyFromID GetKeyFromID = types.GetKeyFromID
GetVoteKey = types.GetVoteKey GetVoteKey = types.GetVoteKey
NewGenesisState = types.NewGenesisState NewCommitteeChangeProposal = types.NewCommitteeChangeProposal
NewMsgSubmitProposal = types.NewMsgSubmitProposal NewCommitteeDeleteProposal = types.NewCommitteeDeleteProposal
NewMsgVote = types.NewMsgVote NewGenesisState = types.NewGenesisState
NewQueryCommitteeParams = types.NewQueryCommitteeParams NewMsgSubmitProposal = types.NewMsgSubmitProposal
NewQueryProposalParams = types.NewQueryProposalParams NewMsgVote = types.NewMsgVote
NewQueryVoteParams = types.NewQueryVoteParams NewQueryCommitteeParams = types.NewQueryCommitteeParams
RegisterCodec = types.RegisterCodec NewQueryProposalParams = types.NewQueryProposalParams
Uint64FromBytes = types.Uint64FromBytes NewQueryVoteParams = types.NewQueryVoteParams
RegisterCodec = types.RegisterCodec
Uint64FromBytes = types.Uint64FromBytes
// variable aliases // variable aliases
CommitteeKeyPrefix = types.CommitteeKeyPrefix CommitteeKeyPrefix = types.CommitteeKeyPrefix
@ -56,10 +61,11 @@ var (
type ( type (
Keeper = keeper.Keeper Keeper = keeper.Keeper
Committee = types.Committee Committee = types.Committee
CommitteeChangeProposal = types.CommitteeChangeProposal
CommitteeDeleteProposal = types.CommitteeDeleteProposal
GeneralShutdownPermission = types.GeneralShutdownPermission GeneralShutdownPermission = types.GeneralShutdownPermission
GenesisState = types.GenesisState GenesisState = types.GenesisState
GodPermission = types.GodPermission GodPermission = types.GodPermission
GroupChangeProposal = types.GroupChangeProposal
InflationRateChangePermission = types.InflationRateChangePermission InflationRateChangePermission = types.InflationRateChangePermission
MsgSubmitProposal = types.MsgSubmitProposal MsgSubmitProposal = types.MsgSubmitProposal
MsgVote = types.MsgVote MsgVote = types.MsgVote

View File

@ -99,11 +99,7 @@ func (k Keeper) CloseOutProposal(ctx sdk.Context, proposalID uint64) sdk.Error {
} }
if proposalPasses || pr.HasExpiredBy(ctx.BlockTime()) { if proposalPasses || pr.HasExpiredBy(ctx.BlockTime()) {
// delete proposal and votes k.DeleteProposalAndVotes(ctx, proposalID)
k.DeleteProposal(ctx, proposalID)
for _, v := range votes {
k.DeleteVote(ctx, v.ProposalID, v.Voter)
}
return nil return nil
} }
return sdk.ErrInternal("note enough votes to close proposal") return sdk.ErrInternal("note enough votes to close proposal")
@ -132,3 +128,16 @@ func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubPropos
return nil return nil
} }
func (k Keeper) DeleteProposalAndVotes(ctx sdk.Context, proposalID uint64) {
var votes []types.Vote
k.IterateVotes(ctx, proposalID, func(vote types.Vote) bool {
votes = append(votes, vote)
return false
})
k.DeleteProposal(ctx, proposalID)
for _, v := range votes {
k.DeleteVote(ctx, v.ProposalID, v.Voter)
}
}

View File

@ -1,4 +1,66 @@
package committee package committee
// TODO create a GroupChangeProposalHandler, see params or distribution import (
// It will overwrite the Members of Permissions field of a group "fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
)
func NewProposalHandler(k Keeper) govtypes.Handler {
return func(ctx sdk.Context, content govtypes.Content) sdk.Error {
switch c := content.(type) {
case CommitteeChangeProposal:
return handleCommitteeChangeProposal(ctx, k, c)
case CommitteeDeleteProposal:
return handleCommitteeDeleteProposal(ctx, k, c)
default:
errMsg := fmt.Sprintf("unrecognized %s proposal content type: %T", ModuleName, c)
return sdk.ErrUnknownRequest(errMsg)
}
}
}
func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeChangeProposal) sdk.Error {
if err := committeeProposal.ValidateBasic(); err != nil {
return err
}
// Remove all committee's ongoing proposals
var proposals []Proposal
k.IterateProposals(ctx, func(p Proposal) bool {
if p.CommitteeID == committeeProposal.NewCommittee.ID {
proposals = append(proposals, p)
}
return false
})
for _, p := range proposals { // split loops to avoid updating the db while iterating
k.DeleteProposalAndVotes(ctx, p.ID)
}
// update/create the committee
k.SetCommittee(ctx, committeeProposal.NewCommittee)
return nil
}
func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeDeleteProposal) sdk.Error {
if err := committeeProposal.ValidateBasic(); err != nil {
return err
}
// Remove all committee's ongoing proposals
var proposals []Proposal
k.IterateProposals(ctx, func(p Proposal) bool {
if p.CommitteeID == committeeProposal.CommitteeID {
proposals = append(proposals, p)
}
return false
})
for _, p := range proposals { // split loops to avoid updating the db while iterating
k.DeleteProposalAndVotes(ctx, p.ID)
}
k.DeleteCommittee(ctx, committeeProposal.CommitteeID)
return nil
}

View File

@ -0,0 +1,247 @@
package committee_test
import (
"testing"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/committee/types"
)
var testTime time.Time = time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)
func NewCommitteeGenState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState {
return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)}
}
type ProposalHandlerTestSuite struct {
suite.Suite
keeper committee.Keeper
app app.TestApp
ctx sdk.Context
addresses []sdk.AccAddress
testGenesis committee.GenesisState
}
func (suite *ProposalHandlerTestSuite) SetupTest() {
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
suite.testGenesis = committee.NewGenesisState(
2,
[]committee.Committee{
{
ID: 1,
Members: suite.addresses[:3],
Permissions: []types.Permission{types.GodPermission{}},
},
{
ID: 2,
Members: suite.addresses[2:],
Permissions: nil,
},
},
[]committee.Proposal{
{ID: 1, CommitteeID: 1, PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)},
},
[]committee.Vote{
{ProposalID: 1, Voter: suite.addresses[0]},
},
)
}
func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() {
testCases := []struct {
name string
proposal committee.CommitteeChangeProposal
expectPass bool
}{
{
name: "add new",
proposal: committee.NewCommitteeChangeProposal(
"A Title",
"A proposal description.",
committee.Committee{
ID: 34,
},
),
expectPass: true,
},
{
name: "update",
proposal: committee.NewCommitteeChangeProposal(
"A Title",
"A proposal description.",
committee.Committee{
ID: 1,
Members: suite.addresses,
Permissions: suite.testGenesis.Committees[0].Permissions,
},
),
expectPass: true,
},
{
name: "invalid title",
proposal: committee.NewCommitteeChangeProposal(
"A Title That Is Much Too Long And Really Quite Unreasonable Given That It Is Trying To Fullfill The Roll Of An Acceptable Governance Proposal Title That Should Succinctly Communicate The Goal And Contents Of The Proposed Proposal To All Parties Involved",
"A proposal description.",
committee.Committee{
ID: 34,
},
),
expectPass: false,
},
{
name: "invalid committee",
proposal: committee.NewCommitteeChangeProposal(
"A Title",
"A proposal description.",
committee.Committee{
ID: 1,
Members: append(suite.addresses, suite.addresses[0]), // duplicate address
Permissions: suite.testGenesis.Committees[0].Permissions,
},
),
expectPass: false,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Setup
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.app = suite.app.InitializeFromGenesisStates(
NewCommitteeGenState(suite.app.Codec(), suite.testGenesis),
)
suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: testTime})
handler := committee.NewProposalHandler(suite.keeper)
// get proposals and votes for target committee
var proposals []committee.Proposal
var votes []committee.Vote
suite.keeper.IterateProposals(suite.ctx, func(p committee.Proposal) bool {
if p.CommitteeID == tc.proposal.NewCommittee.ID {
proposals = append(proposals, p)
suite.keeper.IterateVotes(suite.ctx, p.ID, func(v committee.Vote) bool {
votes = append(votes, v)
return false
})
}
return false
})
// Run
err := handler(suite.ctx, tc.proposal)
// Check
if tc.expectPass {
suite.NoError(err)
// check proposal is accurate
actualCom, found := suite.keeper.GetCommittee(suite.ctx, tc.proposal.NewCommittee.ID)
suite.True(found)
suite.Equal(tc.proposal.NewCommittee, actualCom)
// check proposals and votes for this committee have been removed
for _, p := range proposals {
_, found := suite.keeper.GetProposal(suite.ctx, p.ID)
suite.False(found)
}
for _, v := range votes {
_, found := suite.keeper.GetVote(suite.ctx, v.ProposalID, v.Voter)
suite.False(found)
}
} else {
suite.Error(err)
suite.Equal(suite.testGenesis, committee.ExportGenesis(suite.ctx, suite.keeper))
}
})
}
}
func (suite *ProposalHandlerTestSuite) TestProposalHandler_DeleteCommittee() {
testCases := []struct {
name string
proposal committee.CommitteeDeleteProposal
expectPass bool
}{
{
name: "normal",
proposal: committee.NewCommitteeDeleteProposal(
"A Title",
"A proposal description.",
suite.testGenesis.Committees[0].ID,
),
expectPass: true,
},
{
name: "invalid title",
proposal: committee.NewCommitteeDeleteProposal(
"A Title That Is Much Too Long And Really Quite Unreasonable Given That It Is Trying To Fullfill The Roll Of An Acceptable Governance Proposal Title That Should Succinctly Communicate The Goal And Contents Of The Proposed Proposal To All Parties Involved",
"A proposal description.",
suite.testGenesis.Committees[1].ID,
),
expectPass: false,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Setup
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.app = suite.app.InitializeFromGenesisStates(
NewCommitteeGenState(suite.app.Codec(), suite.testGenesis),
)
suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: testTime})
handler := committee.NewProposalHandler(suite.keeper)
// get proposals and votes for target committee
var proposals []committee.Proposal
var votes []committee.Vote
suite.keeper.IterateProposals(suite.ctx, func(p committee.Proposal) bool {
if p.CommitteeID == tc.proposal.CommitteeID {
proposals = append(proposals, p)
suite.keeper.IterateVotes(suite.ctx, p.ID, func(v committee.Vote) bool {
votes = append(votes, v)
return false
})
}
return false
})
// Run
err := handler(suite.ctx, tc.proposal)
// Check
if tc.expectPass {
suite.NoError(err)
// check proposal is accurate
_, found := suite.keeper.GetCommittee(suite.ctx, tc.proposal.CommitteeID)
suite.False(found)
// check proposals and votes for this committee have been removed
for _, p := range proposals {
_, found := suite.keeper.GetProposal(suite.ctx, p.ID)
suite.False(found)
}
for _, v := range votes {
_, found := suite.keeper.GetVote(suite.ctx, v.ProposalID, v.Voter)
suite.False(found)
}
} else {
suite.Error(err)
suite.Equal(suite.testGenesis, committee.ExportGenesis(suite.ctx, suite.keeper))
}
})
}
}
func TestProposalHandlerTestSuite(t *testing.T) {
suite.Run(t, new(ProposalHandlerTestSuite))
}

View File

@ -2,6 +2,9 @@ package types
import ( import (
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/distribution"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/params"
) )
// ModuleCdc generic sealed codec to be used throughout module // ModuleCdc generic sealed codec to be used throughout module
@ -9,6 +12,13 @@ var ModuleCdc *codec.Codec
func init() { func init() {
cdc := codec.New() cdc := codec.New()
cdc.RegisterInterface((*gov.Content)(nil), nil) // registering the Content interface on the ModuleCdc will not conflict with gov.
// TODO ideally dist and params would register their proposals on here at their init. However can't change them so:
cdc.RegisterConcrete(distribution.CommunityPoolSpendProposal{}, "cosmos-sdk/CommunityPoolSpendProposal", nil)
cdc.RegisterConcrete(params.ParameterChangeProposal{}, "cosmos-sdk/ParameterChangeProposal", nil)
cdc.RegisterConcrete(gov.TextProposal{}, "cosmos-sdk/TextProposal", nil)
cdc.RegisterConcrete(gov.SoftwareUpgradeProposal{}, "cosmos-sdk/SoftwareUpgradeProposal", nil)
RegisterCodec(cdc) RegisterCodec(cdc)
ModuleCdc = cdc.Seal() ModuleCdc = cdc.Seal()
} }
@ -16,9 +26,15 @@ func init() {
// RegisterCodec registers the necessary types for the module // RegisterCodec registers the necessary types for the module
func RegisterCodec(cdc *codec.Codec) { func RegisterCodec(cdc *codec.Codec) {
// TODO need to register Content interface, however amino panics if you try and register it twice and helpfully doesn't provide a way to query registered types // The app codec needs the gov.Content type registered. This is done by the gov module.
// Ideally it would registered here as well in case these modules are ever used separately.
// However amino panics if you register the same interface a second time. So leaving it out for now.
//cdc.RegisterInterface((*gov.Content)(nil), nil) //cdc.RegisterInterface((*gov.Content)(nil), nil)
cdc.RegisterConcrete(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal", nil)
cdc.RegisterConcrete(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal", nil)
cdc.RegisterInterface((*Permission)(nil), nil) cdc.RegisterInterface((*Permission)(nil), nil)
cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil) cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil)
} }

View File

@ -0,0 +1,9 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
DefaultCodespace sdk.CodespaceType = ModuleName
)

View File

@ -0,0 +1,120 @@
package types
import (
"gopkg.in/yaml.v2"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
)
const (
ProposalTypeCommitteeChange = "CommitteeChange"
ProposalTypeCommitteeDelete = "CommitteeDelete"
)
// CommitteeChangeProposal is a gov proposal for creating a new committee or modifying an existing one.
type CommitteeChangeProposal struct {
Title string
Description string
NewCommittee Committee
}
var _ govtypes.Content = CommitteeChangeProposal{}
func init() {
govtypes.RegisterProposalType(ProposalTypeCommitteeChange)
govtypes.RegisterProposalTypeCodec(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal")
// TODO write these
//RegisterProposalType(ProposalTypeCommitteeChange)
//RegisterProposalTypeCodec(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal")
// How will we register distribution and params proposals on this codec?
}
func NewCommitteeChangeProposal(title string, description string, newCommittee Committee) CommitteeChangeProposal {
return CommitteeChangeProposal{
Title: title,
Description: description,
NewCommittee: newCommittee,
}
}
// GetTitle returns the title of the proposal.
func (ccp CommitteeChangeProposal) GetTitle() string { return ccp.Title }
// GetDescription returns the description of the proposal.
func (ccp CommitteeChangeProposal) GetDescription() string { return ccp.Description }
// GetDescription returns the routing key of the proposal.
func (ccp CommitteeChangeProposal) ProposalRoute() string { return RouterKey }
// ProposalType returns the type of the proposal.
func (ccp CommitteeChangeProposal) ProposalType() string { return ProposalTypeCommitteeChange }
// ValidateBasic runs basic stateless validity checks
func (ccp CommitteeChangeProposal) ValidateBasic() sdk.Error {
if err := govtypes.ValidateAbstract(DefaultCodespace, ccp); err != nil {
return err
}
if err := ccp.NewCommittee.Validate(); err != nil {
return err
}
return nil
}
// String implements the Stringer interface.
func (ccp CommitteeChangeProposal) String() string {
bz, _ := yaml.Marshal(ccp) // TODO test
return string(bz)
}
// CommitteeDeleteProposal is a gov proposal for removing a committee.
type CommitteeDeleteProposal struct {
Title string
Description string
CommitteeID uint64
}
var _ govtypes.Content = CommitteeDeleteProposal{}
func init() {
govtypes.RegisterProposalType(ProposalTypeCommitteeDelete)
govtypes.RegisterProposalTypeCodec(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal")
// TODO write these
//RegisterProposalType(ProposalTypeCommitteeDelete)
//RegisterProposalTypeCodec(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal")
// How will we register distribution and params proposals on this codec?
}
func NewCommitteeDeleteProposal(title string, description string, committeeID uint64) CommitteeDeleteProposal {
return CommitteeDeleteProposal{
Title: title,
Description: description,
CommitteeID: committeeID,
}
}
// GetTitle returns the title of the proposal.
func (cdp CommitteeDeleteProposal) GetTitle() string { return cdp.Title }
// GetDescription returns the description of the proposal.
func (cdp CommitteeDeleteProposal) GetDescription() string { return cdp.Description }
// GetDescription returns the routing key of the proposal.
func (cdp CommitteeDeleteProposal) ProposalRoute() string { return RouterKey }
// ProposalType returns the type of the proposal.
func (cdp CommitteeDeleteProposal) ProposalType() string { return ProposalTypeCommitteeDelete }
// ValidateBasic runs basic stateless validity checks
func (cdp CommitteeDeleteProposal) ValidateBasic() sdk.Error {
if err := govtypes.ValidateAbstract(DefaultCodespace, cdp); err != nil {
return err
}
return nil
}
// String implements the Stringer interface.
func (cdp CommitteeDeleteProposal) String() string {
bz, _ := yaml.Marshal(cdp) // TODO test
return string(bz)
}

View File

@ -1,12 +0,0 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// A gov.Proposal to used to add/remove members from a group, or to add/remove permissions.
// Normally registered with standard gov. But could also be registed with committee to allow groups to be controlled by other groups.
type GroupChangeProposal struct {
Members []sdk.AccAddress
Permissions []Permission
}

View File

@ -44,6 +44,20 @@ func (c Committee) HasPermissionsFor(proposal PubProposal) bool {
return false return false
} }
func (c Committee) Validate() sdk.Error {
// check for duplicate addresses
addressMap := make(map[string]bool, len(c.Members))
for _, m := range c.Members {
// check there are no duplicate members
if _, ok := addressMap[m.String()]; ok {
return sdk.ErrInternal(fmt.Sprintf("duplicate member found in committee, %s", m))
}
addressMap[m.String()] = true
}
return nil
}
// Permission is anything with a method that validates whether a proposal is allowed by it or not. // Permission is anything with a method that validates whether a proposal is allowed by it or not.
type Permission interface { type Permission interface {
Allows(PubProposal) bool Allows(PubProposal) bool