From 4989c0938a52c9cfaa17efc21016bb05b7233cac Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 4 Mar 2020 14:35:16 +0000 Subject: [PATCH 01/54] rough draft --- x/circuit-breaker/ante/ante.go | 31 +++++++++++++++++ x/circuit-breaker/gov-handler.go | 14 ++++++++ x/circuit-breaker/keeper/keeper.go | 18 ++++++++++ x/circuit-breaker/spec/README.md | 10 ++++++ x/circuit-breaker/types/types.go | 14 ++++++++ x/groupgov/abci.go | 6 ++++ x/groupgov/handler.go | 56 ++++++++++++++++++++++++++++++ x/groupgov/keeper/keeper.go | 10 ++++++ x/groupgov/spec/README.md | 8 +++++ x/groupgov/types/msg.go | 13 +++++++ x/groupgov/types/permissions.go | 24 +++++++++++++ x/groupgov/types/types.go | 46 ++++++++++++++++++++++++ 12 files changed, 250 insertions(+) create mode 100644 x/circuit-breaker/ante/ante.go create mode 100644 x/circuit-breaker/gov-handler.go create mode 100644 x/circuit-breaker/keeper/keeper.go create mode 100644 x/circuit-breaker/spec/README.md create mode 100644 x/circuit-breaker/types/types.go create mode 100644 x/groupgov/abci.go create mode 100644 x/groupgov/handler.go create mode 100644 x/groupgov/keeper/keeper.go create mode 100644 x/groupgov/spec/README.md create mode 100644 x/groupgov/types/msg.go create mode 100644 x/groupgov/types/permissions.go create mode 100644 x/groupgov/types/types.go diff --git a/x/circuit-breaker/ante/ante.go b/x/circuit-breaker/ante/ante.go new file mode 100644 index 00000000..df8ed9dd --- /dev/null +++ b/x/circuit-breaker/ante/ante.go @@ -0,0 +1,31 @@ +package circuit-breaker + +// CircuiteBreakerDecorator needs to be combined with other standard decorators (from auth) to create the app's AnteHandler. + +// CircuitBreakerDecorator errors if a tx contains a disallowed msg type +// Call next AnteHandler if all msgs are allowed +type CircuitBreakerDecorator struct { + cbk Keeper +} + +func NewCircuitBreakerDecorator(cbk Keeper) CircuitBreakerDecorator { + return CircuitBreakerDecorator{ + cbk: cbk, + } +} + +func (cbd CircuitBreakerDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { +// TODO need to tidy up the types used to store broken routes + + // get msg route, error if not allowed + disallowedRoutes := cbd.cbk.GetRoutes(ctx) + requestedRoutes := tx.Msgs + for m, _ := range tx.Msgs { + for r, _ := range disallowedRoutes { + if r == m.Route() { + return ctx, fmt.Errorf("route %s has been circuit broken, tx rejected", r) + } + } + } + return next(ctx, tx, simulate) +} diff --git a/x/circuit-breaker/gov-handler.go b/x/circuit-breaker/gov-handler.go new file mode 100644 index 00000000..c0397b10 --- /dev/null +++ b/x/circuit-breaker/gov-handler.go @@ -0,0 +1,14 @@ +package circuit-breaker + +func NewCircuitBreakerProposalHandler(k Keeper) govtypes.Handler { + return func(ctx sdk.Context, content govtypes.Content) sdk.Error { + switch c := content.(type) { + case types.CircuitBreakerProposal: + return keeper.HandleCircuitBreakerProposal(ctx, k, c) + + default: + errMsg := fmt.Sprintf("unrecognized circuit-breaker proposal content type: %T", c) + return sdk.ErrUnknownRequest(errMsg) + } + } +} \ No newline at end of file diff --git a/x/circuit-breaker/keeper/keeper.go b/x/circuit-breaker/keeper/keeper.go new file mode 100644 index 00000000..d8efcd16 --- /dev/null +++ b/x/circuit-breaker/keeper/keeper.go @@ -0,0 +1,18 @@ +package circuit-breaker + +// Keeper stores routes that have been "broken" +type Keeper struct { +} + + +func (k Keeper) GetRoutes(ctx sdk.Context) []string { + // TODO +} + +func (k Keeper) SetRoutes(ctx sdk.Context, routes []string) { + // TODO +} + +func (k Keeper) HandleCircuitBreakerProposal(ctx sdk.Context, c Content) { + k.SetRoutes(ctx, c.Routes) +} \ No newline at end of file diff --git a/x/circuit-breaker/spec/README.md b/x/circuit-breaker/spec/README.md new file mode 100644 index 00000000..6f9f0eb7 --- /dev/null +++ b/x/circuit-breaker/spec/README.md @@ -0,0 +1,10 @@ + +# `groupgov` + +## Table of Contents + +## Overview + +The `x/groupgov` module is an additional governance module to `cosmos-sdk/x/gov`. It allows groups of accounts to vote on and enact proposals, mainly to allow certain proposal types to be decided on quickly in emergency situations, or to delegate low risk parameter updates to a smaller group of individuals. + +Groups have permissions. diff --git a/x/circuit-breaker/types/types.go b/x/circuit-breaker/types/types.go new file mode 100644 index 00000000..44db626a --- /dev/null +++ b/x/circuit-breaker/types/types.go @@ -0,0 +1,14 @@ +package types + +// TODO implement a gov proposal for adding a route to the circuit breaker keeper. + +type CircuitBreakProposal struct { + MsgRoutes []MsgRoute +} + +type MsgRoute struct { + Route string + Msg sdk.Msg // how best to store a Msg type? as a string? +} + +// TODO gov.Proposal methods... diff --git a/x/groupgov/abci.go b/x/groupgov/abci.go new file mode 100644 index 00000000..736efcd9 --- /dev/null +++ b/x/groupgov/abci.go @@ -0,0 +1,6 @@ +package groupgov + +func BeginBlocker() { + // TODO do much the same as the current gov endblocker does + // if voting periods are over, collect votes and run proposals through proposal handlers +} diff --git a/x/groupgov/handler.go b/x/groupgov/handler.go new file mode 100644 index 00000000..74d08933 --- /dev/null +++ b/x/groupgov/handler.go @@ -0,0 +1,56 @@ +package cdp + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/groupgov/types" +) + +// NewHandler creates an sdk.Handler for cdp messages +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case types.MsgSubmitProposal: + handleMsgSubmitProposal(ctx, k, msg) + case types.MsgVote: + handleMsgVote(ctx, k, msg) + default: + errMsg := fmt.Sprintf("unrecognized %s msg type: %T", , types.ModuleName, msg) + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +func handleMsgSubmitProposal(ctx sdk.Context, k Keeper, msg types.MsgSubmitProposal) sdk.Result { + // TODO limit proposals to only be submitted by group members + + // get group + group, _ := k.GetGroup(ctx, msg.GroupID) + // Check group has permissions to enact proposal. As long as one permission allows the proposal then it goes through. Its the OR of all permissions. + var hasPermissions := false + for p, _ := range group.Permissions { + if p.Allows(msg.Proposal) { + hasPermissions = true + break + } + } + if !hasPermissions { + return sdk.ErrInternal("group does not have permissions to enact proposal").Result() + } + // TODO validate proposal by running it with cached context like how gov does it + // TODO store the proposal, probably put it in a queue + +} + +func handleMsgVote(ctx sdk.Context, k Keeper, msg types.MsgVote) sdk.Result { + /* TODO + - validate vote + - store vote + */ +} + + + +// TODO create a GroupChangeProposalHandler \ No newline at end of file diff --git a/x/groupgov/keeper/keeper.go b/x/groupgov/keeper/keeper.go new file mode 100644 index 00000000..f7e09804 --- /dev/null +++ b/x/groupgov/keeper/keeper.go @@ -0,0 +1,10 @@ +package types + +import "github.com/cosmos/cosmos-sdk/types" + +type Keeper struct { + // TODO other stuff as needed + + // Proposal router + router types.Router +} diff --git a/x/groupgov/spec/README.md b/x/groupgov/spec/README.md new file mode 100644 index 00000000..0b3294f0 --- /dev/null +++ b/x/groupgov/spec/README.md @@ -0,0 +1,8 @@ + +# `circuit-breaker` + +## Table of Contents + +## Overview + +The `x/circuit-breaker` module allows certain message types to be disabled based on governance votes. diff --git a/x/groupgov/types/msg.go b/x/groupgov/types/msg.go new file mode 100644 index 00000000..d8fbb8b9 --- /dev/null +++ b/x/groupgov/types/msg.go @@ -0,0 +1,13 @@ +package types + +// These msg types should be basically the same as for gov + +// MsgSubmitProposal is used by group members to create a new proposal that they can vote on. +type MsgSubmitProposal struct { + // TODO +} + +// MsgVote is submitted by group members to vote on proposals. +type MsgVote struct { + // TODO +} diff --git a/x/groupgov/types/permissions.go b/x/groupgov/types/permissions.go new file mode 100644 index 00000000..6b2e286a --- /dev/null +++ b/x/groupgov/types/permissions.go @@ -0,0 +1,24 @@ +package types + +// EXAMPLE PERMISSIONS ------------------------------ +type InflationRateChangePermission uint8 + +func (InflationRateChangePermission) Allows(p gov.Proposal) bool { + pcp, _ := p.Content.(params.ParameterChangeProposal) + for pc, _ := range pcp.Changes { + if pc.Key == "inflation_rate" { + return true + } + } + return false +} + +type CircuitBreakCDPDepsitPermission uint8 + +func (CircuitBreakCDPDepsitPermission) Allows(p gov.Proposal) bool { + cbp, _ := p.Content.(CircuitBreakProposal) + if cbp.Route == "cdp" && cbp.Msg == "MsgCDPDeposit" { + return true + } + return false +} diff --git a/x/groupgov/types/types.go b/x/groupgov/types/types.go new file mode 100644 index 00000000..2ef2fd7f --- /dev/null +++ b/x/groupgov/types/types.go @@ -0,0 +1,46 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/params" +) + +// A Group is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. +type Group struct { + Members []sdk.AccAddress + Permissions []Permission +} + +// handler for MsgSubmitProposal needs to loop apply all group permission Allows methods to the proposal and do a bit OR to see if it should be accepted + +// Permission is anything with a method that validates whether a proposal is allowed by it or not. +// Collectively, if one permission allows a proposal then the proposal is allowed through. +type Permission interface { + Allows(gov.Proposal) bool // maybe don't reuse gov's type here +} + +// 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 groupgov to allow groups to be controlled by other groups. +type GroupChangeProposal struct { + Members []sdk.AccAddress + Permissions []Permission +} + +// STANDARD GOV STUFF -------------------------- +// Should be much the same as in gov module. Either import gov types directly or do some copy n pasting. + +type Router struct { + // TODO +} + +type Proposal struct { + ID uint64 + groupID uint64 + // TODO +} + +type Vote struct { + proposalID uint64 + option uint64 + // TODO +} From 8a36e926e821817c2703be1b24fab58208a5f17f Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 4 Mar 2020 16:41:13 +0000 Subject: [PATCH 02/54] tidy up circuit-breaker --- x/circuit-breaker/ante/ante.go | 28 ++++++++------ x/circuit-breaker/gov-handler.go | 12 +++++- x/circuit-breaker/keeper/keeper.go | 18 +++++---- x/circuit-breaker/spec/README.md | 14 +++++-- x/circuit-breaker/types/types.go | 61 ++++++++++++++++++++++++++---- x/groupgov/spec/README.md | 19 +++++++++- 6 files changed, 120 insertions(+), 32 deletions(-) diff --git a/x/circuit-breaker/ante/ante.go b/x/circuit-breaker/ante/ante.go index df8ed9dd..0ad9e78c 100644 --- a/x/circuit-breaker/ante/ante.go +++ b/x/circuit-breaker/ante/ante.go @@ -1,28 +1,34 @@ -package circuit-breaker +package ante -// CircuiteBreakerDecorator needs to be combined with other standard decorators (from auth) to create the app's AnteHandler. +import ( + "fmt" + + "github.com/kava-labs/kava/x/circuit-breaker/keeper" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// CircuitBreakerDecorator needs to be combined with other standard decorators (from auth) to create the app's AnteHandler. // CircuitBreakerDecorator errors if a tx contains a disallowed msg type // Call next AnteHandler if all msgs are allowed type CircuitBreakerDecorator struct { - cbk Keeper + cbk keeper.Keeper } -func NewCircuitBreakerDecorator(cbk Keeper) CircuitBreakerDecorator { +func NewCircuitBreakerDecorator(cbk keeper.Keeper) CircuitBreakerDecorator { return CircuitBreakerDecorator{ - cbk: cbk, + cbk: cbk, } } func (cbd CircuitBreakerDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { -// TODO need to tidy up the types used to store broken routes // get msg route, error if not allowed - disallowedRoutes := cbd.cbk.GetRoutes(ctx) - requestedRoutes := tx.Msgs - for m, _ := range tx.Msgs { - for r, _ := range disallowedRoutes { - if r == m.Route() { + disallowedRoutes := cbd.cbk.GetMsgRoutes(ctx) + for _, m := range tx.GetMsgs() { + for _, r := range disallowedRoutes { + if r.Route == m.Route() && r.Msg == m.Type() { return ctx, fmt.Errorf("route %s has been circuit broken, tx rejected", r) } } diff --git a/x/circuit-breaker/gov-handler.go b/x/circuit-breaker/gov-handler.go index c0397b10..0d5dd79c 100644 --- a/x/circuit-breaker/gov-handler.go +++ b/x/circuit-breaker/gov-handler.go @@ -1,7 +1,15 @@ package circuit-breaker -func NewCircuitBreakerProposalHandler(k Keeper) govtypes.Handler { - return func(ctx sdk.Context, content govtypes.Content) sdk.Error { +import ( + "github.com/cosmos/cosmos-sdk/x/gov" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/circuit-breaker/types" + "github.com/kava-labs/kava/x/circuit-breaker/keeper" +) + +func NewCircuitBreakerProposalHandler(k Keeper) gov.Handler { + return func(ctx sdk.Context, content gov.Content) sdk.Error { switch c := content.(type) { case types.CircuitBreakerProposal: return keeper.HandleCircuitBreakerProposal(ctx, k, c) diff --git a/x/circuit-breaker/keeper/keeper.go b/x/circuit-breaker/keeper/keeper.go index d8efcd16..0a1b650a 100644 --- a/x/circuit-breaker/keeper/keeper.go +++ b/x/circuit-breaker/keeper/keeper.go @@ -1,18 +1,22 @@ -package circuit-breaker +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/circuit-breaker/types" +) // Keeper stores routes that have been "broken" type Keeper struct { } - -func (k Keeper) GetRoutes(ctx sdk.Context) []string { +func (k Keeper) GetMsgRoutes(ctx sdk.Context) []types.MsgRoute { // TODO } -func (k Keeper) SetRoutes(ctx sdk.Context, routes []string) { +func (k Keeper) SetMsgRoutes(ctx sdk.Context, routes []types.MsgRoute) { // TODO } -func (k Keeper) HandleCircuitBreakerProposal(ctx sdk.Context, c Content) { - k.SetRoutes(ctx, c.Routes) -} \ No newline at end of file +func HandleCircuitBreakerProposal(ctx sdk.Context, k Keeper, c types.CircuitBreakProposal) { + k.SetMsgRoutes(ctx, c.MsgRoutes) +} diff --git a/x/circuit-breaker/spec/README.md b/x/circuit-breaker/spec/README.md index 6f9f0eb7..5c53f051 100644 --- a/x/circuit-breaker/spec/README.md +++ b/x/circuit-breaker/spec/README.md @@ -1,10 +1,18 @@ -# `groupgov` +# `circuit-breaker` ## Table of Contents ## Overview -The `x/groupgov` module is an additional governance module to `cosmos-sdk/x/gov`. It allows groups of accounts to vote on and enact proposals, mainly to allow certain proposal types to be decided on quickly in emergency situations, or to delegate low risk parameter updates to a smaller group of individuals. +The `x/circuit-breaker` module allows certain message types to be disabled based on governance votes. -Groups have permissions. +Msgs and routes are disabled via an antehandler decorator. The decorator checks incoming all txs and rejects them if they contain a disallowed msg type. +Disallowed msg types are stored in a circuit breaker keeper. + +The list of disallowed msg types is updated via a custom governance proposal and handler. + +Design Alternatives: + +- store list of disallowed msg types in params, then don't need custom gov proposal +- replace the app Router with a custom one to avoid using the antehandler - can't be done with current baseapp, but v0.38.x enables this. (https://github.com/cosmos/cosmos-sdk/issues/5455) \ No newline at end of file diff --git a/x/circuit-breaker/types/types.go b/x/circuit-breaker/types/types.go index 44db626a..8f700025 100644 --- a/x/circuit-breaker/types/types.go +++ b/x/circuit-breaker/types/types.go @@ -1,14 +1,61 @@ package types -// TODO implement a gov proposal for adding a route to the circuit breaker keeper. - -type CircuitBreakProposal struct { - MsgRoutes []MsgRoute -} +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) type MsgRoute struct { Route string - Msg sdk.Msg // how best to store a Msg type? as a string? + Msg string // how best to store a Msg type? } -// TODO gov.Proposal methods... +const ( + ProposalTypeCircuitBreak = "CircuitBreak" +) + +// Assert CircuitBreakProposal implements govtypes.Content at compile-time +var _ govtypes.Content = CircuitBreakProposal{} + +type CircuitBreakProposal struct { + Title string + Description string + MsgRoutes []MsgRoute +} + +// GetTitle returns the title of a community pool spend proposal. +func (cbp CircuitBreakProposal) GetTitle() string { return cbp.Title } + +// GetDescription returns the description of a community pool spend proposal. +func (cbp CircuitBreakProposal) GetDescription() string { return cbp.Description } + +// GetDescription returns the routing key of a community pool spend proposal. +func (cbp CircuitBreakProposal) ProposalRoute() string { return RouterKey } + +// ProposalType returns the type of a community pool spend proposal. +func (cbp CircuitBreakProposal) ProposalType() string { return ProposalTypeCircuitBreak } + +// ValidateBasic runs basic stateless validity checks +func (cbp CircuitBreakProposal) ValidateBasic() sdk.Error { + err := govtypes.ValidateAbstract(DefaultCodespace, cbp) + if err != nil { + return err + } + // TODO + return nil +} + +// String implements the Stringer interface. +func (cbp CircuitBreakProposal) String() string { + // TODO +} + +const ( + DefaultCodespace sdk.CodespaceType = ModuleName + + // ModuleName is the module name constant used in many places + ModuleName = "circuit-breaker" + + // RouterKey is the message route for distribution + RouterKey = ModuleName +) diff --git a/x/groupgov/spec/README.md b/x/groupgov/spec/README.md index 0b3294f0..db9f0c5b 100644 --- a/x/groupgov/spec/README.md +++ b/x/groupgov/spec/README.md @@ -1,8 +1,23 @@ -# `circuit-breaker` +# `groupgov` ## Table of Contents ## Overview -The `x/circuit-breaker` module allows certain message types to be disabled based on governance votes. +The `x/groupgov` module is an additional governance module to `cosmos-sdk/x/gov`. + +It allows groups of accounts to vote on and enact proposals, mainly to allow certain proposal types to be decided on quickly in emergency situations, or to delegate low risk parameter updates to a smaller group of individuals. + +Groups have members and permissions. + +Members vote on proposals, with just simple one vote per member, no deposits or slashing. More sophisticated voting could be added. + +A permission acts as a filter for incoming gov proposals, rejecting them if they do not pass. A permission can be anything with a method `Allows(p Proposal) bool`. They reject all proposals that they don't explicitly allow. + +This allows permissions to be parameterized to allow fine grained control specified at runtime. For example a generic parameter permission type can exist, but then on a live chain a permission can be added to a group to allow them to only change a particular param, even restricting the range of allowed change. + +Design Alternatives + +- Should this define its own gov types, or reuse those from gov module? +- Should we push changes to sdk gov to make it more general purpose? \ No newline at end of file From 3e1b1b1b7207cb8fc7a42cc79519e7a178927b18 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 4 Mar 2020 16:55:12 +0000 Subject: [PATCH 03/54] tidy up groupgov --- .../{gov-handler.go => proposal_handler.go} | 0 x/groupgov/handler.go | 4 -- x/groupgov/keeper/keeper.go | 8 ++++ x/groupgov/proposal_handler.go | 4 ++ x/groupgov/types/msg.go | 2 +- x/groupgov/types/permissions.go | 38 ++++++++++++++++--- x/groupgov/types/proposal.go | 12 ++++++ x/groupgov/types/types.go | 13 +------ 8 files changed, 59 insertions(+), 22 deletions(-) rename x/circuit-breaker/{gov-handler.go => proposal_handler.go} (100%) create mode 100644 x/groupgov/proposal_handler.go create mode 100644 x/groupgov/types/proposal.go diff --git a/x/circuit-breaker/gov-handler.go b/x/circuit-breaker/proposal_handler.go similarity index 100% rename from x/circuit-breaker/gov-handler.go rename to x/circuit-breaker/proposal_handler.go diff --git a/x/groupgov/handler.go b/x/groupgov/handler.go index 74d08933..5b1a959c 100644 --- a/x/groupgov/handler.go +++ b/x/groupgov/handler.go @@ -50,7 +50,3 @@ func handleMsgVote(ctx sdk.Context, k Keeper, msg types.MsgVote) sdk.Result { - store vote */ } - - - -// TODO create a GroupChangeProposalHandler \ No newline at end of file diff --git a/x/groupgov/keeper/keeper.go b/x/groupgov/keeper/keeper.go index f7e09804..909d53bd 100644 --- a/x/groupgov/keeper/keeper.go +++ b/x/groupgov/keeper/keeper.go @@ -8,3 +8,11 @@ type Keeper struct { // Proposal router router types.Router } + +/* TODO methods - should be similar to gov +- GetGroup +- SetGroup + +- AddVote + +*/ diff --git a/x/groupgov/proposal_handler.go b/x/groupgov/proposal_handler.go new file mode 100644 index 00000000..12598aea --- /dev/null +++ b/x/groupgov/proposal_handler.go @@ -0,0 +1,4 @@ +package groupgov + +// TODO create a GroupChangeProposalHandler, see params or distribution +// It will overwrite the Members of Permissions field of a group diff --git a/x/groupgov/types/msg.go b/x/groupgov/types/msg.go index d8fbb8b9..06cc211c 100644 --- a/x/groupgov/types/msg.go +++ b/x/groupgov/types/msg.go @@ -1,6 +1,6 @@ package types -// These msg types should be basically the same as for gov +// These msg types should be basically the same as for gov, but without deposits. // MsgSubmitProposal is used by group members to create a new proposal that they can vote on. type MsgSubmitProposal struct { diff --git a/x/groupgov/types/permissions.go b/x/groupgov/types/permissions.go index 6b2e286a..4024d8d3 100644 --- a/x/groupgov/types/permissions.go +++ b/x/groupgov/types/permissions.go @@ -1,11 +1,19 @@ package types +import ( + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/params" + cbtypes "github.com/kava-labs/kava/x/circuit-breaker/types" +) + // EXAMPLE PERMISSIONS ------------------------------ -type InflationRateChangePermission uint8 + +// Allow only changes to inflation_rate +type InflationRateChangePermission struct{} func (InflationRateChangePermission) Allows(p gov.Proposal) bool { pcp, _ := p.Content.(params.ParameterChangeProposal) - for pc, _ := range pcp.Changes { + for _, pc := range pcp.Changes { if pc.Key == "inflation_rate" { return true } @@ -13,12 +21,30 @@ func (InflationRateChangePermission) Allows(p gov.Proposal) bool { return false } -type CircuitBreakCDPDepsitPermission uint8 +// Allow only circuit breaking of the CDP Deposit msg +type CircuitBreakCDPDepsitPermission struct{} func (CircuitBreakCDPDepsitPermission) Allows(p gov.Proposal) bool { - cbp, _ := p.Content.(CircuitBreakProposal) - if cbp.Route == "cdp" && cbp.Msg == "MsgCDPDeposit" { - return true + cbp, _ := p.Content.(cbtypes.CircuitBreakProposal) + for _, r := range cbp.MsgRoutes { + if r.Route == "cdp" && r.Msg == "MsgCDPDeposit" { + return true + } + } + return false +} + +// Same as above but the route the permssion allows can be set +type CircuitBreakPermission struct { + MsgRoute cbtypes.MsgRoute +} + +func (perm CircuitBreakPermission) Allows(p gov.Proposal) bool { + cbp, _ := p.Content.(cbtypes.CircuitBreakProposal) + for _, r := range cbp.MsgRoutes { + if r == perm.MsgRoute { + return true + } } return false } diff --git a/x/groupgov/types/proposal.go b/x/groupgov/types/proposal.go new file mode 100644 index 00000000..611e5671 --- /dev/null +++ b/x/groupgov/types/proposal.go @@ -0,0 +1,12 @@ +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 groupgov to allow groups to be controlled by other groups. +type GroupChangeProposal struct { + Members []sdk.AccAddress + Permissions []Permission +} diff --git a/x/groupgov/types/types.go b/x/groupgov/types/types.go index 2ef2fd7f..50940abd 100644 --- a/x/groupgov/types/types.go +++ b/x/groupgov/types/types.go @@ -1,8 +1,8 @@ package types import ( + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov" - "github.com/cosmos/cosmos-sdk/x/params" ) // A Group is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. @@ -11,23 +11,14 @@ type Group struct { Permissions []Permission } -// handler for MsgSubmitProposal needs to loop apply all group permission Allows methods to the proposal and do a bit OR to see if it should be accepted - // Permission is anything with a method that validates whether a proposal is allowed by it or not. // Collectively, if one permission allows a proposal then the proposal is allowed through. type Permission interface { Allows(gov.Proposal) bool // maybe don't reuse gov's type here } -// 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 groupgov to allow groups to be controlled by other groups. -type GroupChangeProposal struct { - Members []sdk.AccAddress - Permissions []Permission -} - // STANDARD GOV STUFF -------------------------- -// Should be much the same as in gov module. Either import gov types directly or do some copy n pasting. +// Should be much the same as in gov module, except Proposals are linked to a group ID. type Router struct { // TODO From 2ab6c4669fda0b45c645aee1d75c254bafa7a0e7 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 4 Mar 2020 19:16:27 +0000 Subject: [PATCH 04/54] rename modules and tidy --- x/circuit-breaker/proposal_handler.go | 22 ------- x/committee/abci.go | 12 ++++ x/committee/handler.go | 47 ++++++++++++++ x/committee/keeper/keeper.go | 61 +++++++++++++++++ x/{groupgov => committee}/proposal_handler.go | 2 +- x/{groupgov => committee}/spec/README.md | 9 +-- x/{groupgov => committee}/types/msg.go | 4 +- x/committee/types/permissions.go | 65 +++++++++++++++++++ x/{groupgov => committee}/types/proposal.go | 2 +- x/committee/types/types.go | 33 ++++++++++ x/groupgov/abci.go | 6 -- x/groupgov/handler.go | 52 --------------- x/groupgov/keeper/keeper.go | 18 ----- x/groupgov/types/permissions.go | 50 -------------- x/groupgov/types/types.go | 37 ----------- x/{circuit-breaker => shutdown}/ante/ante.go | 2 +- .../keeper/keeper.go | 7 +- x/shutdown/proposal_handler.go | 30 +++++++++ .../spec/README.md | 4 +- .../types/types.go | 25 +++---- 20 files changed, 275 insertions(+), 213 deletions(-) delete mode 100644 x/circuit-breaker/proposal_handler.go create mode 100644 x/committee/abci.go create mode 100644 x/committee/handler.go create mode 100644 x/committee/keeper/keeper.go rename x/{groupgov => committee}/proposal_handler.go (88%) rename x/{groupgov => committee}/spec/README.md (84%) rename x/{groupgov => committee}/types/msg.go (51%) create mode 100644 x/committee/types/permissions.go rename x/{groupgov => committee}/types/proposal.go (81%) create mode 100644 x/committee/types/types.go delete mode 100644 x/groupgov/abci.go delete mode 100644 x/groupgov/handler.go delete mode 100644 x/groupgov/keeper/keeper.go delete mode 100644 x/groupgov/types/permissions.go delete mode 100644 x/groupgov/types/types.go rename x/{circuit-breaker => shutdown}/ante/ante.go (94%) rename x/{circuit-breaker => shutdown}/keeper/keeper.go (62%) create mode 100644 x/shutdown/proposal_handler.go rename x/{circuit-breaker => shutdown}/spec/README.md (83%) rename x/{circuit-breaker => shutdown}/types/types.go (56%) diff --git a/x/circuit-breaker/proposal_handler.go b/x/circuit-breaker/proposal_handler.go deleted file mode 100644 index 0d5dd79c..00000000 --- a/x/circuit-breaker/proposal_handler.go +++ /dev/null @@ -1,22 +0,0 @@ -package circuit-breaker - -import ( - "github.com/cosmos/cosmos-sdk/x/gov" - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/kava-labs/kava/x/circuit-breaker/types" - "github.com/kava-labs/kava/x/circuit-breaker/keeper" -) - -func NewCircuitBreakerProposalHandler(k Keeper) gov.Handler { - return func(ctx sdk.Context, content gov.Content) sdk.Error { - switch c := content.(type) { - case types.CircuitBreakerProposal: - return keeper.HandleCircuitBreakerProposal(ctx, k, c) - - default: - errMsg := fmt.Sprintf("unrecognized circuit-breaker proposal content type: %T", c) - return sdk.ErrUnknownRequest(errMsg) - } - } -} \ No newline at end of file diff --git a/x/committee/abci.go b/x/committee/abci.go new file mode 100644 index 00000000..50bb9dc2 --- /dev/null +++ b/x/committee/abci.go @@ -0,0 +1,12 @@ +package committee + +func BeginBlocker() { + // TODO much the same as the current gov endblocker does + + // Get all active proposals + // If voting periods are over, tally up the results + // If a proposal passes run it through the correct handler + // Handler need to be registered in app.go as they are for the current gov module + handler := keeper.Router().GetRoute(proposal.ProposalRoute()) + err := handler(ctx, proposal.Content) +} diff --git a/x/committee/handler.go b/x/committee/handler.go new file mode 100644 index 00000000..bc23eb5a --- /dev/null +++ b/x/committee/handler.go @@ -0,0 +1,47 @@ +package committee + +// committee, subcommittee, council, caucus, commission, synod, board + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/committee/keeper" + "github.com/kava-labs/kava/x/committee/types" +) + +// NewHandler creates an sdk.Handler for committee messages +func NewHandler(k keeper.Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case types.MsgSubmitProposal: + handleMsgSubmitProposal(ctx, k, msg) + case types.MsgVote: + handleMsgVote(ctx, k, msg) + default: + errMsg := fmt.Sprintf("unrecognized %s msg type: %T", types.ModuleName, msg) + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubmitProposal) sdk.Result { + err := keeper.SubmitProposal(ctx, msg) + + if err != nil { + return err.Result() + } + + return sdk.Result{} +} + +func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Result { + err := keeper.AddVote(ctx, msg) + + if err != nil { + return err.Result() + } + + return sdk.Result{} +} diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go new file mode 100644 index 00000000..8dc5158d --- /dev/null +++ b/x/committee/keeper/keeper.go @@ -0,0 +1,61 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/kava-labs/kava/x/committee/types" +) + +type Keeper struct { + // TODO other stuff as needed + + // Proposal router + router govtypes.Router +} + +/* TODO keeper methods - very similar to gov + +- SubmitProposal validate and store a proposal, additionally setting things like timeout +- GetProposal +- SetProposal + +- AddVote - add a vote to a particular proposal from a member +- GetVote +- SetVote + +- GetCommittee +- SetCommittee + +*/ + +func (k Keeper) SubmitProposal(ctx sdk.Context, msg types.MsgSubmitProposal) sdk.Error { + // TODO Limit proposals to only be submitted by group members + + // Check group has permissions to enact proposal. As long as one permission allows the proposal then it goes through. Its the OR of all permissions. + committee, _ := k.GetCommittee(ctx, msg.CommitteeID) + hasPermissions := false + for _, p := range committee.Permissions { + if p.Allows(msg.Proposal) { + hasPermissions = true + break + } + } + if !hasPermissions { + return sdk.ErrInternal("committee does not have permissions to enact proposal").Result() + } + + // TODO validate proposal by running it with cached context like how gov does it + + // TODO store the proposal, probably put it in a queue + + return nil +} + +func (k Keeper) AddVote(ctx sdk.Context, msg types.MsgVote) sdk.Error { + /* TODO + - validate vote + - store vote + */ + return nil +} diff --git a/x/groupgov/proposal_handler.go b/x/committee/proposal_handler.go similarity index 88% rename from x/groupgov/proposal_handler.go rename to x/committee/proposal_handler.go index 12598aea..852a704f 100644 --- a/x/groupgov/proposal_handler.go +++ b/x/committee/proposal_handler.go @@ -1,4 +1,4 @@ -package groupgov +package committee // TODO create a GroupChangeProposalHandler, see params or distribution // It will overwrite the Members of Permissions field of a group diff --git a/x/groupgov/spec/README.md b/x/committee/spec/README.md similarity index 84% rename from x/groupgov/spec/README.md rename to x/committee/spec/README.md index db9f0c5b..ac8103d5 100644 --- a/x/groupgov/spec/README.md +++ b/x/committee/spec/README.md @@ -1,15 +1,15 @@ -# `groupgov` +# `committee` ## Table of Contents ## Overview -The `x/groupgov` module is an additional governance module to `cosmos-sdk/x/gov`. +The `x/committee` module is an additional governance module to `cosmos-sdk/x/gov`. It allows groups of accounts to vote on and enact proposals, mainly to allow certain proposal types to be decided on quickly in emergency situations, or to delegate low risk parameter updates to a smaller group of individuals. -Groups have members and permissions. +Committees have members and permissions. Members vote on proposals, with just simple one vote per member, no deposits or slashing. More sophisticated voting could be added. @@ -20,4 +20,5 @@ This allows permissions to be parameterized to allow fine grained control specif Design Alternatives - Should this define its own gov types, or reuse those from gov module? -- Should we push changes to sdk gov to make it more general purpose? \ No newline at end of file +- Should we push changes to sdk gov to make it more general purpose? +- Could use params more instead of custom gov proposals diff --git a/x/groupgov/types/msg.go b/x/committee/types/msg.go similarity index 51% rename from x/groupgov/types/msg.go rename to x/committee/types/msg.go index 06cc211c..508d4a4a 100644 --- a/x/groupgov/types/msg.go +++ b/x/committee/types/msg.go @@ -2,12 +2,12 @@ package types // These msg types should be basically the same as for gov, but without deposits. -// MsgSubmitProposal is used by group members to create a new proposal that they can vote on. +// MsgSubmitProposal is used by committee members to create a new proposal that they can vote on. type MsgSubmitProposal struct { // TODO } -// MsgVote is submitted by group members to vote on proposals. +// MsgVote is submitted by committee members to vote on proposals. type MsgVote struct { // TODO } diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go new file mode 100644 index 00000000..d560296d --- /dev/null +++ b/x/committee/types/permissions.go @@ -0,0 +1,65 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/params" + sdtypes "github.com/kava-labs/kava/x/shutdown/types" +) + +// EXAMPLE PERMISSIONS ------------------------------ + +// Allow only changes to inflation_rate +type InflationRateChangePermission struct{} + +var _ types.Permission = InflationRateChangePermission + +func (InflationRateChangePermission) Allows(p gov.Proposal) bool { + pcp, ok := p.Content.(params.ParameterChangeProposal) + if !ok { + return false + } + for _, pc := range pcp.Changes { + if pc.Key == "inflation_rate" { + return true + } + } + return false +} + +// Allow only shutdown of the CDP Deposit msg +type ShutdownCDPDepsitPermission struct{} + +var _ types.Permission = ShutdownCDPDepsitPermission + +func (ShutdownCDPDepsitPermission) Allows(p gov.Content) bool { + sdp, ok := p.(sdtypes.ShutdownProposal) + if !ok { + return false + } + for _, r := range sdp.MsgRoutes { + if r.Route == "cdp" && r.Msg == "MsgCDPDeposit" { + return true + } + } + return false +} + +// Same as above but the route isn't static +type GeneralShutdownPermission struct { + MsgRoute cbtypes.MsgRoute +} + +var _ types.Permission = GeneralShutdownPermission + +func (perm GeneralShutdownPermission) Allows(p gov.Content) bool { + sdp, ok := p.Content.(sdtypes.ShutdownProposal) + if !ok { + return false + } + for _, r := range sdp.MsgRoutes { + if r == perm.MsgRoute { + return true + } + } + return false +} diff --git a/x/groupgov/types/proposal.go b/x/committee/types/proposal.go similarity index 81% rename from x/groupgov/types/proposal.go rename to x/committee/types/proposal.go index 611e5671..58403a17 100644 --- a/x/groupgov/types/proposal.go +++ b/x/committee/types/proposal.go @@ -5,7 +5,7 @@ import ( ) // 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 groupgov to allow groups to be controlled by other groups. +// 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 diff --git a/x/committee/types/types.go b/x/committee/types/types.go new file mode 100644 index 00000000..2994a179 --- /dev/null +++ b/x/committee/types/types.go @@ -0,0 +1,33 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" +) + +// A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. +type Committee struct { + Members []sdk.AccAddress + Permissions []Permission +} + +// Permission is anything with a method that validates whether a proposal is allowed by it or not. +type Permission interface { + Allows(gov.Content) bool +} + +// GOV STUFF -------------------------- +// Should be much the same as in gov module, except Proposals are linked to a committee ID. + +type Proposal struct { + gov.Content + ID uint64 + committeeID uint64 + // TODO +} + +type Vote struct { + ProposalID uint64 + Voter sdk.AccAddress + Option byte +} diff --git a/x/groupgov/abci.go b/x/groupgov/abci.go deleted file mode 100644 index 736efcd9..00000000 --- a/x/groupgov/abci.go +++ /dev/null @@ -1,6 +0,0 @@ -package groupgov - -func BeginBlocker() { - // TODO do much the same as the current gov endblocker does - // if voting periods are over, collect votes and run proposals through proposal handlers -} diff --git a/x/groupgov/handler.go b/x/groupgov/handler.go deleted file mode 100644 index 5b1a959c..00000000 --- a/x/groupgov/handler.go +++ /dev/null @@ -1,52 +0,0 @@ -package cdp - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/kava-labs/kava/x/groupgov/types" -) - -// NewHandler creates an sdk.Handler for cdp messages -func NewHandler(k Keeper) sdk.Handler { - return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { - switch msg := msg.(type) { - case types.MsgSubmitProposal: - handleMsgSubmitProposal(ctx, k, msg) - case types.MsgVote: - handleMsgVote(ctx, k, msg) - default: - errMsg := fmt.Sprintf("unrecognized %s msg type: %T", , types.ModuleName, msg) - return sdk.ErrUnknownRequest(errMsg).Result() - } - } -} - -func handleMsgSubmitProposal(ctx sdk.Context, k Keeper, msg types.MsgSubmitProposal) sdk.Result { - // TODO limit proposals to only be submitted by group members - - // get group - group, _ := k.GetGroup(ctx, msg.GroupID) - // Check group has permissions to enact proposal. As long as one permission allows the proposal then it goes through. Its the OR of all permissions. - var hasPermissions := false - for p, _ := range group.Permissions { - if p.Allows(msg.Proposal) { - hasPermissions = true - break - } - } - if !hasPermissions { - return sdk.ErrInternal("group does not have permissions to enact proposal").Result() - } - // TODO validate proposal by running it with cached context like how gov does it - // TODO store the proposal, probably put it in a queue - -} - -func handleMsgVote(ctx sdk.Context, k Keeper, msg types.MsgVote) sdk.Result { - /* TODO - - validate vote - - store vote - */ -} diff --git a/x/groupgov/keeper/keeper.go b/x/groupgov/keeper/keeper.go deleted file mode 100644 index 909d53bd..00000000 --- a/x/groupgov/keeper/keeper.go +++ /dev/null @@ -1,18 +0,0 @@ -package types - -import "github.com/cosmos/cosmos-sdk/types" - -type Keeper struct { - // TODO other stuff as needed - - // Proposal router - router types.Router -} - -/* TODO methods - should be similar to gov -- GetGroup -- SetGroup - -- AddVote - -*/ diff --git a/x/groupgov/types/permissions.go b/x/groupgov/types/permissions.go deleted file mode 100644 index 4024d8d3..00000000 --- a/x/groupgov/types/permissions.go +++ /dev/null @@ -1,50 +0,0 @@ -package types - -import ( - "github.com/cosmos/cosmos-sdk/x/gov" - "github.com/cosmos/cosmos-sdk/x/params" - cbtypes "github.com/kava-labs/kava/x/circuit-breaker/types" -) - -// EXAMPLE PERMISSIONS ------------------------------ - -// Allow only changes to inflation_rate -type InflationRateChangePermission struct{} - -func (InflationRateChangePermission) Allows(p gov.Proposal) bool { - pcp, _ := p.Content.(params.ParameterChangeProposal) - for _, pc := range pcp.Changes { - if pc.Key == "inflation_rate" { - return true - } - } - return false -} - -// Allow only circuit breaking of the CDP Deposit msg -type CircuitBreakCDPDepsitPermission struct{} - -func (CircuitBreakCDPDepsitPermission) Allows(p gov.Proposal) bool { - cbp, _ := p.Content.(cbtypes.CircuitBreakProposal) - for _, r := range cbp.MsgRoutes { - if r.Route == "cdp" && r.Msg == "MsgCDPDeposit" { - return true - } - } - return false -} - -// Same as above but the route the permssion allows can be set -type CircuitBreakPermission struct { - MsgRoute cbtypes.MsgRoute -} - -func (perm CircuitBreakPermission) Allows(p gov.Proposal) bool { - cbp, _ := p.Content.(cbtypes.CircuitBreakProposal) - for _, r := range cbp.MsgRoutes { - if r == perm.MsgRoute { - return true - } - } - return false -} diff --git a/x/groupgov/types/types.go b/x/groupgov/types/types.go deleted file mode 100644 index 50940abd..00000000 --- a/x/groupgov/types/types.go +++ /dev/null @@ -1,37 +0,0 @@ -package types - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/gov" -) - -// A Group is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. -type Group struct { - Members []sdk.AccAddress - Permissions []Permission -} - -// Permission is anything with a method that validates whether a proposal is allowed by it or not. -// Collectively, if one permission allows a proposal then the proposal is allowed through. -type Permission interface { - Allows(gov.Proposal) bool // maybe don't reuse gov's type here -} - -// STANDARD GOV STUFF -------------------------- -// Should be much the same as in gov module, except Proposals are linked to a group ID. - -type Router struct { - // TODO -} - -type Proposal struct { - ID uint64 - groupID uint64 - // TODO -} - -type Vote struct { - proposalID uint64 - option uint64 - // TODO -} diff --git a/x/circuit-breaker/ante/ante.go b/x/shutdown/ante/ante.go similarity index 94% rename from x/circuit-breaker/ante/ante.go rename to x/shutdown/ante/ante.go index 0ad9e78c..9a748340 100644 --- a/x/circuit-breaker/ante/ante.go +++ b/x/shutdown/ante/ante.go @@ -3,7 +3,7 @@ package ante import ( "fmt" - "github.com/kava-labs/kava/x/circuit-breaker/keeper" + "github.com/kava-labs/kava/x/shutdown/keeper" sdk "github.com/cosmos/cosmos-sdk/types" ) diff --git a/x/circuit-breaker/keeper/keeper.go b/x/shutdown/keeper/keeper.go similarity index 62% rename from x/circuit-breaker/keeper/keeper.go rename to x/shutdown/keeper/keeper.go index 0a1b650a..50118606 100644 --- a/x/circuit-breaker/keeper/keeper.go +++ b/x/shutdown/keeper/keeper.go @@ -2,7 +2,7 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/kava-labs/kava/x/circuit-breaker/types" + "github.com/kava-labs/kava/x/shutdown/types" ) // Keeper stores routes that have been "broken" @@ -11,12 +11,9 @@ type Keeper struct { func (k Keeper) GetMsgRoutes(ctx sdk.Context) []types.MsgRoute { // TODO + return []types.MsgRoute{} } func (k Keeper) SetMsgRoutes(ctx sdk.Context, routes []types.MsgRoute) { // TODO } - -func HandleCircuitBreakerProposal(ctx sdk.Context, k Keeper, c types.CircuitBreakProposal) { - k.SetMsgRoutes(ctx, c.MsgRoutes) -} diff --git a/x/shutdown/proposal_handler.go b/x/shutdown/proposal_handler.go new file mode 100644 index 00000000..97d2a251 --- /dev/null +++ b/x/shutdown/proposal_handler.go @@ -0,0 +1,30 @@ +package shutdown + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + + "github.com/kava-labs/kava/x/shutdown/keeper" + "github.com/kava-labs/kava/x/shutdown/types" +) + +func NewShutdownProposalHandler(k keeper.Keeper) gov.Handler { + return func(ctx sdk.Context, content gov.Content) sdk.Error { + switch c := content.(type) { + case types.ShutdownProposal: + return handleShutdownProposal(ctx, k, c) + + default: + errMsg := fmt.Sprintf("unrecognized %s proposal content type: %T", types.ModuleName, c) + return sdk.ErrUnknownRequest(errMsg) + } + } +} + +func handleShutdownProposal(ctx sdk.Context, k keeper.Keeper, c types.ShutdownProposal) sdk.Error { + // TODO validate proposal + k.SetMsgRoutes(ctx, c.MsgRoutes) + return nil +} diff --git a/x/circuit-breaker/spec/README.md b/x/shutdown/spec/README.md similarity index 83% rename from x/circuit-breaker/spec/README.md rename to x/shutdown/spec/README.md index 5c53f051..96a98410 100644 --- a/x/circuit-breaker/spec/README.md +++ b/x/shutdown/spec/README.md @@ -1,11 +1,11 @@ -# `circuit-breaker` +# `shutdown` ## Table of Contents ## Overview -The `x/circuit-breaker` module allows certain message types to be disabled based on governance votes. +The `x/shutdown` module allows certain message types to be disabled based on governance votes. Msgs and routes are disabled via an antehandler decorator. The decorator checks incoming all txs and rejects them if they contain a disallowed msg type. Disallowed msg types are stored in a circuit breaker keeper. diff --git a/x/circuit-breaker/types/types.go b/x/shutdown/types/types.go similarity index 56% rename from x/circuit-breaker/types/types.go rename to x/shutdown/types/types.go index 8f700025..110b5349 100644 --- a/x/circuit-breaker/types/types.go +++ b/x/shutdown/types/types.go @@ -11,33 +11,33 @@ type MsgRoute struct { } const ( - ProposalTypeCircuitBreak = "CircuitBreak" + ProposalTypeShutdown = "Shutdown" ) -// Assert CircuitBreakProposal implements govtypes.Content at compile-time -var _ govtypes.Content = CircuitBreakProposal{} +// Assert ShutdownProposal implements govtypes.Content at compile-time +var _ govtypes.Content = ShutdownProposal{} -type CircuitBreakProposal struct { +type ShutdownProposal struct { Title string Description string MsgRoutes []MsgRoute } // GetTitle returns the title of a community pool spend proposal. -func (cbp CircuitBreakProposal) GetTitle() string { return cbp.Title } +func (sp ShutdownProposal) GetTitle() string { return sp.Title } // GetDescription returns the description of a community pool spend proposal. -func (cbp CircuitBreakProposal) GetDescription() string { return cbp.Description } +func (sp ShutdownProposal) GetDescription() string { return sp.Description } // GetDescription returns the routing key of a community pool spend proposal. -func (cbp CircuitBreakProposal) ProposalRoute() string { return RouterKey } +func (sp ShutdownProposal) ProposalRoute() string { return RouterKey } // ProposalType returns the type of a community pool spend proposal. -func (cbp CircuitBreakProposal) ProposalType() string { return ProposalTypeCircuitBreak } +func (sp ShutdownProposal) ProposalType() string { return ProposalTypeShutdown } // ValidateBasic runs basic stateless validity checks -func (cbp CircuitBreakProposal) ValidateBasic() sdk.Error { - err := govtypes.ValidateAbstract(DefaultCodespace, cbp) +func (sp ShutdownProposal) ValidateBasic() sdk.Error { + err := govtypes.ValidateAbstract(DefaultCodespace, sp) if err != nil { return err } @@ -46,15 +46,16 @@ func (cbp CircuitBreakProposal) ValidateBasic() sdk.Error { } // String implements the Stringer interface. -func (cbp CircuitBreakProposal) String() string { +func (sp ShutdownProposal) String() string { // TODO + return "" } const ( DefaultCodespace sdk.CodespaceType = ModuleName // ModuleName is the module name constant used in many places - ModuleName = "circuit-breaker" + ModuleName = "shutdown" // RouterKey is the message route for distribution RouterKey = ModuleName From 54c2e44a2d6e227c45109d02f7f40f64e4c040e4 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 4 Mar 2020 19:50:30 +0000 Subject: [PATCH 05/54] add antehandler to app --- app/app.go | 31 +++++++++++++++++++++++++------ x/committee/spec/README.md | 2 +- x/shutdown/ante/ante.go | 22 ++++++++++------------ x/shutdown/spec/README.md | 2 +- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/app/app.go b/app/app.go index 3d4daa72..51f811db 100644 --- a/app/app.go +++ b/app/app.go @@ -4,11 +4,6 @@ import ( "io" "os" - "github.com/kava-labs/kava/x/auction" - "github.com/kava-labs/kava/x/cdp" - "github.com/kava-labs/kava/x/pricefeed" - validatorvesting "github.com/kava-labs/kava/x/validator-vesting" - abci "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/log" @@ -32,6 +27,13 @@ import ( "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/x/supply" + + "github.com/kava-labs/kava/x/auction" + "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/pricefeed" + validatorvesting "github.com/kava-labs/kava/x/validator-vesting" + shutdownAnte "github.com/kava-labs/kava/x/shutdown/ante" + "github.com/kava-labs/kava/x/shutdown" ) const ( @@ -319,7 +321,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // initialize the app app.SetInitChainer(app.InitChainer) app.SetBeginBlocker(app.BeginBlocker) - app.SetAnteHandler(auth.NewAnteHandler(app.accountKeeper, app.supplyKeeper, auth.DefaultSigVerificationGasConsumer)) + app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.supplyKeeper, app.shutdownKeeper, auth.DefaultSigVerificationGasConsumer)) app.SetEndBlocker(app.EndBlocker) // load store @@ -333,6 +335,23 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, return app } +func NewAnteHandler(ak auth.AccountKeeper, supplyKeeper supply.SupplyKeeper, shutdownKeeper shutdown.Keeper, sigGasConsumer SignatureVerificationGasConsumer) sdk.AnteHandler { + return sdk.ChainAnteDecorators( + auth.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first + shutdownAnte.NewDisableMsgDecorator(shutdownKeeper) + auth.NewMempoolFeeDecorator(), + auth.NewValidateBasicDecorator(), + auth.NewValidateMemoDecorator(ak), + auth.NewConsumeGasForTxSizeDecorator(ak), + auth.NewSetPubKeyDecorator(ak), // SetPubKeyDecorator must be called before all signature verification decorators + auth.NewValidateSigCountDecorator(ak), + auth.NewDeductFeeDecorator(ak, supplyKeeper), + auth.NewSigGasConsumeDecorator(ak, sigGasConsumer), + auth.NewSigVerificationDecorator(ak), + auth.NewIncrementSequenceDecorator(ak), // innermost AnteDecorator + ) +} + // custom tx codec func MakeCodec() *codec.Codec { var cdc = codec.New() diff --git a/x/committee/spec/README.md b/x/committee/spec/README.md index ac8103d5..f084da68 100644 --- a/x/committee/spec/README.md +++ b/x/committee/spec/README.md @@ -15,7 +15,7 @@ Members vote on proposals, with just simple one vote per member, no deposits or A permission acts as a filter for incoming gov proposals, rejecting them if they do not pass. A permission can be anything with a method `Allows(p Proposal) bool`. They reject all proposals that they don't explicitly allow. -This allows permissions to be parameterized to allow fine grained control specified at runtime. For example a generic parameter permission type can exist, but then on a live chain a permission can be added to a group to allow them to only change a particular param, even restricting the range of allowed change. +This allows permissions to be parameterized to allow fine grained control specified at runtime. For example a generic parameter permission type can allow a group to only change a particular param, or only change params within a certain percentage. Design Alternatives diff --git a/x/shutdown/ante/ante.go b/x/shutdown/ante/ante.go index 9a748340..a19da955 100644 --- a/x/shutdown/ante/ante.go +++ b/x/shutdown/ante/ante.go @@ -8,30 +8,28 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// CircuitBreakerDecorator needs to be combined with other standard decorators (from auth) to create the app's AnteHandler. - -// CircuitBreakerDecorator errors if a tx contains a disallowed msg type -// Call next AnteHandler if all msgs are allowed -type CircuitBreakerDecorator struct { - cbk keeper.Keeper +// DisableMsgDecorator errors if a tx contains a disallowed msg type and calls the next AnteHandler if all msgs are allowed +type DisableMsgDecorator struct { + shutdownKeeper keeper.Keeper } -func NewCircuitBreakerDecorator(cbk keeper.Keeper) CircuitBreakerDecorator { - return CircuitBreakerDecorator{ - cbk: cbk, +func NewDisableMsgDecorator(shutdownKeeper keeper.Keeper) DisableMsgDecorator { + return DisableMsgDecorator{ + shutdownKeeper: shutdownKeeper, } } -func (cbd CircuitBreakerDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { +func (dmd DisableMsgDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { // get msg route, error if not allowed - disallowedRoutes := cbd.cbk.GetMsgRoutes(ctx) + disallowedRoutes := dmd.shutdownKeeper.GetMsgRoutes(ctx) for _, m := range tx.GetMsgs() { for _, r := range disallowedRoutes { if r.Route == m.Route() && r.Msg == m.Type() { - return ctx, fmt.Errorf("route %s has been circuit broken, tx rejected", r) + return ctx, fmt.Errorf("route %s has been disabled, tx rejected", r) } } } + // otherwise continue to next antehandler decorator return next(ctx, tx, simulate) } diff --git a/x/shutdown/spec/README.md b/x/shutdown/spec/README.md index 96a98410..2bfe0107 100644 --- a/x/shutdown/spec/README.md +++ b/x/shutdown/spec/README.md @@ -14,5 +14,5 @@ The list of disallowed msg types is updated via a custom governance proposal and Design Alternatives: -- store list of disallowed msg types in params, then don't need custom gov proposal +- store list of disallowed msg types in params, then we don't need the custom gov proposal - replace the app Router with a custom one to avoid using the antehandler - can't be done with current baseapp, but v0.38.x enables this. (https://github.com/cosmos/cosmos-sdk/issues/5455) \ No newline at end of file From cae6cb196cdfa6f9dc7c395df72903a7c068cd6a Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 10 Mar 2020 21:41:10 +0000 Subject: [PATCH 06/54] make builds pass --- x/committee/abci.go | 18 ++++++++--------- x/committee/handler.go | 4 +++- x/committee/keeper/keeper.go | 33 +++++++++++++++++++++++++++----- x/committee/types/permissions.go | 14 +++++++------- x/committee/types/types.go | 7 ++++++- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/x/committee/abci.go b/x/committee/abci.go index 50bb9dc2..54e706d8 100644 --- a/x/committee/abci.go +++ b/x/committee/abci.go @@ -1,12 +1,12 @@ package committee -func BeginBlocker() { - // TODO much the same as the current gov endblocker does +// func BeginBlocker() { +// // TODO much the same as the current gov endblocker does - // Get all active proposals - // If voting periods are over, tally up the results - // If a proposal passes run it through the correct handler - // Handler need to be registered in app.go as they are for the current gov module - handler := keeper.Router().GetRoute(proposal.ProposalRoute()) - err := handler(ctx, proposal.Content) -} +// // Get all active proposals +// // If voting periods are over, tally up the results +// // If a proposal passes run it through the correct handler +// // Handler need to be registered in app.go as they are for the current gov module +// handler := keeper.Router().GetRoute(proposal.ProposalRoute()) +// err := handler(ctx, proposal.Content) +// } diff --git a/x/committee/handler.go b/x/committee/handler.go index bc23eb5a..f3f9b03f 100644 --- a/x/committee/handler.go +++ b/x/committee/handler.go @@ -1,13 +1,14 @@ package committee // committee, subcommittee, council, caucus, commission, synod, board - +/* import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/committee/keeper" + "github.com/kava-labs/kava/x/committee/types" ) @@ -45,3 +46,4 @@ func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Resu return sdk.Result{} } +*/ diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 8dc5158d..3952f0e1 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -1,4 +1,4 @@ -package types +package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" @@ -29,20 +29,20 @@ type Keeper struct { */ -func (k Keeper) SubmitProposal(ctx sdk.Context, msg types.MsgSubmitProposal) sdk.Error { +func (k Keeper) SubmitProposal(ctx sdk.Context, proposal types.Proposal) sdk.Error { // TODO Limit proposals to only be submitted by group members // Check group has permissions to enact proposal. As long as one permission allows the proposal then it goes through. Its the OR of all permissions. - committee, _ := k.GetCommittee(ctx, msg.CommitteeID) + committee, _ := k.GetCommittee(ctx, proposal.CommitteeID) hasPermissions := false for _, p := range committee.Permissions { - if p.Allows(msg.Proposal) { + if p.Allows(proposal) { hasPermissions = true break } } if !hasPermissions { - return sdk.ErrInternal("committee does not have permissions to enact proposal").Result() + return sdk.ErrInternal("committee does not have permissions to enact proposal") } // TODO validate proposal by running it with cached context like how gov does it @@ -59,3 +59,26 @@ func (k Keeper) AddVote(ctx sdk.Context, msg types.MsgVote) sdk.Error { */ return nil } + +// -------------------- + +func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committee, bool) { + return types.Committee{}, false +} +func (k Keeper) SetCommittee(ctx sdk.Context, committee types.Committee) { + +} + +func (k Keeper) GetVote(ctx sdk.Context, voteID uint64) (types.Vote, bool) { + return types.Vote{}, false +} +func (k Keeper) SetVote(ctx sdk.Context, vote types.Vote) { + +} + +func (k Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (types.Proposal, bool) { + return types.Proposal{}, false +} +func (k Keeper) SetProposal(ctx sdk.Context, proposal types.Proposal) { + +} diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index d560296d..8338ea1d 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -11,10 +11,10 @@ import ( // Allow only changes to inflation_rate type InflationRateChangePermission struct{} -var _ types.Permission = InflationRateChangePermission +var _ Permission = InflationRateChangePermission{} -func (InflationRateChangePermission) Allows(p gov.Proposal) bool { - pcp, ok := p.Content.(params.ParameterChangeProposal) +func (InflationRateChangePermission) Allows(p gov.Content) bool { + pcp, ok := p.(params.ParameterChangeProposal) if !ok { return false } @@ -29,7 +29,7 @@ func (InflationRateChangePermission) Allows(p gov.Proposal) bool { // Allow only shutdown of the CDP Deposit msg type ShutdownCDPDepsitPermission struct{} -var _ types.Permission = ShutdownCDPDepsitPermission +var _ Permission = ShutdownCDPDepsitPermission{} func (ShutdownCDPDepsitPermission) Allows(p gov.Content) bool { sdp, ok := p.(sdtypes.ShutdownProposal) @@ -46,13 +46,13 @@ func (ShutdownCDPDepsitPermission) Allows(p gov.Content) bool { // Same as above but the route isn't static type GeneralShutdownPermission struct { - MsgRoute cbtypes.MsgRoute + MsgRoute sdtypes.MsgRoute } -var _ types.Permission = GeneralShutdownPermission +var _ Permission = GeneralShutdownPermission{} func (perm GeneralShutdownPermission) Allows(p gov.Content) bool { - sdp, ok := p.Content.(sdtypes.ShutdownProposal) + sdp, ok := p.(sdtypes.ShutdownProposal) if !ok { return false } diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 2994a179..0aac38b7 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -22,8 +22,9 @@ type Permission interface { type Proposal struct { gov.Content ID uint64 - committeeID uint64 + CommitteeID uint64 // TODO + // could store votes on the proposal object } type Vote struct { @@ -31,3 +32,7 @@ type Vote struct { Voter sdk.AccAddress Option byte } + +// Genesis ------------------- +// Ok just to dump everything to json and reload - if time involved then begin blocker will take care of closing expired proposals. And it won't enact proposals because they would've been immediately enacted before the halt if they passed. +// committee, proposals, votes From f2e4956d8803dfc8f478aeb2c1bf4a7231b46498 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 10 Mar 2020 22:29:16 +0000 Subject: [PATCH 07/54] hook into app to get integration tests running --- app/app.go | 50 +++++---- app/test_common.go | 2 + x/committee/alias.go | 36 +++++++ x/committee/keeper/keeper.go | 18 +++- x/committee/keeper/keeper_test.go | 35 +++++++ x/committee/module.go | 162 ++++++++++++++++++++++++++++++ x/committee/types/codec.go | 22 ++++ x/committee/types/keys.go | 19 ++++ 8 files changed, 319 insertions(+), 25 deletions(-) create mode 100644 x/committee/alias.go create mode 100644 x/committee/keeper/keeper_test.go create mode 100644 x/committee/module.go create mode 100644 x/committee/types/codec.go create mode 100644 x/committee/types/keys.go diff --git a/app/app.go b/app/app.go index 51f811db..151c5836 100644 --- a/app/app.go +++ b/app/app.go @@ -30,10 +30,9 @@ import ( "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/committee" "github.com/kava-labs/kava/x/pricefeed" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" - shutdownAnte "github.com/kava-labs/kava/x/shutdown/ante" - "github.com/kava-labs/kava/x/shutdown" ) const ( @@ -64,6 +63,7 @@ var ( auction.AppModuleBasic{}, cdp.AppModuleBasic{}, pricefeed.AppModuleBasic{}, + committee.AppModuleBasic{}, ) // module account permissions @@ -107,6 +107,7 @@ type App struct { auctionKeeper auction.Keeper cdpKeeper cdp.Keeper pricefeedKeeper pricefeed.Keeper + committeeKeeper committee.Keeper // the module manager mm *module.Manager @@ -130,7 +131,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, bam.MainStoreKey, auth.StoreKey, staking.StoreKey, supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, gov.StoreKey, params.StoreKey, validatorvesting.StoreKey, - auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, + auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, committee.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(params.TStoreKey) @@ -245,6 +246,11 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.auctionKeeper, app.supplyKeeper, cdp.DefaultCodespace) + app.committeeKeeper = committee.NewKeeper( + app.cdc, + keys[committee.StoreKey], + // TODO blacklist module addresses? + ) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -268,6 +274,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, auction.NewAppModule(app.auctionKeeper, app.supplyKeeper), cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), pricefeed.NewAppModule(app.pricefeedKeeper), + committee.NewAppModule(app.committeeKeeper), ) // During begin block slashing happens after distr.BeginBlocker so that @@ -287,7 +294,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName, staking.ModuleName, bank.ModuleName, slashing.ModuleName, gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName, - pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, // TODO is this order ok? + pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, committee.ModuleName, // TODO is this order ok? ) app.mm.RegisterInvariants(&app.crisisKeeper) @@ -310,6 +317,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), // TODO how is the order be decided here? Is this order correct? pricefeed.NewAppModule(app.pricefeedKeeper), auction.NewAppModule(app.auctionKeeper, app.supplyKeeper), + // TODO committee ) app.sm.RegisterStoreDecoders() @@ -321,7 +329,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // initialize the app app.SetInitChainer(app.InitChainer) app.SetBeginBlocker(app.BeginBlocker) - app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.supplyKeeper, app.shutdownKeeper, auth.DefaultSigVerificationGasConsumer)) + // app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.supplyKeeper, app.shutdownKeeper, auth.DefaultSigVerificationGasConsumer)) app.SetEndBlocker(app.EndBlocker) // load store @@ -335,22 +343,22 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, return app } -func NewAnteHandler(ak auth.AccountKeeper, supplyKeeper supply.SupplyKeeper, shutdownKeeper shutdown.Keeper, sigGasConsumer SignatureVerificationGasConsumer) sdk.AnteHandler { - return sdk.ChainAnteDecorators( - auth.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first - shutdownAnte.NewDisableMsgDecorator(shutdownKeeper) - auth.NewMempoolFeeDecorator(), - auth.NewValidateBasicDecorator(), - auth.NewValidateMemoDecorator(ak), - auth.NewConsumeGasForTxSizeDecorator(ak), - auth.NewSetPubKeyDecorator(ak), // SetPubKeyDecorator must be called before all signature verification decorators - auth.NewValidateSigCountDecorator(ak), - auth.NewDeductFeeDecorator(ak, supplyKeeper), - auth.NewSigGasConsumeDecorator(ak, sigGasConsumer), - auth.NewSigVerificationDecorator(ak), - auth.NewIncrementSequenceDecorator(ak), // innermost AnteDecorator - ) -} +// func NewAnteHandler(ak auth.AccountKeeper, supplyKeeper supply.Keeper, shutdownKeeper shutdown.Keeper, sigGasConsumer SignatureVerificationGasConsumer) sdk.AnteHandler { +// return sdk.ChainAnteDecorators( +// auth.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first +// shutdownAnte.NewDisableMsgDecorator(shutdownKeeper), +// auth.NewMempoolFeeDecorator(), +// auth.NewValidateBasicDecorator(), +// auth.NewValidateMemoDecorator(ak), +// auth.NewConsumeGasForTxSizeDecorator(ak), +// auth.NewSetPubKeyDecorator(ak), // SetPubKeyDecorator must be called before all signature verification decorators +// auth.NewValidateSigCountDecorator(ak), +// auth.NewDeductFeeDecorator(ak, supplyKeeper), +// auth.NewSigGasConsumeDecorator(ak, sigGasConsumer), +// auth.NewSigVerificationDecorator(ak), +// auth.NewIncrementSequenceDecorator(ak), // innermost AnteDecorator +// ) +// } // custom tx codec func MakeCodec() *codec.Codec { diff --git a/app/test_common.go b/app/test_common.go index 7bb0da05..3d687a5a 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -29,6 +29,7 @@ import ( "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/committee" "github.com/kava-labs/kava/x/pricefeed" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" ) @@ -67,6 +68,7 @@ func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper } func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper } func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } +func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper } // This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp { diff --git a/x/committee/alias.go b/x/committee/alias.go new file mode 100644 index 00000000..41aadd1b --- /dev/null +++ b/x/committee/alias.go @@ -0,0 +1,36 @@ +// nolint +// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen) +package committee + +import ( + "github.com/kava-labs/kava/x/committee/keeper" + "github.com/kava-labs/kava/x/committee/types" +) + +const ( + ModuleName = types.ModuleName + StoreKey = types.StoreKey +) + +var ( + // function aliases + NewKeeper = keeper.NewKeeper + RegisterCodec = types.RegisterCodec + + // variable aliases + ModuleCdc = types.ModuleCdc +) + +type ( + Keeper = keeper.Keeper + Committee = types.Committee + GeneralShutdownPermission = types.GeneralShutdownPermission + GroupChangeProposal = types.GroupChangeProposal + InflationRateChangePermission = types.InflationRateChangePermission + MsgSubmitProposal = types.MsgSubmitProposal + MsgVote = types.MsgVote + Permission = types.Permission + Proposal = types.Proposal + ShutdownCDPDepsitPermission = types.ShutdownCDPDepsitPermission + Vote = types.Vote +) diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 3952f0e1..a35e1b9b 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -1,17 +1,27 @@ package keeper import ( + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + //govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/kava-labs/kava/x/committee/types" ) type Keeper struct { - // TODO other stuff as needed + cdc *codec.Codec + storeKey sdk.StoreKey - // Proposal router - router govtypes.Router + // TODO Proposal router + //router govtypes.Router +} + +func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey) Keeper { + return Keeper{ + cdc: cdc, + storeKey: storeKey, + } } /* TODO keeper methods - very similar to gov diff --git a/x/committee/keeper/keeper_test.go b/x/committee/keeper/keeper_test.go new file mode 100644 index 00000000..f38d3acd --- /dev/null +++ b/x/committee/keeper/keeper_test.go @@ -0,0 +1,35 @@ +package keeper_test + +import ( + "testing" + "github.com/stretchr/testify/suite" + + abci "github.com/tendermint/tendermint/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/committee/keeper" +) + +type KeeperTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context +} + +func (suite *KeeperTestSuite) SetupTest() { + + suite.app = app.NewTestApp() + suite.keeper = suite.app.GetCommitteeKeeper() + suite.ctx = suite.app.NewContext(true, abci.Header{}) +} + +func (suite *KeeperTestSuite) TestGetSetCommittee() { +} + + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} \ No newline at end of file diff --git a/x/committee/module.go b/x/committee/module.go new file mode 100644 index 00000000..fe068a93 --- /dev/null +++ b/x/committee/module.go @@ -0,0 +1,162 @@ +package committee + +import ( + "encoding/json" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + abci "github.com/tendermint/tendermint/abci/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + // TODO_ module.AppModuleSimulation = AppModuleSimulation{} +) + +// AppModuleBasic app module basics object +type AppModuleBasic struct{} + +// Name get module name +func (AppModuleBasic) Name() string { + return ModuleName +} + +// RegisterCodec register module codec +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) +} + +// DefaultGenesis default genesis state +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + //return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) + return nil +} + +// ValidateGenesis module validate genesis +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + // var gs GenesisState + // err := ModuleCdc.UnmarshalJSON(bz, &gs) + // if err != nil { + // return err + // } + // return gs.Validate() + return nil +} + +// RegisterRESTRoutes registers the REST routes for the module. +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { + //rest.RegisterRoutes(ctx, rtr) +} + +// GetTxCmd returns the root tx command for the module. +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + //return cli.GetTxCmd(cdc) + return nil +} + +// GetQueryCmd returns the root query command for the auction module. +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + //return cli.GetQueryCmd(StoreKey, cdc) + return nil +} + +//____________________________________________________________________________ + +// TODO +// // AppModuleSimulation defines the module simulation functions used by the module. +// type AppModuleSimulation struct{} + +// // RegisterStoreDecoder registers a decoder for the module's types +// func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { +// sdr[StoreKey] = simulation.DecodeStore +// } + +// // GenerateGenesisState creates a randomized GenState of the module +// func (AppModuleSimulation) GenerateGenesisState(simState *module.SimulationState) { +// simulation.RandomizedGenState(simState) +// } + +// // RandomizedParams creates randomized param changes for the simulator. +// func (AppModuleSimulation) RandomizedParams(r *rand.Rand) []sim.ParamChange { +// return simulation.ParamChanges(r) +// } + +//____________________________________________________________________________ + +// AppModule app module type +type AppModule struct { + AppModuleBasic + // TODO AppModuleSimulation + + keeper Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// Name module name +func (AppModule) Name() string { + return ModuleName +} + +// RegisterInvariants register module invariants +func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// Route module message route name +func (AppModule) Route() string { + return ModuleName +} + +// NewHandler module handler +func (am AppModule) NewHandler() sdk.Handler { + //return NewHandler(am.keeper) + return nil +} + +// QuerierRoute module querier route name +func (AppModule) QuerierRoute() string { + return ModuleName +} + +// NewQuerierHandler module querier +func (am AppModule) NewQuerierHandler() sdk.Querier { + // return NewQuerier(am.keeper) + return nil +} + +// InitGenesis module init-genesis +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + // var genesisState GenesisState + // ModuleCdc.MustUnmarshalJSON(data, &genesisState) + // InitGenesis(ctx, am.keeper, am.pricefeedKeeper, genesisState) + + return []abci.ValidatorUpdate{} +} + +// ExportGenesis module export genesis +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + // gs := ExportGenesis(ctx, am.keeper) + // return ModuleCdc.MustMarshalJSON(gs) + return nil +} + +// BeginBlock module begin-block +func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { + // TODO BeginBlocker(ctx, req, am.keeper) +} + +// EndBlock module end-block +func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/committee/types/codec.go b/x/committee/types/codec.go new file mode 100644 index 00000000..898769d3 --- /dev/null +++ b/x/committee/types/codec.go @@ -0,0 +1,22 @@ +package types + +import "github.com/cosmos/cosmos-sdk/codec" + +// ModuleCdc generic sealed codec to be used throughout module +var ModuleCdc *codec.Codec + +func init() { + cdc := codec.New() + RegisterCodec(cdc) + ModuleCdc = cdc.Seal() +} + +// RegisterCodec registers the necessary types for the module +func RegisterCodec(cdc *codec.Codec) { + // TODO + // cdc.RegisterConcrete(MsgCreateCDP{}, "cdp/MsgCreateCDP", nil) + // cdc.RegisterConcrete(MsgDeposit{}, "cdp/MsgDeposit", nil) + // cdc.RegisterConcrete(MsgWithdraw{}, "cdp/MsgWithdraw", nil) + // cdc.RegisterConcrete(MsgDrawDebt{}, "cdp/MsgDrawDebt", nil) + // cdc.RegisterConcrete(MsgRepayDebt{}, "cdp/MsgRepayDebt", nil) +} diff --git a/x/committee/types/keys.go b/x/committee/types/keys.go new file mode 100644 index 00000000..1a945680 --- /dev/null +++ b/x/committee/types/keys.go @@ -0,0 +1,19 @@ +package types + +const ( + // ModuleName The name that will be used throughout the module + ModuleName = "committee" + + // StoreKey Top level store key where all module items will be stored + StoreKey = ModuleName +/* + // RouterKey Top level router key + RouterKey = ModuleName + + // QuerierRoute Top level query string + QuerierRoute = ModuleName + + // DefaultParamspace default name for parameter store + DefaultParamspace = ModuleName + */ +) \ No newline at end of file From a145846ed2dafd851f3f548b236eb4c0fbb31f30 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 10 Mar 2020 23:16:22 +0000 Subject: [PATCH 08/54] add get set methods --- x/committee/keeper/keeper.go | 55 ++++++++++++++++++++++------- x/committee/keeper/keeper_test.go | 58 ++++++++++++++++++++++++++++--- x/committee/types/keys.go | 47 +++++++++++++++++++++++-- x/committee/types/types.go | 1 + 4 files changed, 142 insertions(+), 19 deletions(-) diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index a35e1b9b..3e403955 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -2,6 +2,7 @@ package keeper import ( "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" //govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" @@ -70,25 +71,53 @@ func (k Keeper) AddVote(ctx sdk.Context, msg types.MsgVote) sdk.Error { return nil } -// -------------------- - func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committee, bool) { - return types.Committee{}, false + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix) + bz := store.Get(types.GetKeyFromID(committeeID)) + if bz == nil { + return types.Committee{}, false + } + var committee types.Committee + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &committee) + return committee, true } + func (k Keeper) SetCommittee(ctx sdk.Context, committee types.Committee) { - -} - -func (k Keeper) GetVote(ctx sdk.Context, voteID uint64) (types.Vote, bool) { - return types.Vote{}, false -} -func (k Keeper) SetVote(ctx sdk.Context, vote types.Vote) { - + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(committee) + store.Set(types.GetKeyFromID(committee.ID), bz) } func (k Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (types.Proposal, bool) { - return types.Proposal{}, false + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix) + bz := store.Get(types.GetKeyFromID(proposalID)) + if bz == nil { + return types.Proposal{}, false + } + var proposal types.Proposal + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &proposal) + return proposal, true } -func (k Keeper) SetProposal(ctx sdk.Context, proposal types.Proposal) { +func (k Keeper) SetProposal(ctx sdk.Context, proposal types.Proposal) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(proposal) + store.Set(types.GetKeyFromID(proposal.ID), bz) +} + +func (k Keeper) GetVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) (types.Vote, bool) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix) + bz := store.Get(types.GetVoteKey(proposalID, voter)) + if bz == nil { + return types.Vote{}, false + } + var vote types.Vote + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &vote) + return vote, true +} + +func (k Keeper) SetVote(ctx sdk.Context, vote types.Vote) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(vote) + store.Set(types.GetVoteKey(vote.ProposalID, vote.Voter), bz) } diff --git a/x/committee/keeper/keeper_test.go b/x/committee/keeper/keeper_test.go index f38d3acd..90f5556d 100644 --- a/x/committee/keeper/keeper_test.go +++ b/x/committee/keeper/keeper_test.go @@ -2,10 +2,12 @@ package keeper_test import ( "testing" + + "github.com/kava-labs/kava/x/committee/types" "github.com/stretchr/testify/suite" - abci "github.com/tendermint/tendermint/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/committee/keeper" @@ -17,19 +19,67 @@ type KeeperTestSuite struct { keeper keeper.Keeper app app.TestApp ctx sdk.Context + + addresses []sdk.AccAddress } func (suite *KeeperTestSuite) SetupTest() { - suite.app = app.NewTestApp() + suite.app = app.NewTestApp() suite.keeper = suite.app.GetCommitteeKeeper() - suite.ctx = suite.app.NewContext(true, abci.Header{}) + suite.ctx = suite.app.NewContext(true, abci.Header{}) + _, suite.addresses = app.GeneratePrivKeyAddressPairs(2) } func (suite *KeeperTestSuite) TestGetSetCommittee() { + // test setup + com := types.Committee{ + ID: 12, + // TODO other fields + } + + // write and read from store + suite.keeper.SetCommittee(suite.ctx, com) + readCommittee, found := suite.keeper.GetCommittee(suite.ctx, com.ID) + + // check before and after match + suite.True(found) + suite.Equal(com, readCommittee) } +func (suite *KeeperTestSuite) TestGetSetProposal() { + // test setup + prop := types.Proposal{ + ID: 12, + // TODO other fields + } + + // write and read from store + suite.keeper.SetProposal(suite.ctx, prop) + readProposal, found := suite.keeper.GetProposal(suite.ctx, prop.ID) + + // check before and after match + suite.True(found) + suite.Equal(prop, readProposal) +} + +func (suite *KeeperTestSuite) TestGetSetVote() { + // test setup + vote := types.Vote{ + ProposalID: 12, + Voter: suite.addresses[0], + // TODO other fields + } + + // write and read from store + suite.keeper.SetVote(suite.ctx, vote) + readVote, found := suite.keeper.GetVote(suite.ctx, vote.ProposalID, vote.Voter) + + // check before and after match + suite.True(found) + suite.Equal(vote, readVote) +} func TestKeeperTestSuite(t *testing.T) { suite.Run(t, new(KeeperTestSuite)) -} \ No newline at end of file +} diff --git a/x/committee/types/keys.go b/x/committee/types/keys.go index 1a945680..09eff742 100644 --- a/x/committee/types/keys.go +++ b/x/committee/types/keys.go @@ -1,11 +1,18 @@ package types +import ( + "encoding/binary" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + const ( // ModuleName The name that will be used throughout the module ModuleName = "committee" // StoreKey Top level store key where all module items will be stored StoreKey = ModuleName + /* // RouterKey Top level router key RouterKey = ModuleName @@ -15,5 +22,41 @@ const ( // DefaultParamspace default name for parameter store DefaultParamspace = ModuleName - */ -) \ No newline at end of file +*/ +) + +// Key prefixes +var ( + CommitteeKeyPrefix = []byte{0x00} // prefix for keys that store committees + ProposalKeyPrefix = []byte{0x01} // prefix for keys that store proposals + VoteKeyPrefix = []byte{0x02} // prefix for keys that store votes + //AuctionByTimeKeyPrefix = []byte{0x01} // prefix for keys that are part of the auctionsByTime index + + //NextAuctionIDKey = []byte{0x02} // key for the next auction id +) + +// GetKeyFromID returns the bytes to use as a key for a uint64 id +func GetKeyFromID(id uint64) []byte { + return uint64ToBytes(id) +} + +func GetVoteKey(proposalID uint64, voter sdk.AccAddress) []byte { + return append(GetKeyFromID(proposalID), voter.Bytes()...) +} + +// // GetAuctionByTimeKey returns the key for iterating auctions by time +// func GetAuctionByTimeKey(endTime time.Time, auctionID uint64) []byte { +// return append(sdk.FormatTimeBytes(endTime), Uint64ToBytes(auctionID)...) +// } + +// Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys. +func uint64ToBytes(id uint64) []byte { + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, uint64(id)) + return bz +} + +// Uint64FromBytes converts some fixed length bytes back into a uint64. +func uint64FromBytes(bz []byte) uint64 { + return binary.BigEndian.Uint64(bz) +} diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 0aac38b7..961a5a4c 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -7,6 +7,7 @@ import ( // A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. type Committee struct { + ID uint64 // TODO or a name? Members []sdk.AccAddress Permissions []Permission } From 8c64fd375044a1f5aeb68e86d52c97f411eac408 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 10 Mar 2020 23:28:25 +0000 Subject: [PATCH 09/54] add keeper delete methods --- x/committee/keeper/keeper.go | 24 ++++++++++++++++++++++++ x/committee/keeper/keeper_test.go | 23 ++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 3e403955..55b3c875 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -71,6 +71,7 @@ func (k Keeper) AddVote(ctx sdk.Context, msg types.MsgVote) sdk.Error { return nil } +// GetCommittee gets a committee from the store. func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committee, bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix) bz := store.Get(types.GetKeyFromID(committeeID)) @@ -82,12 +83,20 @@ func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committ return committee, true } +// SetCommittee puts a committee into the store. func (k Keeper) SetCommittee(ctx sdk.Context, committee types.Committee) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix) bz := k.cdc.MustMarshalBinaryLengthPrefixed(committee) store.Set(types.GetKeyFromID(committee.ID), bz) } +// DeleteCommittee removes a committee from the store. +func (k Keeper) DeleteCommittee(ctx sdk.Context, committeeID uint64) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix) + store.Delete(types.GetKeyFromID(committeeID)) +} + +// GetProposal gets a proposal from the store. func (k Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (types.Proposal, bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix) bz := store.Get(types.GetKeyFromID(proposalID)) @@ -99,12 +108,20 @@ func (k Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (types.Proposal, return proposal, true } +// SetProposal puts a proposal into the store. func (k Keeper) SetProposal(ctx sdk.Context, proposal types.Proposal) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix) bz := k.cdc.MustMarshalBinaryLengthPrefixed(proposal) store.Set(types.GetKeyFromID(proposal.ID), bz) } +// DeleteProposal removes a proposal from the store. +func (k Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix) + store.Delete(types.GetKeyFromID(proposalID)) +} + +// GetVote gets a vote from the store. func (k Keeper) GetVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) (types.Vote, bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix) bz := store.Get(types.GetVoteKey(proposalID, voter)) @@ -116,8 +133,15 @@ func (k Keeper) GetVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress return vote, true } +// SetVote puts a vote into the store. func (k Keeper) SetVote(ctx sdk.Context, vote types.Vote) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix) bz := k.cdc.MustMarshalBinaryLengthPrefixed(vote) store.Set(types.GetVoteKey(vote.ProposalID, vote.Voter), bz) } + +// DeleteVote removes a Vote from the store. +func (k Keeper) DeleteVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix) + store.Delete(types.GetVoteKey(proposalID, voter)) +} diff --git a/x/committee/keeper/keeper_test.go b/x/committee/keeper/keeper_test.go index 90f5556d..e4fec90a 100644 --- a/x/committee/keeper/keeper_test.go +++ b/x/committee/keeper/keeper_test.go @@ -31,7 +31,7 @@ func (suite *KeeperTestSuite) SetupTest() { _, suite.addresses = app.GeneratePrivKeyAddressPairs(2) } -func (suite *KeeperTestSuite) TestGetSetCommittee() { +func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() { // test setup com := types.Committee{ ID: 12, @@ -45,6 +45,13 @@ func (suite *KeeperTestSuite) TestGetSetCommittee() { // check before and after match suite.True(found) suite.Equal(com, readCommittee) + + // delete from store + suite.keeper.DeleteCommittee(suite.ctx, com.ID) + + // check does not exist + _, found = suite.keeper.GetCommittee(suite.ctx, com.ID) + suite.False(found) } func (suite *KeeperTestSuite) TestGetSetProposal() { @@ -61,6 +68,13 @@ func (suite *KeeperTestSuite) TestGetSetProposal() { // check before and after match suite.True(found) suite.Equal(prop, readProposal) + + // delete from store + suite.keeper.DeleteProposal(suite.ctx, prop.ID) + + // check does not exist + _, found = suite.keeper.GetProposal(suite.ctx, prop.ID) + suite.False(found) } func (suite *KeeperTestSuite) TestGetSetVote() { @@ -78,6 +92,13 @@ func (suite *KeeperTestSuite) TestGetSetVote() { // check before and after match suite.True(found) suite.Equal(vote, readVote) + + // delete from store + suite.keeper.DeleteVote(suite.ctx, vote.ProposalID, vote.Voter) + + // check does not exist + _, found = suite.keeper.GetVote(suite.ctx, vote.ProposalID, vote.Voter) + suite.False(found) } func TestKeeperTestSuite(t *testing.T) { From f9dab88c165754f4880364666c0d12007c72b9f9 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 11 Mar 2020 00:58:42 +0000 Subject: [PATCH 10/54] add main keeper methods --- x/committee/keeper/keeper.go | 117 +++++++++++++++++++++--------- x/committee/keeper/keeper_test.go | 49 ++++++++++++- x/committee/types/keys.go | 4 +- x/committee/types/types.go | 23 +++++- 4 files changed, 152 insertions(+), 41 deletions(-) diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 55b3c875..535d3cb3 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -25,49 +25,51 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey) Keeper { } } -/* TODO keeper methods - very similar to gov - -- SubmitProposal validate and store a proposal, additionally setting things like timeout -- GetProposal -- SetProposal - -- AddVote - add a vote to a particular proposal from a member -- GetVote -- SetVote - -- GetCommittee -- SetCommittee - -*/ - -func (k Keeper) SubmitProposal(ctx sdk.Context, proposal types.Proposal) sdk.Error { - // TODO Limit proposals to only be submitted by group members - - // Check group has permissions to enact proposal. As long as one permission allows the proposal then it goes through. Its the OR of all permissions. - committee, _ := k.GetCommittee(ctx, proposal.CommitteeID) - hasPermissions := false - for _, p := range committee.Permissions { - if p.Allows(proposal) { - hasPermissions = true - break - } +func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, proposal types.Proposal) (uint64, sdk.Error) { + // Limit proposals to only be submitted by committee members + com, found := k.GetCommittee(ctx, proposal.CommitteeID) + if !found { + return 0, sdk.ErrInternal("committee doesn't exist") } - if !hasPermissions { - return sdk.ErrInternal("committee does not have permissions to enact proposal") + if !com.HasMember(proposer) { + return 0, sdk.ErrInternal("only member can propose proposals") + } + + // Check proposal is valid + if err := proposal.ValidateBasic(); err != nil { + return 0, err + } + + // Check group has permissions to enact proposal. + if !com.HasPermissionsFor(proposal) { + return 0, sdk.ErrInternal("committee does not have permissions to enact proposal") } // TODO validate proposal by running it with cached context like how gov does it + // what if it's not valid now but will be in the future? - // TODO store the proposal, probably put it in a queue - - return nil + // Get a new ID and store the proposal + return k.StoreNewProposal(ctx, proposal) } -func (k Keeper) AddVote(ctx sdk.Context, msg types.MsgVote) sdk.Error { - /* TODO - - validate vote - - store vote - */ +func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) sdk.Error { + // Validate + proposal, found := k.GetProposal(ctx, proposalID) + if !found { + return sdk.ErrInternal("proposal not found") + } + com, found := k.GetCommittee(ctx, proposal.CommitteeID) + if !found { + return sdk.ErrInternal("committee disbanded") + } + if !com.HasMember(voter) { + return sdk.ErrInternal("not authorized to vote on proposal") + } + + // Store vote, overwriting any prior vote + k.SetVote(ctx, types.Vote{ProposalID: proposalID, Voter: voter}) + + // TODO close vote if tally has been reached return nil } @@ -96,6 +98,49 @@ func (k Keeper) DeleteCommittee(ctx sdk.Context, committeeID uint64) { store.Delete(types.GetKeyFromID(committeeID)) } +// SetNextProposalID stores an ID to be used for the next created proposal +func (k Keeper) SetNextProposalID(ctx sdk.Context, id uint64) { + store := ctx.KVStore(k.storeKey) + store.Set(types.NextProposalIDKey, types.GetKeyFromID(id)) +} + +// GetNextProposalID reads the next available global ID from store +func (k Keeper) GetNextProposalID(ctx sdk.Context) (uint64, sdk.Error) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.NextProposalIDKey) + if bz == nil { + return 0, sdk.ErrInternal("proposal ID not set at genesis") + } + return types.Uint64FromBytes(bz), nil +} + +// IncrementNextProposalID increments the next proposal ID in the store by 1. +func (k Keeper) IncrementNextProposalID(ctx sdk.Context) sdk.Error { + id, err := k.GetNextProposalID(ctx) + if err != nil { + return err + } + k.SetNextProposalID(ctx, id+1) + return nil +} + +// StoreNewProposal stores a proposal, adding a new ID +func (k Keeper) StoreNewProposal(ctx sdk.Context, proposal types.Proposal) (uint64, sdk.Error) { + newProposalID, err := k.GetNextProposalID(ctx) + if err != nil { + return 0, err + } + proposal.ID = newProposalID + + k.SetProposal(ctx, proposal) + + err = k.IncrementNextProposalID(ctx) + if err != nil { + return 0, err + } + return newProposalID, nil +} + // GetProposal gets a proposal from the store. func (k Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (types.Proposal, bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix) diff --git a/x/committee/keeper/keeper_test.go b/x/committee/keeper/keeper_test.go index e4fec90a..5244e9d2 100644 --- a/x/committee/keeper/keeper_test.go +++ b/x/committee/keeper/keeper_test.go @@ -24,15 +24,60 @@ type KeeperTestSuite struct { } func (suite *KeeperTestSuite) SetupTest() { - suite.app = app.NewTestApp() suite.keeper = suite.app.GetCommitteeKeeper() suite.ctx = suite.app.NewContext(true, abci.Header{}) _, suite.addresses = app.GeneratePrivKeyAddressPairs(2) } +func (suite *KeeperTestSuite) TestSubmitProposal() { + testcases := []struct { + name string + proposal types.Proposal + proposer sdk.AccAddress + expectPass bool + }{ + {name: "empty proposal", proposer: suite.addresses[0], expectPass: false}, + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + _, err := suite.keeper.SubmitProposal(suite.ctx, tc.proposer, tc.proposal) + if tc.expectPass { + suite.NoError(err) + // TODO suite.keeper.GetProposal(suite.ctx, tc.proposal.ID) + } else { + suite.NotNil(err) + } + }) + } +} + +func (suite *KeeperTestSuite) TestAddVote() { + testcases := []struct { + name string + proposalID uint64 + voter sdk.AccAddress + expectPass bool + }{ + {name: "no proposal", proposalID: 9999999, voter: suite.addresses[0], expectPass: false}, + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + err := suite.keeper.AddVote(suite.ctx, tc.proposalID, tc.voter) + if tc.expectPass { + suite.NoError(err) + // TODO GetVote + } else { + suite.NotNil(err) + } + }) + } +} + func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() { - // test setup + // setup test com := types.Committee{ ID: 12, // TODO other fields diff --git a/x/committee/types/keys.go b/x/committee/types/keys.go index 09eff742..7ad1731d 100644 --- a/x/committee/types/keys.go +++ b/x/committee/types/keys.go @@ -32,7 +32,7 @@ var ( VoteKeyPrefix = []byte{0x02} // prefix for keys that store votes //AuctionByTimeKeyPrefix = []byte{0x01} // prefix for keys that are part of the auctionsByTime index - //NextAuctionIDKey = []byte{0x02} // key for the next auction id + NextProposalIDKey = []byte{0x03} // key for the next proposal id ) // GetKeyFromID returns the bytes to use as a key for a uint64 id @@ -57,6 +57,6 @@ func uint64ToBytes(id uint64) []byte { } // Uint64FromBytes converts some fixed length bytes back into a uint64. -func uint64FromBytes(bz []byte) uint64 { +func Uint64FromBytes(bz []byte) uint64 { return binary.BigEndian.Uint64(bz) } diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 961a5a4c..4da5f720 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -12,6 +12,25 @@ type Committee struct { Permissions []Permission } +func (c Committee) HasMember(addr sdk.AccAddress) bool { + for _, m := range c.Members { + if m.Equals(addr) { + return true + } + } + return false +} + +// As long as one permission allows the proposal then it goes through. Its the OR of all permissions. +func (c Committee) HasPermissionsFor(proposal gov.Content) bool { + for _, p := range c.Permissions { + if p.Allows(proposal) { + return true + } + } + return false +} + // Permission is anything with a method that validates whether a proposal is allowed by it or not. type Permission interface { Allows(gov.Content) bool @@ -20,6 +39,8 @@ type Permission interface { // GOV STUFF -------------------------- // Should be much the same as in gov module, except Proposals are linked to a committee ID. +var _ gov.Content = Proposal{} + type Proposal struct { gov.Content ID uint64 @@ -31,7 +52,7 @@ type Proposal struct { type Vote struct { ProposalID uint64 Voter sdk.AccAddress - Option byte + // Option byte // TODO for now don't need more than just a yes as options } // Genesis ------------------- From e473d972ecdd34eecb3b6df5599e247a9271d718 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 11 Mar 2020 19:27:36 +0000 Subject: [PATCH 11/54] add vote tallying and tests --- app/app.go | 1 + x/committee/alias.go | 24 +++- x/committee/genesis.go | 24 ++++ x/committee/handler.go | 10 ++ x/committee/keeper/keeper.go | 133 ++++++++++++++++--- x/committee/keeper/keeper_test.go | 209 ++++++++++++++++++++++++++++-- x/committee/module.go | 29 ++--- x/committee/types/codec.go | 16 ++- x/committee/types/genesis.go | 51 ++++++++ x/committee/types/permissions.go | 4 + x/committee/types/types.go | 8 +- 11 files changed, 446 insertions(+), 63 deletions(-) create mode 100644 x/committee/genesis.go create mode 100644 x/committee/types/genesis.go diff --git a/app/app.go b/app/app.go index 151c5836..41a26c22 100644 --- a/app/app.go +++ b/app/app.go @@ -249,6 +249,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.committeeKeeper = committee.NewKeeper( app.cdc, keys[committee.StoreKey], + govRouter, // TODO blacklist module addresses? ) diff --git a/x/committee/alias.go b/x/committee/alias.go index 41aadd1b..48abeee3 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -8,29 +8,43 @@ import ( ) const ( - ModuleName = types.ModuleName - StoreKey = types.StoreKey + DefaultNextProposalID = types.DefaultNextProposalID + ModuleName = types.ModuleName + StoreKey = types.StoreKey ) var ( // function aliases - NewKeeper = keeper.NewKeeper - RegisterCodec = types.RegisterCodec + NewKeeper = keeper.NewKeeper + DefaultGenesisState = types.DefaultGenesisState + GetKeyFromID = types.GetKeyFromID + GetVoteKey = types.GetVoteKey + NewGenesisState = types.NewGenesisState + RegisterCodec = types.RegisterCodec + Uint64FromBytes = types.Uint64FromBytes // variable aliases - ModuleCdc = types.ModuleCdc + CommitteeKeyPrefix = types.CommitteeKeyPrefix + ModuleCdc = types.ModuleCdc + NextProposalIDKey = types.NextProposalIDKey + ProposalKeyPrefix = types.ProposalKeyPrefix + VoteKeyPrefix = types.VoteKeyPrefix + VoteThreshold = types.VoteThreshold ) type ( Keeper = keeper.Keeper Committee = types.Committee GeneralShutdownPermission = types.GeneralShutdownPermission + GenesisState = types.GenesisState + GodPermission = types.GodPermission GroupChangeProposal = types.GroupChangeProposal InflationRateChangePermission = types.InflationRateChangePermission MsgSubmitProposal = types.MsgSubmitProposal MsgVote = types.MsgVote Permission = types.Permission Proposal = types.Proposal + PubProposal = types.PubProposal ShutdownCDPDepsitPermission = types.ShutdownCDPDepsitPermission Vote = types.Vote ) diff --git a/x/committee/genesis.go b/x/committee/genesis.go new file mode 100644 index 00000000..fef015c9 --- /dev/null +++ b/x/committee/genesis.go @@ -0,0 +1,24 @@ +package committee + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// InitGenesis initializes the store state from a genesis state. +func InitGenesis(ctx sdk.Context, keeper Keeper, gs GenesisState) { + if err := gs.Validate(); err != nil { + panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err)) + } + + keeper.SetNextProposalID(ctx, gs.NextProposalID) + + // TODO set votes, committee, proposals +} + +// ExportGenesis returns a GenesisState for a given context and keeper. +func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { + // TODO + return GenesisState{} +} diff --git a/x/committee/handler.go b/x/committee/handler.go index f3f9b03f..158faa1f 100644 --- a/x/committee/handler.go +++ b/x/committee/handler.go @@ -40,6 +40,16 @@ func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubm func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Result { err := keeper.AddVote(ctx, msg) + // Try closing proposal + _ = k.CloseOutProposal(ctx, proposalID) + + // if err.Error() == "note enough votes to close proposal" { // TODO + // return nil // This is not a reason to error + // } + // if err != nil { + // return err + // } + if err != nil { return err.Result() } diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 535d3cb3..033deebd 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - //govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/kava-labs/kava/x/committee/types" ) @@ -14,20 +14,28 @@ type Keeper struct { cdc *codec.Codec storeKey sdk.StoreKey - // TODO Proposal router - //router govtypes.Router + // Proposal router + router govtypes.Router } -func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey) Keeper { +func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) Keeper { + // It is vital to seal the governance proposal router here as to not allow + // further handlers to be registered after the keeper is created since this + // could create invalid or non-deterministic behavior. + // TODO why? + // Not sealing the router because for some reason the function panics if it has already been sealed and there is no way to tell if has already been called. + // router.Seal() + return Keeper{ cdc: cdc, storeKey: storeKey, + router: router, } } -func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, proposal types.Proposal) (uint64, sdk.Error) { +func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committeeID uint64, pubProposal types.PubProposal) (uint64, sdk.Error) { // Limit proposals to only be submitted by committee members - com, found := k.GetCommittee(ctx, proposal.CommitteeID) + com, found := k.GetCommittee(ctx, committeeID) if !found { return 0, sdk.ErrInternal("committee doesn't exist") } @@ -35,30 +43,29 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, proposa return 0, sdk.ErrInternal("only member can propose proposals") } - // Check proposal is valid - if err := proposal.ValidateBasic(); err != nil { - return 0, err - } - - // Check group has permissions to enact proposal. - if !com.HasPermissionsFor(proposal) { + // Check committee has permissions to enact proposal. + if !com.HasPermissionsFor(pubProposal) { return 0, sdk.ErrInternal("committee does not have permissions to enact proposal") } - // TODO validate proposal by running it with cached context like how gov does it - // what if it's not valid now but will be in the future? + // Check proposal is valid + // TODO what if it's not valid now but will be in the future? + // TODO does this need to be before permission check? + if err := k.ValidatePubProposal(ctx, pubProposal); err != nil { + return 0, err + } // Get a new ID and store the proposal - return k.StoreNewProposal(ctx, proposal) + return k.StoreNewProposal(ctx, committeeID, pubProposal) } func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) sdk.Error { // Validate - proposal, found := k.GetProposal(ctx, proposalID) + pr, found := k.GetProposal(ctx, proposalID) if !found { return sdk.ErrInternal("proposal not found") } - com, found := k.GetCommittee(ctx, proposal.CommitteeID) + com, found := k.GetCommittee(ctx, pr.CommitteeID) if !found { return sdk.ErrInternal("committee disbanded") } @@ -69,7 +76,70 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress // Store vote, overwriting any prior vote k.SetVote(ctx, types.Vote{ProposalID: proposalID, Voter: voter}) - // TODO close vote if tally has been reached + return nil +} + +func (k Keeper) CloseOutProposal(ctx sdk.Context, proposalID uint64) sdk.Error { + pr, found := k.GetProposal(ctx, proposalID) + if !found { + return sdk.ErrInternal("proposal not found") + } + com, found := k.GetCommittee(ctx, pr.CommitteeID) + if !found { + return sdk.ErrInternal("committee disbanded") + } + + var votes []types.Vote + k.IterateVotes(ctx, proposalID, func(vote types.Vote) bool { + votes = append(votes, vote) + return false + }) + if sdk.NewDec(int64(len(votes))).GTE(types.VoteThreshold.MulInt64(int64(len(com.Members)))) { // TODO move vote counting stuff to committee methods // TODO add timeout check here - close if expired regardless of votes + // eneact vote + // The proposal handler may execute state mutating logic depending + // on the proposal content. If the handler fails, no state mutation + // is written and the error message is logged. + handler := k.router.GetRoute(pr.ProposalRoute()) + cacheCtx, writeCache := ctx.CacheContext() + err := handler(cacheCtx, pr.PubProposal) // need to pass pubProposal as the handlers type assert it into the concrete types + if err == nil { + // write state to the underlying multi-store + writeCache() + } // if handler returns error, then still delete the proposal - it's still over, but send an event + + // delete proposal and votes + k.DeleteProposal(ctx, proposalID) + for _, v := range votes { + k.DeleteVote(ctx, v.ProposalID, v.Voter) + } + } else { + return sdk.ErrInternal("note enough votes to close proposal") + } + return nil +} + +func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error { + // TODO not sure if the basic validation is required - should be run in msg.ValidateBasic + if pubProposal == nil { + return sdk.ErrInternal("proposal is empty") + } + if err := pubProposal.ValidateBasic(); err != nil { + return err + } + + if !k.router.HasRoute(pubProposal.ProposalRoute()) { + return sdk.ErrInternal("no handler found for proposal") + } + + // Execute the proposal content in a cache-wrapped context to validate the + // actual parameter changes before the proposal proceeds through the + // governance process. State is not persisted. + cacheCtx, _ := ctx.CacheContext() + handler := k.router.GetRoute(pubProposal.ProposalRoute()) + if err := handler(cacheCtx, pubProposal); err != nil { + return err + } + return nil } @@ -125,12 +195,16 @@ func (k Keeper) IncrementNextProposalID(ctx sdk.Context) sdk.Error { } // StoreNewProposal stores a proposal, adding a new ID -func (k Keeper) StoreNewProposal(ctx sdk.Context, proposal types.Proposal) (uint64, sdk.Error) { +func (k Keeper) StoreNewProposal(ctx sdk.Context, committeeID uint64, pubProposal types.PubProposal) (uint64, sdk.Error) { newProposalID, err := k.GetNextProposalID(ctx) if err != nil { return 0, err } - proposal.ID = newProposalID + proposal := types.Proposal{ + PubProposal: pubProposal, + ID: newProposalID, + CommitteeID: committeeID, + } k.SetProposal(ctx, proposal) @@ -166,6 +240,23 @@ func (k Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) { store.Delete(types.GetKeyFromID(proposalID)) } +// IterateVotes provides an iterator over all stored votes for a given proposal. +// For each vote, cb will be called. If cb returns true, the iterator will close and stop. +func (k Keeper) IterateVotes(ctx sdk.Context, proposalID uint64, cb func(vote types.Vote) (stop bool)) { + // iterate over the section of the votes store that has all votes for a particular proposal + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), append(types.VoteKeyPrefix, types.GetKeyFromID(proposalID)...)) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var vote types.Vote + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &vote) + + if cb(vote) { + break + } + } +} + // GetVote gets a vote from the store. func (k Keeper) GetVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) (types.Vote, bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix) diff --git a/x/committee/keeper/keeper_test.go b/x/committee/keeper/keeper_test.go index 5244e9d2..5605ce99 100644 --- a/x/committee/keeper/keeper_test.go +++ b/x/committee/keeper/keeper_test.go @@ -1,16 +1,18 @@ package keeper_test import ( + "reflect" "testing" - "github.com/kava-labs/kava/x/committee/types" "github.com/stretchr/testify/suite" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/committee/keeper" + "github.com/kava-labs/kava/x/committee/types" ) type KeeperTestSuite struct { @@ -27,25 +29,89 @@ func (suite *KeeperTestSuite) SetupTest() { suite.app = app.NewTestApp() suite.keeper = suite.app.GetCommitteeKeeper() suite.ctx = suite.app.NewContext(true, abci.Header{}) - _, suite.addresses = app.GeneratePrivKeyAddressPairs(2) + _, suite.addresses = app.GeneratePrivKeyAddressPairs(5) } func (suite *KeeperTestSuite) TestSubmitProposal() { + normalCom := types.Committee{ + ID: 12, + Members: suite.addresses[:2], + Permissions: []types.Permission{types.GodPermission{}}, + } + noPermissionsCom := types.Committee{ + ID: 12, + Members: suite.addresses[:2], + Permissions: []types.Permission{}, + } + testcases := []struct { - name string - proposal types.Proposal - proposer sdk.AccAddress - expectPass bool + name string + committee types.Committee + pubProposal types.PubProposal + proposer sdk.AccAddress + committeeID uint64 + expectPass bool }{ - {name: "empty proposal", proposer: suite.addresses[0], expectPass: false}, + { + name: "normal", + committee: normalCom, + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + proposer: normalCom.Members[0], + committeeID: normalCom.ID, + expectPass: true, + }, + { + name: "invalid proposal", + committee: normalCom, + pubProposal: nil, + proposer: normalCom.Members[0], + committeeID: normalCom.ID, + expectPass: false, + }, + { + name: "missing committee", + // no committee + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + proposer: suite.addresses[0], + committeeID: 0, + expectPass: false, + }, + { + name: "not a member", + committee: normalCom, + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + proposer: suite.addresses[4], + committeeID: normalCom.ID, + expectPass: false, + }, + { + name: "not enough permissions", + committee: noPermissionsCom, + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + proposer: noPermissionsCom.Members[0], + committeeID: noPermissionsCom.ID, + expectPass: false, + }, } for _, tc := range testcases { suite.Run(tc.name, func() { - _, err := suite.keeper.SubmitProposal(suite.ctx, tc.proposer, tc.proposal) + // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. + tApp := app.NewTestApp() + keeper := tApp.GetCommitteeKeeper() + ctx := tApp.NewContext(true, abci.Header{}) + tApp.InitializeFromGenesisStates() + // setup committee (if required) + if !(reflect.DeepEqual(tc.committee, types.Committee{})) { + keeper.SetCommittee(ctx, tc.committee) + } + + id, err := keeper.SubmitProposal(ctx, tc.proposer, tc.committeeID, tc.pubProposal) + if tc.expectPass { suite.NoError(err) - // TODO suite.keeper.GetProposal(suite.ctx, tc.proposal.ID) + _, found := keeper.GetProposal(ctx, id) + suite.True(found) } else { suite.NotNil(err) } @@ -54,21 +120,140 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { } func (suite *KeeperTestSuite) TestAddVote() { + normalCom := types.Committee{ + ID: 12, + Members: suite.addresses[:2], + Permissions: []types.Permission{types.GodPermission{}}, + } + testcases := []struct { name string proposalID uint64 voter sdk.AccAddress expectPass bool }{ - {name: "no proposal", proposalID: 9999999, voter: suite.addresses[0], expectPass: false}, + { + name: "normal", + proposalID: types.DefaultNextProposalID, + voter: normalCom.Members[0], + expectPass: true, + }, + { + name: "nonexistent proposal", + proposalID: 9999999, + voter: normalCom.Members[0], + expectPass: false, + }, + { + name: "voter not committee member", + proposalID: types.DefaultNextProposalID, + voter: suite.addresses[4], + expectPass: false, + }, } for _, tc := range testcases { suite.Run(tc.name, func() { - err := suite.keeper.AddVote(suite.ctx, tc.proposalID, tc.voter) + // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. + tApp := app.NewTestApp() + keeper := tApp.GetCommitteeKeeper() + ctx := tApp.NewContext(true, abci.Header{}) + tApp.InitializeFromGenesisStates() + + // setup the committee and proposal + keeper.SetCommittee(ctx, normalCom) + _, err := keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, gov.NewTextProposal("A Title", "A description of this proposal.")) + suite.NoError(err) + + err = keeper.AddVote(ctx, tc.proposalID, tc.voter) + + if tc.expectPass { + suite.NoError(err) + _, found := keeper.GetVote(ctx, tc.proposalID, tc.voter) + suite.True(found) + } else { + suite.NotNil(err) + } + }) + } +} + +func (suite *KeeperTestSuite) TestCloseOutProposal() { + // setup test + suite.app.InitializeFromGenesisStates() + // TODO replace below with genesis state + normalCom := types.Committee{ + ID: 12, + Members: suite.addresses[:2], + Permissions: []types.Permission{types.GodPermission{}}, + } + suite.keeper.SetCommittee(suite.ctx, normalCom) + pprop := gov.NewTextProposal("A Title", "A description of this proposal.") + id, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop) + suite.NoError(err) + err = suite.keeper.AddVote(suite.ctx, id, normalCom.Members[0]) + suite.NoError(err) + err = suite.keeper.AddVote(suite.ctx, id, normalCom.Members[1]) + suite.NoError(err) + + // run test + err = suite.keeper.CloseOutProposal(suite.ctx, id) + + // check + suite.NoError(err) + _, found := suite.keeper.GetProposal(suite.ctx, id) + suite.False(found) + suite.keeper.IterateVotes(suite.ctx, id, func(v types.Vote) bool { + suite.Fail("found vote when none should exist") + return false + }) + +} + +type UnregisteredProposal struct { + gov.TextProposal +} + +func (UnregisteredProposal) ProposalRoute() string { return "unregistered" } +func (UnregisteredProposal) ProposalType() string { return "unregistered" } + +var _ types.PubProposal = UnregisteredProposal{} + +func (suite *KeeperTestSuite) TestValidatePubProposal() { + + testcases := []struct { + name string + pubProposal types.PubProposal + expectPass bool + }{ + { + name: "valid", + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + expectPass: true, + }, + { + name: "invalid (missing title)", + pubProposal: gov.TextProposal{Description: "A description of this proposal."}, + expectPass: false, + }, + { + name: "invalid (unregistered)", + pubProposal: UnregisteredProposal{gov.TextProposal{Title: "A Title", Description: "A description of this proposal."}}, + expectPass: false, + }, + { + name: "invalid (nil)", + pubProposal: nil, + expectPass: false, + }, + // TODO test case when the handler fails + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + err := suite.keeper.ValidatePubProposal(suite.ctx, tc.pubProposal) if tc.expectPass { suite.NoError(err) - // TODO GetVote } else { suite.NotNil(err) } diff --git a/x/committee/module.go b/x/committee/module.go index fe068a93..6372da75 100644 --- a/x/committee/module.go +++ b/x/committee/module.go @@ -22,7 +22,7 @@ var ( // AppModuleBasic app module basics object type AppModuleBasic struct{} -// Name get module name +// Name gets the module name func (AppModuleBasic) Name() string { return ModuleName } @@ -34,19 +34,17 @@ func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { // DefaultGenesis default genesis state func (AppModuleBasic) DefaultGenesis() json.RawMessage { - //return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) - return nil + return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) } // ValidateGenesis module validate genesis func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { - // var gs GenesisState - // err := ModuleCdc.UnmarshalJSON(bz, &gs) - // if err != nil { - // return err - // } - // return gs.Validate() - return nil + var gs GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &gs) + if err != nil { + return err + } + return gs.Validate() } // RegisterRESTRoutes registers the REST routes for the module. @@ -137,18 +135,17 @@ func (am AppModule) NewQuerierHandler() sdk.Querier { // InitGenesis module init-genesis func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { - // var genesisState GenesisState - // ModuleCdc.MustUnmarshalJSON(data, &genesisState) - // InitGenesis(ctx, am.keeper, am.pricefeedKeeper, genesisState) + var genesisState GenesisState + ModuleCdc.MustUnmarshalJSON(data, &genesisState) + InitGenesis(ctx, am.keeper, genesisState) return []abci.ValidatorUpdate{} } // ExportGenesis module export genesis func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { - // gs := ExportGenesis(ctx, am.keeper) - // return ModuleCdc.MustMarshalJSON(gs) - return nil + gs := ExportGenesis(ctx, am.keeper) + return ModuleCdc.MustMarshalJSON(gs) } // BeginBlock module begin-block diff --git a/x/committee/types/codec.go b/x/committee/types/codec.go index 898769d3..f44d1795 100644 --- a/x/committee/types/codec.go +++ b/x/committee/types/codec.go @@ -1,6 +1,8 @@ package types -import "github.com/cosmos/cosmos-sdk/codec" +import ( + "github.com/cosmos/cosmos-sdk/codec" +) // ModuleCdc generic sealed codec to be used throughout module var ModuleCdc *codec.Codec @@ -13,10 +15,10 @@ func init() { // RegisterCodec registers the necessary types for the module func RegisterCodec(cdc *codec.Codec) { - // TODO - // cdc.RegisterConcrete(MsgCreateCDP{}, "cdp/MsgCreateCDP", nil) - // cdc.RegisterConcrete(MsgDeposit{}, "cdp/MsgDeposit", nil) - // cdc.RegisterConcrete(MsgWithdraw{}, "cdp/MsgWithdraw", nil) - // cdc.RegisterConcrete(MsgDrawDebt{}, "cdp/MsgDrawDebt", nil) - // cdc.RegisterConcrete(MsgRepayDebt{}, "cdp/MsgRepayDebt", nil) + + // 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 + //cdc.RegisterInterface((*gov.Content)(nil), nil) + + cdc.RegisterInterface((*Permission)(nil), nil) + cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil) } diff --git a/x/committee/types/genesis.go b/x/committee/types/genesis.go new file mode 100644 index 00000000..c44b14c6 --- /dev/null +++ b/x/committee/types/genesis.go @@ -0,0 +1,51 @@ +package types + +import ( + "bytes" +) + +// DefaultNextProposalID is the starting poiint for proposal IDs. +const DefaultNextProposalID uint64 = 1 + +// GenesisState is state that must be provided at chain genesis. +type GenesisState struct { + NextProposalID uint64 + Votes []Vote + Proposals []Proposal + Committees []Committee +} + +// NewGenesisState returns a new genesis state object for the module. +func NewGenesisState(nextProposalID uint64, votes []Vote, proposals []Proposal, committees []Committee) GenesisState { + return GenesisState{ + NextProposalID: nextProposalID, + Votes: votes, + Proposals: proposals, + Committees: committees, + } +} + +// DefaultGenesisState returns the default genesis state for the module. +func DefaultGenesisState() GenesisState { + return NewGenesisState( + DefaultNextProposalID, + []Vote{}, + []Proposal{}, + []Committee{}, + ) +} + +// Equal checks whether two gov GenesisState structs are equivalent +func (data GenesisState) Equal(data2 GenesisState) bool { + b1 := ModuleCdc.MustMarshalBinaryBare(data) + b2 := ModuleCdc.MustMarshalBinaryBare(data2) + return bytes.Equal(b1, b2) +} + +// IsEmpty returns true if a GenesisState is empty +func (data GenesisState) IsEmpty() bool { + return data.Equal(GenesisState{}) +} + +// Validate performs basic validation of genesis data. +func (gs GenesisState) Validate() error { return nil } diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index 8338ea1d..068fadc3 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -8,6 +8,10 @@ import ( // EXAMPLE PERMISSIONS ------------------------------ +type GodPermission struct{} + +func (GodPermission) Allows(gov.Content) bool { return true } + // Allow only changes to inflation_rate type InflationRateChangePermission struct{} diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 4da5f720..08ff1db3 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -5,6 +5,8 @@ import ( "github.com/cosmos/cosmos-sdk/x/gov" ) +var VoteThreshold sdk.Dec = sdk.MustNewDecFromStr("0.75") + // A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. type Committee struct { ID uint64 // TODO or a name? @@ -39,10 +41,12 @@ type Permission interface { // GOV STUFF -------------------------- // Should be much the same as in gov module, except Proposals are linked to a committee ID. -var _ gov.Content = Proposal{} +// TODO not needed? var _ gov.Content = Proposal{} + +type PubProposal = gov.Content // TODO better name type Proposal struct { - gov.Content + PubProposal ID uint64 CommitteeID uint64 // TODO From 5911e648b7ab7e37fc0d3ccb34be5e65a56190d5 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 11 Mar 2020 19:52:25 +0000 Subject: [PATCH 12/54] improve code organisation --- x/committee/keeper/keeper.go | 146 +++-------------- x/committee/keeper/keeper_test.go | 231 -------------------------- x/committee/keeper/proposal.go | 117 ++++++++++++++ x/committee/keeper/proposal_test.go | 241 ++++++++++++++++++++++++++++ x/committee/types/keys.go | 6 - x/committee/types/types.go | 23 ++- 6 files changed, 388 insertions(+), 376 deletions(-) create mode 100644 x/committee/keeper/proposal.go create mode 100644 x/committee/keeper/proposal_test.go diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 033deebd..523ba417 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -33,115 +33,7 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) } } -func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committeeID uint64, pubProposal types.PubProposal) (uint64, sdk.Error) { - // Limit proposals to only be submitted by committee members - com, found := k.GetCommittee(ctx, committeeID) - if !found { - return 0, sdk.ErrInternal("committee doesn't exist") - } - if !com.HasMember(proposer) { - return 0, sdk.ErrInternal("only member can propose proposals") - } - - // Check committee has permissions to enact proposal. - if !com.HasPermissionsFor(pubProposal) { - return 0, sdk.ErrInternal("committee does not have permissions to enact proposal") - } - - // Check proposal is valid - // TODO what if it's not valid now but will be in the future? - // TODO does this need to be before permission check? - if err := k.ValidatePubProposal(ctx, pubProposal); err != nil { - return 0, err - } - - // Get a new ID and store the proposal - return k.StoreNewProposal(ctx, committeeID, pubProposal) -} - -func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) sdk.Error { - // Validate - pr, found := k.GetProposal(ctx, proposalID) - if !found { - return sdk.ErrInternal("proposal not found") - } - com, found := k.GetCommittee(ctx, pr.CommitteeID) - if !found { - return sdk.ErrInternal("committee disbanded") - } - if !com.HasMember(voter) { - return sdk.ErrInternal("not authorized to vote on proposal") - } - - // Store vote, overwriting any prior vote - k.SetVote(ctx, types.Vote{ProposalID: proposalID, Voter: voter}) - - return nil -} - -func (k Keeper) CloseOutProposal(ctx sdk.Context, proposalID uint64) sdk.Error { - pr, found := k.GetProposal(ctx, proposalID) - if !found { - return sdk.ErrInternal("proposal not found") - } - com, found := k.GetCommittee(ctx, pr.CommitteeID) - if !found { - return sdk.ErrInternal("committee disbanded") - } - - var votes []types.Vote - k.IterateVotes(ctx, proposalID, func(vote types.Vote) bool { - votes = append(votes, vote) - return false - }) - if sdk.NewDec(int64(len(votes))).GTE(types.VoteThreshold.MulInt64(int64(len(com.Members)))) { // TODO move vote counting stuff to committee methods // TODO add timeout check here - close if expired regardless of votes - // eneact vote - // The proposal handler may execute state mutating logic depending - // on the proposal content. If the handler fails, no state mutation - // is written and the error message is logged. - handler := k.router.GetRoute(pr.ProposalRoute()) - cacheCtx, writeCache := ctx.CacheContext() - err := handler(cacheCtx, pr.PubProposal) // need to pass pubProposal as the handlers type assert it into the concrete types - if err == nil { - // write state to the underlying multi-store - writeCache() - } // if handler returns error, then still delete the proposal - it's still over, but send an event - - // delete proposal and votes - k.DeleteProposal(ctx, proposalID) - for _, v := range votes { - k.DeleteVote(ctx, v.ProposalID, v.Voter) - } - } else { - return sdk.ErrInternal("note enough votes to close proposal") - } - return nil -} - -func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error { - // TODO not sure if the basic validation is required - should be run in msg.ValidateBasic - if pubProposal == nil { - return sdk.ErrInternal("proposal is empty") - } - if err := pubProposal.ValidateBasic(); err != nil { - return err - } - - if !k.router.HasRoute(pubProposal.ProposalRoute()) { - return sdk.ErrInternal("no handler found for proposal") - } - - // Execute the proposal content in a cache-wrapped context to validate the - // actual parameter changes before the proposal proceeds through the - // governance process. State is not persisted. - cacheCtx, _ := ctx.CacheContext() - handler := k.router.GetRoute(pubProposal.ProposalRoute()) - if err := handler(cacheCtx, pubProposal); err != nil { - return err - } - - return nil -} +// ---------- Committees ---------- // GetCommittee gets a committee from the store. func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committee, bool) { @@ -168,6 +60,8 @@ func (k Keeper) DeleteCommittee(ctx sdk.Context, committeeID uint64) { store.Delete(types.GetKeyFromID(committeeID)) } +// ---------- Proposals ---------- + // SetNextProposalID stores an ID to be used for the next created proposal func (k Keeper) SetNextProposalID(ctx sdk.Context, id uint64) { store := ctx.KVStore(k.storeKey) @@ -240,22 +134,7 @@ func (k Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) { store.Delete(types.GetKeyFromID(proposalID)) } -// IterateVotes provides an iterator over all stored votes for a given proposal. -// For each vote, cb will be called. If cb returns true, the iterator will close and stop. -func (k Keeper) IterateVotes(ctx sdk.Context, proposalID uint64, cb func(vote types.Vote) (stop bool)) { - // iterate over the section of the votes store that has all votes for a particular proposal - iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), append(types.VoteKeyPrefix, types.GetKeyFromID(proposalID)...)) - - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var vote types.Vote - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &vote) - - if cb(vote) { - break - } - } -} +// ---------- Votes ---------- // GetVote gets a vote from the store. func (k Keeper) GetVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) (types.Vote, bool) { @@ -281,3 +160,20 @@ func (k Keeper) DeleteVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddr store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix) store.Delete(types.GetVoteKey(proposalID, voter)) } + +// IterateVotes provides an iterator over all stored votes for a given proposal. +// For each vote, cb will be called. If cb returns true, the iterator will close and stop. +func (k Keeper) IterateVotes(ctx sdk.Context, proposalID uint64, cb func(vote types.Vote) (stop bool)) { + // iterate over the section of the votes store that has all votes for a particular proposal + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), append(types.VoteKeyPrefix, types.GetKeyFromID(proposalID)...)) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var vote types.Vote + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &vote) + + if cb(vote) { + break + } + } +} diff --git a/x/committee/keeper/keeper_test.go b/x/committee/keeper/keeper_test.go index 5605ce99..f57e380a 100644 --- a/x/committee/keeper/keeper_test.go +++ b/x/committee/keeper/keeper_test.go @@ -1,13 +1,11 @@ package keeper_test import ( - "reflect" "testing" "github.com/stretchr/testify/suite" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/gov" abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" @@ -32,235 +30,6 @@ func (suite *KeeperTestSuite) SetupTest() { _, suite.addresses = app.GeneratePrivKeyAddressPairs(5) } -func (suite *KeeperTestSuite) TestSubmitProposal() { - normalCom := types.Committee{ - ID: 12, - Members: suite.addresses[:2], - Permissions: []types.Permission{types.GodPermission{}}, - } - noPermissionsCom := types.Committee{ - ID: 12, - Members: suite.addresses[:2], - Permissions: []types.Permission{}, - } - - testcases := []struct { - name string - committee types.Committee - pubProposal types.PubProposal - proposer sdk.AccAddress - committeeID uint64 - expectPass bool - }{ - { - name: "normal", - committee: normalCom, - pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), - proposer: normalCom.Members[0], - committeeID: normalCom.ID, - expectPass: true, - }, - { - name: "invalid proposal", - committee: normalCom, - pubProposal: nil, - proposer: normalCom.Members[0], - committeeID: normalCom.ID, - expectPass: false, - }, - { - name: "missing committee", - // no committee - pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), - proposer: suite.addresses[0], - committeeID: 0, - expectPass: false, - }, - { - name: "not a member", - committee: normalCom, - pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), - proposer: suite.addresses[4], - committeeID: normalCom.ID, - expectPass: false, - }, - { - name: "not enough permissions", - committee: noPermissionsCom, - pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), - proposer: noPermissionsCom.Members[0], - committeeID: noPermissionsCom.ID, - expectPass: false, - }, - } - - for _, tc := range testcases { - suite.Run(tc.name, func() { - // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. - tApp := app.NewTestApp() - keeper := tApp.GetCommitteeKeeper() - ctx := tApp.NewContext(true, abci.Header{}) - tApp.InitializeFromGenesisStates() - // setup committee (if required) - if !(reflect.DeepEqual(tc.committee, types.Committee{})) { - keeper.SetCommittee(ctx, tc.committee) - } - - id, err := keeper.SubmitProposal(ctx, tc.proposer, tc.committeeID, tc.pubProposal) - - if tc.expectPass { - suite.NoError(err) - _, found := keeper.GetProposal(ctx, id) - suite.True(found) - } else { - suite.NotNil(err) - } - }) - } -} - -func (suite *KeeperTestSuite) TestAddVote() { - normalCom := types.Committee{ - ID: 12, - Members: suite.addresses[:2], - Permissions: []types.Permission{types.GodPermission{}}, - } - - testcases := []struct { - name string - proposalID uint64 - voter sdk.AccAddress - expectPass bool - }{ - { - name: "normal", - proposalID: types.DefaultNextProposalID, - voter: normalCom.Members[0], - expectPass: true, - }, - { - name: "nonexistent proposal", - proposalID: 9999999, - voter: normalCom.Members[0], - expectPass: false, - }, - { - name: "voter not committee member", - proposalID: types.DefaultNextProposalID, - voter: suite.addresses[4], - expectPass: false, - }, - } - - for _, tc := range testcases { - suite.Run(tc.name, func() { - // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. - tApp := app.NewTestApp() - keeper := tApp.GetCommitteeKeeper() - ctx := tApp.NewContext(true, abci.Header{}) - tApp.InitializeFromGenesisStates() - - // setup the committee and proposal - keeper.SetCommittee(ctx, normalCom) - _, err := keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, gov.NewTextProposal("A Title", "A description of this proposal.")) - suite.NoError(err) - - err = keeper.AddVote(ctx, tc.proposalID, tc.voter) - - if tc.expectPass { - suite.NoError(err) - _, found := keeper.GetVote(ctx, tc.proposalID, tc.voter) - suite.True(found) - } else { - suite.NotNil(err) - } - }) - } -} - -func (suite *KeeperTestSuite) TestCloseOutProposal() { - // setup test - suite.app.InitializeFromGenesisStates() - // TODO replace below with genesis state - normalCom := types.Committee{ - ID: 12, - Members: suite.addresses[:2], - Permissions: []types.Permission{types.GodPermission{}}, - } - suite.keeper.SetCommittee(suite.ctx, normalCom) - pprop := gov.NewTextProposal("A Title", "A description of this proposal.") - id, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop) - suite.NoError(err) - err = suite.keeper.AddVote(suite.ctx, id, normalCom.Members[0]) - suite.NoError(err) - err = suite.keeper.AddVote(suite.ctx, id, normalCom.Members[1]) - suite.NoError(err) - - // run test - err = suite.keeper.CloseOutProposal(suite.ctx, id) - - // check - suite.NoError(err) - _, found := suite.keeper.GetProposal(suite.ctx, id) - suite.False(found) - suite.keeper.IterateVotes(suite.ctx, id, func(v types.Vote) bool { - suite.Fail("found vote when none should exist") - return false - }) - -} - -type UnregisteredProposal struct { - gov.TextProposal -} - -func (UnregisteredProposal) ProposalRoute() string { return "unregistered" } -func (UnregisteredProposal) ProposalType() string { return "unregistered" } - -var _ types.PubProposal = UnregisteredProposal{} - -func (suite *KeeperTestSuite) TestValidatePubProposal() { - - testcases := []struct { - name string - pubProposal types.PubProposal - expectPass bool - }{ - { - name: "valid", - pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), - expectPass: true, - }, - { - name: "invalid (missing title)", - pubProposal: gov.TextProposal{Description: "A description of this proposal."}, - expectPass: false, - }, - { - name: "invalid (unregistered)", - pubProposal: UnregisteredProposal{gov.TextProposal{Title: "A Title", Description: "A description of this proposal."}}, - expectPass: false, - }, - { - name: "invalid (nil)", - pubProposal: nil, - expectPass: false, - }, - // TODO test case when the handler fails - } - - for _, tc := range testcases { - suite.Run(tc.name, func() { - err := suite.keeper.ValidatePubProposal(suite.ctx, tc.pubProposal) - if tc.expectPass { - suite.NoError(err) - } else { - suite.NotNil(err) - } - }) - } -} - func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() { // setup test com := types.Committee{ diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go new file mode 100644 index 00000000..acb604ba --- /dev/null +++ b/x/committee/keeper/proposal.go @@ -0,0 +1,117 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/committee/types" +) + +func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committeeID uint64, pubProposal types.PubProposal) (uint64, sdk.Error) { + // Limit proposals to only be submitted by committee members + com, found := k.GetCommittee(ctx, committeeID) + if !found { + return 0, sdk.ErrInternal("committee doesn't exist") + } + if !com.HasMember(proposer) { + return 0, sdk.ErrInternal("only member can propose proposals") + } + + // Check committee has permissions to enact proposal. + if !com.HasPermissionsFor(pubProposal) { + return 0, sdk.ErrInternal("committee does not have permissions to enact proposal") + } + + // Check proposal is valid + // TODO what if it's not valid now but will be in the future? + // TODO does this need to be before permission check? + if err := k.ValidatePubProposal(ctx, pubProposal); err != nil { + return 0, err + } + + // Get a new ID and store the proposal + return k.StoreNewProposal(ctx, committeeID, pubProposal) +} + +func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) sdk.Error { + // Validate + pr, found := k.GetProposal(ctx, proposalID) + if !found { + return sdk.ErrInternal("proposal not found") + } + com, found := k.GetCommittee(ctx, pr.CommitteeID) + if !found { + return sdk.ErrInternal("committee disbanded") + } + if !com.HasMember(voter) { + return sdk.ErrInternal("not authorized to vote on proposal") + } + + // Store vote, overwriting any prior vote + k.SetVote(ctx, types.Vote{ProposalID: proposalID, Voter: voter}) + + return nil +} + +func (k Keeper) CloseOutProposal(ctx sdk.Context, proposalID uint64) sdk.Error { + pr, found := k.GetProposal(ctx, proposalID) + if !found { + return sdk.ErrInternal("proposal not found") + } + com, found := k.GetCommittee(ctx, pr.CommitteeID) + if !found { + return sdk.ErrInternal("committee disbanded") + } + + var votes []types.Vote + k.IterateVotes(ctx, proposalID, func(vote types.Vote) bool { + votes = append(votes, vote) + return false + }) + if sdk.NewDec(int64(len(votes))).GTE(types.VoteThreshold.MulInt64(int64(len(com.Members)))) { // TODO move vote counting stuff to committee methods // TODO add timeout check here - close if expired regardless of votes + // eneact vote + // The proposal handler may execute state mutating logic depending + // on the proposal content. If the handler fails, no state mutation + // is written and the error message is logged. + handler := k.router.GetRoute(pr.ProposalRoute()) + cacheCtx, writeCache := ctx.CacheContext() + err := handler(cacheCtx, pr.PubProposal) // need to pass pubProposal as the handlers type assert it into the concrete types + if err == nil { + // write state to the underlying multi-store + writeCache() + } // if handler returns error, then still delete the proposal - it's still over, but send an event + + // delete proposal and votes + k.DeleteProposal(ctx, proposalID) + for _, v := range votes { + k.DeleteVote(ctx, v.ProposalID, v.Voter) + } + } else { + return sdk.ErrInternal("note enough votes to close proposal") + } + return nil +} + +func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error { + // TODO not sure if the basic validation is required - should be run in msg.ValidateBasic + if pubProposal == nil { + return sdk.ErrInternal("proposal is empty") + } + if err := pubProposal.ValidateBasic(); err != nil { + return err + } + + if !k.router.HasRoute(pubProposal.ProposalRoute()) { + return sdk.ErrInternal("no handler found for proposal") + } + + // Execute the proposal content in a cache-wrapped context to validate the + // actual parameter changes before the proposal proceeds through the + // governance process. State is not persisted. + cacheCtx, _ := ctx.CacheContext() + handler := k.router.GetRoute(pubProposal.ProposalRoute()) + if err := handler(cacheCtx, pubProposal); err != nil { + return err + } + + return nil +} diff --git a/x/committee/keeper/proposal_test.go b/x/committee/keeper/proposal_test.go new file mode 100644 index 00000000..fe5ca6c7 --- /dev/null +++ b/x/committee/keeper/proposal_test.go @@ -0,0 +1,241 @@ +package keeper_test + +import ( + "reflect" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/committee/types" +) + +func (suite *KeeperTestSuite) TestSubmitProposal() { + normalCom := types.Committee{ + ID: 12, + Members: suite.addresses[:2], + Permissions: []types.Permission{types.GodPermission{}}, + } + noPermissionsCom := types.Committee{ + ID: 12, + Members: suite.addresses[:2], + Permissions: []types.Permission{}, + } + + testcases := []struct { + name string + committee types.Committee + pubProposal types.PubProposal + proposer sdk.AccAddress + committeeID uint64 + expectPass bool + }{ + { + name: "normal", + committee: normalCom, + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + proposer: normalCom.Members[0], + committeeID: normalCom.ID, + expectPass: true, + }, + { + name: "invalid proposal", + committee: normalCom, + pubProposal: nil, + proposer: normalCom.Members[0], + committeeID: normalCom.ID, + expectPass: false, + }, + { + name: "missing committee", + // no committee + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + proposer: suite.addresses[0], + committeeID: 0, + expectPass: false, + }, + { + name: "not a member", + committee: normalCom, + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + proposer: suite.addresses[4], + committeeID: normalCom.ID, + expectPass: false, + }, + { + name: "not enough permissions", + committee: noPermissionsCom, + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + proposer: noPermissionsCom.Members[0], + committeeID: noPermissionsCom.ID, + expectPass: false, + }, + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. + tApp := app.NewTestApp() + keeper := tApp.GetCommitteeKeeper() + ctx := tApp.NewContext(true, abci.Header{}) + tApp.InitializeFromGenesisStates() + // setup committee (if required) + if !(reflect.DeepEqual(tc.committee, types.Committee{})) { + keeper.SetCommittee(ctx, tc.committee) + } + + id, err := keeper.SubmitProposal(ctx, tc.proposer, tc.committeeID, tc.pubProposal) + + if tc.expectPass { + suite.NoError(err) + _, found := keeper.GetProposal(ctx, id) + suite.True(found) + } else { + suite.NotNil(err) + } + }) + } +} + +func (suite *KeeperTestSuite) TestAddVote() { + normalCom := types.Committee{ + ID: 12, + Members: suite.addresses[:2], + Permissions: []types.Permission{types.GodPermission{}}, + } + + testcases := []struct { + name string + proposalID uint64 + voter sdk.AccAddress + expectPass bool + }{ + { + name: "normal", + proposalID: types.DefaultNextProposalID, + voter: normalCom.Members[0], + expectPass: true, + }, + { + name: "nonexistent proposal", + proposalID: 9999999, + voter: normalCom.Members[0], + expectPass: false, + }, + { + name: "voter not committee member", + proposalID: types.DefaultNextProposalID, + voter: suite.addresses[4], + expectPass: false, + }, + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. + tApp := app.NewTestApp() + keeper := tApp.GetCommitteeKeeper() + ctx := tApp.NewContext(true, abci.Header{}) + tApp.InitializeFromGenesisStates() + + // setup the committee and proposal + keeper.SetCommittee(ctx, normalCom) + _, err := keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, gov.NewTextProposal("A Title", "A description of this proposal.")) + suite.NoError(err) + + err = keeper.AddVote(ctx, tc.proposalID, tc.voter) + + if tc.expectPass { + suite.NoError(err) + _, found := keeper.GetVote(ctx, tc.proposalID, tc.voter) + suite.True(found) + } else { + suite.NotNil(err) + } + }) + } +} + +func (suite *KeeperTestSuite) TestCloseOutProposal() { + // setup test + suite.app.InitializeFromGenesisStates() + // TODO replace below with genesis state + normalCom := types.Committee{ + ID: 12, + Members: suite.addresses[:2], + Permissions: []types.Permission{types.GodPermission{}}, + } + suite.keeper.SetCommittee(suite.ctx, normalCom) + pprop := gov.NewTextProposal("A Title", "A description of this proposal.") + id, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop) + suite.NoError(err) + err = suite.keeper.AddVote(suite.ctx, id, normalCom.Members[0]) + suite.NoError(err) + err = suite.keeper.AddVote(suite.ctx, id, normalCom.Members[1]) + suite.NoError(err) + + // run test + err = suite.keeper.CloseOutProposal(suite.ctx, id) + + // check + suite.NoError(err) + _, found := suite.keeper.GetProposal(suite.ctx, id) + suite.False(found) + suite.keeper.IterateVotes(suite.ctx, id, func(v types.Vote) bool { + suite.Fail("found vote when none should exist") + return false + }) + +} + +type UnregisteredProposal struct { + gov.TextProposal +} + +func (UnregisteredProposal) ProposalRoute() string { return "unregistered" } +func (UnregisteredProposal) ProposalType() string { return "unregistered" } + +var _ types.PubProposal = UnregisteredProposal{} + +func (suite *KeeperTestSuite) TestValidatePubProposal() { + + testcases := []struct { + name string + pubProposal types.PubProposal + expectPass bool + }{ + { + name: "valid", + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + expectPass: true, + }, + { + name: "invalid (missing title)", + pubProposal: gov.TextProposal{Description: "A description of this proposal."}, + expectPass: false, + }, + { + name: "invalid (unregistered)", + pubProposal: UnregisteredProposal{gov.TextProposal{Title: "A Title", Description: "A description of this proposal."}}, + expectPass: false, + }, + { + name: "invalid (nil)", + pubProposal: nil, + expectPass: false, + }, + // TODO test case when the handler fails + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + err := suite.keeper.ValidatePubProposal(suite.ctx, tc.pubProposal) + if tc.expectPass { + suite.NoError(err) + } else { + suite.NotNil(err) + } + }) + } +} diff --git a/x/committee/types/keys.go b/x/committee/types/keys.go index 7ad1731d..6b0a8c4d 100644 --- a/x/committee/types/keys.go +++ b/x/committee/types/keys.go @@ -30,7 +30,6 @@ var ( CommitteeKeyPrefix = []byte{0x00} // prefix for keys that store committees ProposalKeyPrefix = []byte{0x01} // prefix for keys that store proposals VoteKeyPrefix = []byte{0x02} // prefix for keys that store votes - //AuctionByTimeKeyPrefix = []byte{0x01} // prefix for keys that are part of the auctionsByTime index NextProposalIDKey = []byte{0x03} // key for the next proposal id ) @@ -44,11 +43,6 @@ func GetVoteKey(proposalID uint64, voter sdk.AccAddress) []byte { return append(GetKeyFromID(proposalID), voter.Bytes()...) } -// // GetAuctionByTimeKey returns the key for iterating auctions by time -// func GetAuctionByTimeKey(endTime time.Time, auctionID uint64) []byte { -// return append(sdk.FormatTimeBytes(endTime), Uint64ToBytes(auctionID)...) -// } - // Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys. func uint64ToBytes(id uint64) []byte { bz := make([]byte, 8) diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 08ff1db3..2930bd37 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -5,6 +5,8 @@ import ( "github.com/cosmos/cosmos-sdk/x/gov" ) +// -------- Committees -------- + var VoteThreshold sdk.Dec = sdk.MustNewDecFromStr("0.75") // A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. @@ -23,8 +25,9 @@ func (c Committee) HasMember(addr sdk.AccAddress) bool { return false } -// As long as one permission allows the proposal then it goes through. Its the OR of all permissions. -func (c Committee) HasPermissionsFor(proposal gov.Content) bool { +// HasPermissionsFor returns whether the committee is authorized to enact a proposal. +// As long as one permission allows the proposal then it goes through. Its the OR of all permissions. +func (c Committee) HasPermissionsFor(proposal PubProposal) bool { for _, p := range c.Permissions { if p.Allows(proposal) { return true @@ -35,22 +38,18 @@ func (c Committee) HasPermissionsFor(proposal gov.Content) bool { // Permission is anything with a method that validates whether a proposal is allowed by it or not. type Permission interface { - Allows(gov.Content) bool + Allows(PubProposal) bool } -// GOV STUFF -------------------------- -// Should be much the same as in gov module, except Proposals are linked to a committee ID. +// -------- Proposals -------- -// TODO not needed? var _ gov.Content = Proposal{} - -type PubProposal = gov.Content // TODO better name +// PubProposal is an interface that all gov proposals defined in other modules must satisfy. +type PubProposal = gov.Content // TODO find a better name type Proposal struct { PubProposal ID uint64 CommitteeID uint64 - // TODO - // could store votes on the proposal object } type Vote struct { @@ -58,7 +57,3 @@ type Vote struct { Voter sdk.AccAddress // Option byte // TODO for now don't need more than just a yes as options } - -// Genesis ------------------- -// Ok just to dump everything to json and reload - if time involved then begin blocker will take care of closing expired proposals. And it won't enact proposals because they would've been immediately enacted before the halt if they passed. -// committee, proposals, votes From 029842168ab0669e75c8638d38ca7cbfb16d8347 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 11 Mar 2020 23:52:54 +0000 Subject: [PATCH 13/54] address some TODOs --- x/committee/keeper/keeper.go | 17 +++++++++++------ x/committee/keeper/proposal.go | 3 --- x/committee/types/msg.go | 2 -- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 523ba417..59337b39 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -19,12 +19,10 @@ type Keeper struct { } func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) Keeper { - // It is vital to seal the governance proposal router here as to not allow - // further handlers to be registered after the keeper is created since this - // could create invalid or non-deterministic behavior. - // TODO why? - // Not sealing the router because for some reason the function panics if it has already been sealed and there is no way to tell if has already been called. - // router.Seal() + // Logic in the keeper methods assume the set of gov handlers is fixed. + // So the gov router must be sealed so no handlers can be added or removed after the keeper is created. + // Note: for some reason the gov router panics if it has already been sealed, so a helper func is used to make sealing idempotent. + sealGovRouterIdempotently(router) return Keeper{ cdc: cdc, @@ -33,6 +31,13 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) } } +func sealGovRouterIdempotently(router govtypes.Router) { + defer func() { + recover() + }() + router.Seal() +} + // ---------- Committees ---------- // GetCommittee gets a committee from the store. diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index acb604ba..4aeeac82 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -22,8 +22,6 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ } // Check proposal is valid - // TODO what if it's not valid now but will be in the future? - // TODO does this need to be before permission check? if err := k.ValidatePubProposal(ctx, pubProposal); err != nil { return 0, err } @@ -92,7 +90,6 @@ func (k Keeper) CloseOutProposal(ctx sdk.Context, proposalID uint64) sdk.Error { } func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error { - // TODO not sure if the basic validation is required - should be run in msg.ValidateBasic if pubProposal == nil { return sdk.ErrInternal("proposal is empty") } diff --git a/x/committee/types/msg.go b/x/committee/types/msg.go index 508d4a4a..74b21fa4 100644 --- a/x/committee/types/msg.go +++ b/x/committee/types/msg.go @@ -1,7 +1,5 @@ package types -// These msg types should be basically the same as for gov, but without deposits. - // MsgSubmitProposal is used by committee members to create a new proposal that they can vote on. type MsgSubmitProposal struct { // TODO From 20bcfec407137972529cf5d61291ebd1bc886794 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 12 Mar 2020 00:47:25 +0000 Subject: [PATCH 14/54] add msgs --- x/committee/handler.go | 48 ++++++++++++------- x/committee/types/keys.go | 2 - x/committee/types/msg.go | 88 ++++++++++++++++++++++++++++++++++- x/committee/types/msg_test.go | 81 ++++++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 x/committee/types/msg_test.go diff --git a/x/committee/handler.go b/x/committee/handler.go index 158faa1f..bbc0530c 100644 --- a/x/committee/handler.go +++ b/x/committee/handler.go @@ -1,25 +1,24 @@ package committee -// committee, subcommittee, council, caucus, commission, synod, board -/* import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/committee/keeper" - "github.com/kava-labs/kava/x/committee/types" ) // NewHandler creates an sdk.Handler for committee messages func NewHandler(k keeper.Keeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + switch msg := msg.(type) { case types.MsgSubmitProposal: - handleMsgSubmitProposal(ctx, k, msg) + return handleMsgSubmitProposal(ctx, k, msg) case types.MsgVote: - handleMsgVote(ctx, k, msg) + return handleMsgVote(ctx, k, msg) default: errMsg := fmt.Sprintf("unrecognized %s msg type: %T", types.ModuleName, msg) return sdk.ErrUnknownRequest(errMsg).Result() @@ -28,21 +27,33 @@ func NewHandler(k keeper.Keeper) sdk.Handler { } func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubmitProposal) sdk.Result { - err := keeper.SubmitProposal(ctx, msg) - + proposalID, err := k.SubmitProposal(ctx, msg.Proposer, msg.CommitteeID, msg.PubProposal) if err != nil { return err.Result() } - return sdk.Result{} + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + // TODO sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Proposer.String()), + ), + ) + + return sdk.Result{ + Data: GetKeyFromID(proposalID), + Events: ctx.EventManager().Events(), + } } func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Result { - err := keeper.AddVote(ctx, msg) - - // Try closing proposal - _ = k.CloseOutProposal(ctx, proposalID) + err := k.AddVote(ctx, msg.ProposalID, msg.Voter) + if err != nil { + return err.Result() + } + // Try closing proposal in case enough votes have been cast + _ = k.CloseOutProposal(ctx, msg.ProposalID) // if err.Error() == "note enough votes to close proposal" { // TODO // return nil // This is not a reason to error // } @@ -50,10 +61,13 @@ func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Resu // return err // } - if err != nil { - return err.Result() - } + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + // TODO sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Voter.String()), + ), + ) - return sdk.Result{} + return sdk.Result{Events: ctx.EventManager().Events()} } -*/ diff --git a/x/committee/types/keys.go b/x/committee/types/keys.go index 6b0a8c4d..43455f39 100644 --- a/x/committee/types/keys.go +++ b/x/committee/types/keys.go @@ -13,7 +13,6 @@ const ( // StoreKey Top level store key where all module items will be stored StoreKey = ModuleName -/* // RouterKey Top level router key RouterKey = ModuleName @@ -22,7 +21,6 @@ const ( // DefaultParamspace default name for parameter store DefaultParamspace = ModuleName -*/ ) // Key prefixes diff --git a/x/committee/types/msg.go b/x/committee/types/msg.go index 74b21fa4..e262260b 100644 --- a/x/committee/types/msg.go +++ b/x/committee/types/msg.go @@ -1,11 +1,97 @@ package types +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + TypeMsgSubmitProposal = "submit_proposal" + TypeMsgVote = "vote" +) + +var _, _ sdk.Msg = MsgSubmitProposal{}, MsgVote{} + // MsgSubmitProposal is used by committee members to create a new proposal that they can vote on. type MsgSubmitProposal struct { + PubProposal PubProposal `json:"pub_proposal" yaml:"pub_proposal"` + Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` + CommitteeID uint64 +} + +// NewMsgSubmitProposal creates a new MsgSubmitProposal instance +func NewMsgSubmitProposal(pubProposal PubProposal, proposer sdk.AccAddress, committeeID uint64) MsgSubmitProposal { + return MsgSubmitProposal{ + PubProposal: pubProposal, + Proposer: proposer, + CommitteeID: committeeID, + } +} + +// Route return the message type used for routing the message. +func (msg MsgSubmitProposal) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within events. +func (msg MsgSubmitProposal) Type() string { return TypeMsgSubmitProposal } + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgSubmitProposal) ValidateBasic() sdk.Error { + if msg.PubProposal == nil { + return sdk.ErrInternal("no proposal") + } + if msg.Proposer.Empty() { + return sdk.ErrInvalidAddress(msg.Proposer.String()) + } // TODO + // if !IsValidProposalType(msg.Content.ProposalType()) { + // return ErrInvalidProposalType(DefaultCodespace, msg.Content.ProposalType()) + // } + + return msg.PubProposal.ValidateBasic() +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgSubmitProposal) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgSubmitProposal) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Proposer} } // MsgVote is submitted by committee members to vote on proposals. type MsgVote struct { - // TODO + ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` + Voter sdk.AccAddress `json:"voter" yaml:"voter"` +} + +// NewMsgVote creates a message to cast a vote on an active proposal +func NewMsgVote(voter sdk.AccAddress, proposalID uint64) MsgVote { + return MsgVote{proposalID, voter} +} + +// Route return the message type used for routing the message. +func (msg MsgVote) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within events. +func (msg MsgVote) Type() string { return TypeMsgVote } + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgVote) ValidateBasic() sdk.Error { + if msg.Voter.Empty() { + return sdk.ErrInvalidAddress(msg.Voter.String()) + } + return nil +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgVote) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgVote) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Voter} } diff --git a/x/committee/types/msg_test.go b/x/committee/types/msg_test.go new file mode 100644 index 00000000..1a7b15f7 --- /dev/null +++ b/x/committee/types/msg_test.go @@ -0,0 +1,81 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" +) + +func TestMsgSubmitProposal_ValidateBasic(t *testing.T) { + addr := sdk.AccAddress([]byte("someName")) + tests := []struct { + name string + msg MsgSubmitProposal + expectPass bool + }{ + { + name: "normal", + msg: MsgSubmitProposal{gov.NewTextProposal("A Title", "A proposal description."), addr, 3}, + expectPass: true, + }, + { + name: "empty address", + msg: MsgSubmitProposal{gov.NewTextProposal("A Title", "A proposal description."), nil, 3}, + expectPass: false, + }, + { + name: "invalid proposal", + msg: MsgSubmitProposal{gov.TextProposal{}, addr, 3}, + expectPass: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + + err := tc.msg.ValidateBasic() + + if tc.expectPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} + +func TestMsgVote_ValidateBasic(t *testing.T) { + addr := sdk.AccAddress([]byte("someName")) + tests := []struct { + name string + msg MsgVote + expectPass bool + }{ + { + name: "normal", + msg: MsgVote{5, addr}, + expectPass: true, + }, + { + name: "empty address", + msg: MsgVote{5, nil}, + expectPass: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + + err := tc.msg.ValidateBasic() + + if tc.expectPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} From f773f7f278a612e9444dd183cb5747c32db64bc1 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 12 Mar 2020 17:05:40 +0000 Subject: [PATCH 15/54] add proposal voting deadlines --- app/app.go | 2 +- x/committee/abci.go | 29 ++++++++---- x/committee/abci_test.go | 68 +++++++++++++++++++++++++++++ x/committee/alias.go | 34 +++++++++------ x/committee/keeper/keeper.go | 22 +++++++++- x/committee/keeper/proposal.go | 17 +++++--- x/committee/keeper/proposal_test.go | 17 +++++++- x/committee/module.go | 2 +- x/committee/types/types.go | 17 +++++++- 9 files changed, 173 insertions(+), 35 deletions(-) create mode 100644 x/committee/abci_test.go diff --git a/app/app.go b/app/app.go index 41a26c22..bfc2d75e 100644 --- a/app/app.go +++ b/app/app.go @@ -282,7 +282,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // there is nothing left over in the validator fee pool, so as to keep the // CanWithdrawInvariant invariant. // Auction.BeginBlocker will close out expired auctions and pay debt back to cdp. So it should be run before cdp.BeginBlocker which cancels out debt with stable and starts more auctions. - app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, auction.ModuleName, cdp.ModuleName) + app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, auction.ModuleName, cdp.ModuleName, committee.ModuleName) app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName) diff --git a/x/committee/abci.go b/x/committee/abci.go index 54e706d8..5ab25598 100644 --- a/x/committee/abci.go +++ b/x/committee/abci.go @@ -1,12 +1,23 @@ package committee -// func BeginBlocker() { -// // TODO much the same as the current gov endblocker does +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" -// // Get all active proposals -// // If voting periods are over, tally up the results -// // If a proposal passes run it through the correct handler -// // Handler need to be registered in app.go as they are for the current gov module -// handler := keeper.Router().GetRoute(proposal.ProposalRoute()) -// err := handler(ctx, proposal.Content) -// } + "github.com/kava-labs/kava/x/committee/types" +) + +// BeginBlocker runs at the start of every block. +func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k Keeper) { + + // Close all expired proposals + // TODO optimize by using an index to avoid iterating over non expired proposals + k.IterateProposals(ctx, func(proposal types.Proposal) bool { + if proposal.HasExpiredBy(ctx.BlockTime()) { + if err := k.CloseOutProposal(ctx, proposal.ID); err != nil { + panic(err) // if an expired proposal does not close then something has gone very wrong + } + } + return false + }) +} diff --git a/x/committee/abci_test.go b/x/committee/abci_test.go new file mode 100644 index 00000000..7c64b766 --- /dev/null +++ b/x/committee/abci_test.go @@ -0,0 +1,68 @@ +package committee_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/committee" +) + +type ModuleTestSuite struct { + suite.Suite + + keeper committee.Keeper + app app.TestApp + ctx sdk.Context + + addresses []sdk.AccAddress +} + +func (suite *ModuleTestSuite) SetupTest() { + suite.app = app.NewTestApp() + suite.keeper = suite.app.GetCommitteeKeeper() + suite.ctx = suite.app.NewContext(true, abci.Header{}) + _, suite.addresses = app.GeneratePrivKeyAddressPairs(5) +} + +func (suite *ModuleTestSuite) TestBeginBlock() { + suite.app.InitializeFromGenesisStates() + // TODO replace below with genesis state + normalCom := committee.Committee{ + ID: 12, + Members: suite.addresses[:2], + Permissions: []committee.Permission{committee.GodPermission{}}, + } + suite.keeper.SetCommittee(suite.ctx, normalCom) + + pprop1 := gov.NewTextProposal("1A Title", "A description of this proposal.") + id1, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop1) + suite.NoError(err) + + oneHrLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour)) + pprop2 := gov.NewTextProposal("2A Title", "A description of this proposal.") + id2, err := suite.keeper.SubmitProposal(oneHrLaterCtx, normalCom.Members[0], normalCom.ID, pprop2) + suite.NoError(err) + + // Run BeginBlocker + proposalDurationLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(committee.MaxProposalDuration)) + suite.NotPanics(func() { + committee.BeginBlocker(proposalDurationLaterCtx, abci.RequestBeginBlock{}, suite.keeper) + }) + + // Check expired proposals are gone + _, found := suite.keeper.GetProposal(suite.ctx, id1) + suite.False(found, "expected expired proposal to be closed") + _, found = suite.keeper.GetProposal(suite.ctx, id2) + suite.True(found, "expected non expired proposal to be not closed") +} + +func TestModuleTestSuite(t *testing.T) { + suite.Run(t, new(ModuleTestSuite)) +} diff --git a/x/committee/alias.go b/x/committee/alias.go index 48abeee3..cd23dfbf 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -9,27 +9,35 @@ import ( const ( DefaultNextProposalID = types.DefaultNextProposalID + DefaultParamspace = types.DefaultParamspace ModuleName = types.ModuleName + QuerierRoute = types.QuerierRoute + RouterKey = types.RouterKey StoreKey = types.StoreKey + TypeMsgSubmitProposal = types.TypeMsgSubmitProposal + TypeMsgVote = types.TypeMsgVote ) var ( // function aliases - NewKeeper = keeper.NewKeeper - DefaultGenesisState = types.DefaultGenesisState - GetKeyFromID = types.GetKeyFromID - GetVoteKey = types.GetVoteKey - NewGenesisState = types.NewGenesisState - RegisterCodec = types.RegisterCodec - Uint64FromBytes = types.Uint64FromBytes + NewKeeper = keeper.NewKeeper + DefaultGenesisState = types.DefaultGenesisState + GetKeyFromID = types.GetKeyFromID + GetVoteKey = types.GetVoteKey + NewGenesisState = types.NewGenesisState + NewMsgSubmitProposal = types.NewMsgSubmitProposal + NewMsgVote = types.NewMsgVote + RegisterCodec = types.RegisterCodec + Uint64FromBytes = types.Uint64FromBytes // variable aliases - CommitteeKeyPrefix = types.CommitteeKeyPrefix - ModuleCdc = types.ModuleCdc - NextProposalIDKey = types.NextProposalIDKey - ProposalKeyPrefix = types.ProposalKeyPrefix - VoteKeyPrefix = types.VoteKeyPrefix - VoteThreshold = types.VoteThreshold + CommitteeKeyPrefix = types.CommitteeKeyPrefix + MaxProposalDuration = types.MaxProposalDuration + ModuleCdc = types.ModuleCdc + NextProposalIDKey = types.NextProposalIDKey + ProposalKeyPrefix = types.ProposalKeyPrefix + VoteKeyPrefix = types.VoteKeyPrefix + VoteThreshold = types.VoteThreshold ) type ( diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 59337b39..f6ba1fee 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -1,10 +1,11 @@ package keeper import ( + "time" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/kava-labs/kava/x/committee/types" @@ -94,7 +95,7 @@ func (k Keeper) IncrementNextProposalID(ctx sdk.Context) sdk.Error { } // StoreNewProposal stores a proposal, adding a new ID -func (k Keeper) StoreNewProposal(ctx sdk.Context, committeeID uint64, pubProposal types.PubProposal) (uint64, sdk.Error) { +func (k Keeper) StoreNewProposal(ctx sdk.Context, pubProposal types.PubProposal, committeeID uint64, deadline time.Time) (uint64, sdk.Error) { newProposalID, err := k.GetNextProposalID(ctx) if err != nil { return 0, err @@ -103,6 +104,7 @@ func (k Keeper) StoreNewProposal(ctx sdk.Context, committeeID uint64, pubProposa PubProposal: pubProposal, ID: newProposalID, CommitteeID: committeeID, + Deadline: deadline, } k.SetProposal(ctx, proposal) @@ -139,6 +141,22 @@ func (k Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) { store.Delete(types.GetKeyFromID(proposalID)) } +// IterateProposals provides an iterator over all stored proposals. +// For each proposal, cb will be called. If cb returns true, the iterator will close and stop. +func (k Keeper) IterateProposals(ctx sdk.Context, cb func(proposal types.Proposal) (stop bool)) { + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var proposal types.Proposal + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &proposal) + + if cb(proposal) { + break + } + } +} + // ---------- Votes ---------- // GetVote gets a vote from the store. diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index 4aeeac82..0bd65072 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -27,7 +27,8 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ } // Get a new ID and store the proposal - return k.StoreNewProposal(ctx, committeeID, pubProposal) + deadline := ctx.BlockTime().Add(types.MaxProposalDuration) + return k.StoreNewProposal(ctx, pubProposal, committeeID, deadline) } func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) sdk.Error { @@ -36,6 +37,9 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress if !found { return sdk.ErrInternal("proposal not found") } + if pr.HasExpiredBy(ctx.BlockTime()) { + return sdk.ErrInternal("proposal expired") + } com, found := k.GetCommittee(ctx, pr.CommitteeID) if !found { return sdk.ErrInternal("committee disbanded") @@ -65,7 +69,9 @@ func (k Keeper) CloseOutProposal(ctx sdk.Context, proposalID uint64) sdk.Error { votes = append(votes, vote) return false }) - if sdk.NewDec(int64(len(votes))).GTE(types.VoteThreshold.MulInt64(int64(len(com.Members)))) { // TODO move vote counting stuff to committee methods // TODO add timeout check here - close if expired regardless of votes + proposalPasses := sdk.NewDec(int64(len(votes))).GTE(types.VoteThreshold.MulInt64(int64(len(com.Members)))) + + if proposalPasses { // eneact vote // The proposal handler may execute state mutating logic depending // on the proposal content. If the handler fails, no state mutation @@ -77,16 +83,17 @@ func (k Keeper) CloseOutProposal(ctx sdk.Context, proposalID uint64) sdk.Error { // write state to the underlying multi-store writeCache() } // if handler returns error, then still delete the proposal - it's still over, but send an event + } + if proposalPasses || pr.HasExpiredBy(ctx.BlockTime()) { // delete proposal and votes k.DeleteProposal(ctx, proposalID) for _, v := range votes { k.DeleteVote(ctx, v.ProposalID, v.Voter) } - } else { - return sdk.ErrInternal("note enough votes to close proposal") + return nil } - return nil + return sdk.ErrInternal("note enough votes to close proposal") } func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error { diff --git a/x/committee/keeper/proposal_test.go b/x/committee/keeper/proposal_test.go index fe5ca6c7..0d9edcc2 100644 --- a/x/committee/keeper/proposal_test.go +++ b/x/committee/keeper/proposal_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "reflect" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov" @@ -89,8 +90,10 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { if tc.expectPass { suite.NoError(err) - _, found := keeper.GetProposal(ctx, id) + pr, found := keeper.GetProposal(ctx, id) suite.True(found) + suite.Equal(tc.committeeID, pr.CommitteeID) + suite.Equal(ctx.BlockTime().Add(types.MaxProposalDuration), pr.Deadline) } else { suite.NotNil(err) } @@ -104,11 +107,13 @@ func (suite *KeeperTestSuite) TestAddVote() { Members: suite.addresses[:2], Permissions: []types.Permission{types.GodPermission{}}, } + firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC) testcases := []struct { name string proposalID uint64 voter sdk.AccAddress + voteTime time.Time expectPass bool }{ { @@ -129,6 +134,13 @@ func (suite *KeeperTestSuite) TestAddVote() { voter: suite.addresses[4], expectPass: false, }, + { + name: "proposal expired", + proposalID: types.DefaultNextProposalID, + voter: normalCom.Members[0], + voteTime: firstBlockTime.Add(types.MaxProposalDuration), + expectPass: false, + }, } for _, tc := range testcases { @@ -136,7 +148,7 @@ func (suite *KeeperTestSuite) TestAddVote() { // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. tApp := app.NewTestApp() keeper := tApp.GetCommitteeKeeper() - ctx := tApp.NewContext(true, abci.Header{}) + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime}) tApp.InitializeFromGenesisStates() // setup the committee and proposal @@ -144,6 +156,7 @@ func (suite *KeeperTestSuite) TestAddVote() { _, err := keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, gov.NewTextProposal("A Title", "A description of this proposal.")) suite.NoError(err) + ctx = ctx.WithBlockTime(tc.voteTime) err = keeper.AddVote(ctx, tc.proposalID, tc.voter) if tc.expectPass { diff --git a/x/committee/module.go b/x/committee/module.go index 6372da75..97752b05 100644 --- a/x/committee/module.go +++ b/x/committee/module.go @@ -150,7 +150,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { // BeginBlock module begin-block func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { - // TODO BeginBlocker(ctx, req, am.keeper) + BeginBlocker(ctx, req, am.keeper) } // EndBlock module end-block diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 2930bd37..deec6e68 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -1,13 +1,19 @@ package types import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov" ) -// -------- Committees -------- +// TODO move these into params +var ( + VoteThreshold sdk.Dec = sdk.MustNewDecFromStr("0.75") + MaxProposalDuration time.Duration = time.Hour * 24 * 7 +) -var VoteThreshold sdk.Dec = sdk.MustNewDecFromStr("0.75") +// -------- Committees -------- // A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. type Committee struct { @@ -50,6 +56,13 @@ type Proposal struct { PubProposal ID uint64 CommitteeID uint64 + Deadline time.Time +} + +// HasExpiredBy calculates if the proposal will have expired by a certain time. +// All votes must be cast before deadline, those cast at time == deadline are not valid +func (p Proposal) HasExpiredBy(time time.Time) bool { + return !time.Before(p.Deadline) } type Vote struct { From a0e4ee77366d84d14f2e9da2c062e9e7410db671 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 13 Mar 2020 15:11:31 +0000 Subject: [PATCH 16/54] add querier --- x/committee/keeper/keeper.go | 16 ++ x/committee/keeper/querier.go | 220 +++++++++++++++++++++++++ x/committee/keeper/querier_test.go | 252 +++++++++++++++++++++++++++++ x/committee/types/querier.go | 49 ++++++ x/committee/types/types.go | 17 ++ 5 files changed, 554 insertions(+) create mode 100644 x/committee/keeper/querier.go create mode 100644 x/committee/keeper/querier_test.go create mode 100644 x/committee/types/querier.go diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index f6ba1fee..3b01c33e 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -66,6 +66,22 @@ func (k Keeper) DeleteCommittee(ctx sdk.Context, committeeID uint64) { store.Delete(types.GetKeyFromID(committeeID)) } +// IterateCommittees provides an iterator over all stored committees. +// For each committee, cb will be called. If cb returns true, the iterator will close and stop. +func (k Keeper) IterateCommittees(ctx sdk.Context, cb func(committee types.Committee) (stop bool)) { + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var committee types.Committee + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &committee) + + if cb(committee) { + break + } + } +} + // ---------- Proposals ---------- // SetNextProposalID stores an ID to be used for the next created proposal diff --git a/x/committee/keeper/querier.go b/x/committee/keeper/querier.go new file mode 100644 index 00000000..e0fa5439 --- /dev/null +++ b/x/committee/keeper/querier.go @@ -0,0 +1,220 @@ +package keeper + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/x/committee/types" +) + +// NewQuerier creates a new gov Querier instance +func NewQuerier(keeper Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, sdk.Error) { + switch path[0] { + + case types.QueryCommittees: + return queryCommittees(ctx, path[1:], req, keeper) + case types.QueryCommittee: + return queryCommittee(ctx, path[1:], req, keeper) + case types.QueryProposals: + return queryProposals(ctx, path[1:], req, keeper) + case types.QueryProposal: + return queryProposal(ctx, path[1:], req, keeper) + case types.QueryVotes: + return queryVotes(ctx, path[1:], req, keeper) + case types.QueryVote: + return queryVote(ctx, path[1:], req, keeper) + case types.QueryTally: + return queryTally(ctx, path[1:], req, keeper) + // case types.QueryParams: + // return queryParams(ctx, path[1:], req, keeper) + + default: + return nil, sdk.ErrUnknownRequest(fmt.Sprintf("unknown %s query endpoint", types.ModuleName)) + } + } +} + +// ---------- Committees ---------- + +func queryCommittees(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + + committees := []types.Committee{} + keeper.IterateCommittees(ctx, func(com types.Committee) bool { + committees = append(committees, com) + return false + }) + + bz, err := codec.MarshalJSONIndent(keeper.cdc, committees) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil +} + +func queryCommittee(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var params types.QueryCommitteeParams + err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + + committee, found := keeper.GetCommittee(ctx, params.CommitteeID) + if !found { + return nil, sdk.ErrInternal("not found") ///types.ErrUnknownProposal(types.DefaultCodespace, params.ProposalID) + } + + bz, err := codec.MarshalJSONIndent(keeper.cdc, committee) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil +} + +// ---------- Proposals ---------- + +func queryProposals(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var params types.QueryCommitteeParams + err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + + proposals := []types.Proposal{} + keeper.IterateProposals(ctx, func(p types.Proposal) bool { + if p.CommitteeID == params.CommitteeID { + proposals = append(proposals, p) + } + return false + }) + + bz, err := codec.MarshalJSONIndent(keeper.cdc, proposals) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil +} + +func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var params types.QueryProposalParams + err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + + proposal, found := keeper.GetProposal(ctx, params.ProposalID) + if !found { + return nil, sdk.ErrInternal("not found") // TODO types.ErrUnknownProposal(types.DefaultCodespace, params.ProposalID) + } + + bz, err := codec.MarshalJSONIndent(keeper.cdc, proposal) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil +} + +// ---------- Votes ---------- + +func queryVotes(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var params types.QueryProposalParams + err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + + votes := []types.Vote{} + keeper.IterateVotes(ctx, params.ProposalID, func(v types.Vote) bool { + votes = append(votes, v) + return false + }) + + bz, err := codec.MarshalJSONIndent(keeper.cdc, votes) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil +} + +func queryVote(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var params types.QueryVoteParams + err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + + vote, found := keeper.GetVote(ctx, params.ProposalID, params.Voter) + if !found { + return nil, sdk.ErrInternal("not found") + } + + bz, err := codec.MarshalJSONIndent(keeper.cdc, vote) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil +} + +// ---------- Tally ---------- + +func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var params types.QueryProposalParams + err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + + // TODO split tally and process result logic so tally logic can be used here + pr, found := keeper.GetProposal(ctx, params.ProposalID) + if !found { + return nil, sdk.ErrInternal("proposal not found") + } + com, found := keeper.GetCommittee(ctx, pr.CommitteeID) + if !found { + return nil, sdk.ErrInternal("committee disbanded") + } + votes := []types.Vote{} + keeper.IterateVotes(ctx, params.ProposalID, func(vote types.Vote) bool { + votes = append(votes, vote) + return false + }) + proposalPasses := sdk.NewDec(int64(len(votes))).GTE(types.VoteThreshold.MulInt64(int64(len(com.Members)))) + // TODO return some kind of tally object, rather than just a bool + + bz, err := codec.MarshalJSONIndent(keeper.cdc, proposalPasses) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil +} + +// ---------- Params ---------- + +// func queryParams(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { +// switch path[0] { +// case types.ParamDeposit: +// bz, err := codec.MarshalJSONIndent(keeper.cdc, keeper.GetDepositParams(ctx)) +// if err != nil { +// return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) +// } +// return bz, nil +// case types.ParamVoting: +// bz, err := codec.MarshalJSONIndent(keeper.cdc, keeper.GetVotingParams(ctx)) +// if err != nil { +// return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) +// } +// return bz, nil +// case types.ParamTallying: +// bz, err := codec.MarshalJSONIndent(keeper.cdc, keeper.GetTallyParams(ctx)) +// if err != nil { +// return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) +// } +// return bz, nil +// default: +// return nil, sdk.ErrUnknownRequest(fmt.Sprintf("%s is not a valid query request path", req.Path)) +// } +// } diff --git a/x/committee/keeper/querier_test.go b/x/committee/keeper/querier_test.go new file mode 100644 index 00000000..a2d8fd7e --- /dev/null +++ b/x/committee/keeper/querier_test.go @@ -0,0 +1,252 @@ +package keeper_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/committee/keeper" + "github.com/kava-labs/kava/x/committee/types" +) + +const ( + custom = "custom" +) + +type QuerierTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context + cdc *codec.Codec + + querier sdk.Querier + + addresses []sdk.AccAddress + committees []types.Committee + proposals []types.Proposal + votes map[uint64]([]types.Vote) + expectedTallyForTheFirstProposal bool // TODO replace once tallying has been refactored +} + +func (suite *QuerierTestSuite) SetupTest() { + // SetupTest function runs before every test, but a new suite is not created every time. + // So be careful about modifying data on suite as data from previous tests will still be there. + // For example, don't append proposal to suite.proposals, initialize a new slice value. + suite.app = app.NewTestApp() + suite.keeper = suite.app.GetCommitteeKeeper() + suite.ctx = suite.app.NewContext(true, abci.Header{}) + suite.cdc = suite.app.Codec() + suite.querier = keeper.NewQuerier(suite.keeper) + + _, suite.addresses = app.GeneratePrivKeyAddressPairs(5) + suite.app.InitializeFromGenesisStates() + // TODO replace below with genesis state + normalCom := types.Committee{ + ID: 12, + Members: suite.addresses[:2], + Permissions: []types.Permission{types.GodPermission{}}, + } + suite.keeper.SetCommittee(suite.ctx, normalCom) + + pprop1 := gov.NewTextProposal("1A Title", "A description of this proposal.") + id1, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop1) + suite.NoError(err) + + pprop2 := gov.NewTextProposal("2A Title", "A description of this proposal.") + id2, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop2) + suite.NoError(err) + + err = suite.keeper.AddVote(suite.ctx, id1, normalCom.Members[0]) + suite.NoError(err) + err = suite.keeper.AddVote(suite.ctx, id1, normalCom.Members[1]) + suite.NoError(err) + err = suite.keeper.AddVote(suite.ctx, id2, normalCom.Members[1]) + suite.NoError(err) + + suite.committees = []types.Committee{} + suite.committees = []types.Committee{normalCom} // TODO + suite.proposals = []types.Proposal{} + suite.keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { + suite.proposals = append(suite.proposals, p) + return false + }) + suite.votes = map[uint64]([]types.Vote){} + suite.keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { + suite.keeper.IterateVotes(suite.ctx, p.ID, func(v types.Vote) bool { + suite.votes[p.ID] = append(suite.votes[p.ID], v) + return false + }) + return false + }) + suite.expectedTallyForTheFirstProposal = true // TODO replace once tallying has been refactored + +} + +func (suite *QuerierTestSuite) TestQueryCommittees() { + ctx := suite.ctx.WithIsCheckTx(false) + // Set up request query + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryCommittees}, "/"), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryCommittees}, query) + suite.NoError(err) + suite.NotNil(bz) + + // Unmarshal the bytes + var committees []types.Committee + suite.NoError(suite.cdc.UnmarshalJSON(bz, &committees)) + + // Check + suite.Equal(suite.committees, committees) +} + +func (suite *QuerierTestSuite) TestQueryCommittee() { + ctx := suite.ctx.WithIsCheckTx(false) // ? + // Set up request query + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryCommittee}, "/"), + Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(suite.committees[0].ID)), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryCommittee}, query) + suite.NoError(err) + suite.NotNil(bz) + + // Unmarshal the bytes + var committee types.Committee + suite.NoError(suite.cdc.UnmarshalJSON(bz, &committee)) + + // Check + suite.Equal(suite.committees[0], committee) +} + +func (suite *QuerierTestSuite) TestQueryProposals() { + ctx := suite.ctx.WithIsCheckTx(false) + // Set up request query + comID := suite.proposals[0].CommitteeID + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposals}, "/"), + Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(comID)), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryProposals}, query) + suite.NoError(err) + suite.NotNil(bz) + + // Unmarshal the bytes + var proposals []types.Proposal + suite.NoError(suite.cdc.UnmarshalJSON(bz, &proposals)) + + // Check + expectedProposals := []types.Proposal{} + for _, p := range suite.proposals { + if p.CommitteeID == comID { + expectedProposals = append(expectedProposals, p) + } + } + suite.Equal(expectedProposals, proposals) +} + +func (suite *QuerierTestSuite) TestQueryProposal() { + ctx := suite.ctx.WithIsCheckTx(false) // ? + // Set up request query + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposal}, "/"), + Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(suite.proposals[0].ID)), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryProposal}, query) + suite.NoError(err) + suite.NotNil(bz) + + // Unmarshal the bytes + var proposal types.Proposal + suite.NoError(suite.cdc.UnmarshalJSON(bz, &proposal)) + + // Check + suite.Equal(suite.proposals[0], proposal) +} + +func (suite *QuerierTestSuite) TestQueryVotes() { + ctx := suite.ctx.WithIsCheckTx(false) + // Set up request query + propID := suite.proposals[0].ID + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryVotes}, "/"), + Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryVotes}, query) + suite.NoError(err) + suite.NotNil(bz) + + // Unmarshal the bytes + var votes []types.Vote + suite.NoError(suite.cdc.UnmarshalJSON(bz, &votes)) + + // Check + suite.Equal(suite.votes[propID], votes) +} + +func (suite *QuerierTestSuite) TestQueryVote() { + ctx := suite.ctx.WithIsCheckTx(false) // ? + // Set up request query + propID := suite.proposals[0].ID + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryVote}, "/"), + Data: suite.cdc.MustMarshalJSON(types.NewQueryVoteParams(propID, suite.votes[propID][0].Voter)), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryVote}, query) + suite.NoError(err) + suite.NotNil(bz) + + // Unmarshal the bytes + var vote types.Vote + suite.NoError(suite.cdc.UnmarshalJSON(bz, &vote)) + + // Check + suite.Equal(suite.votes[propID][0], vote) +} + +func (suite *QuerierTestSuite) TestQueryTally() { + ctx := suite.ctx.WithIsCheckTx(false) // ? + // Set up request query + propID := suite.proposals[0].ID + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryTally}, "/"), + Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryTally}, query) + suite.NoError(err) + suite.NotNil(bz) + + // Unmarshal the bytes + var tally bool + suite.NoError(suite.cdc.UnmarshalJSON(bz, &tally)) + + // Check + expectedTally := suite.expectedTallyForTheFirstProposal + suite.Equal(expectedTally, tally) +} +func TestQuerierTestSuite(t *testing.T) { + suite.Run(t, new(QuerierTestSuite)) +} diff --git a/x/committee/types/querier.go b/x/committee/types/querier.go new file mode 100644 index 00000000..5f8a4988 --- /dev/null +++ b/x/committee/types/querier.go @@ -0,0 +1,49 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Query endpoints supported by the Querier +const ( + //QueryParams = "params" + QueryCommittees = "committees" + QueryCommittee = "committee" + QueryProposals = "proposals" + QueryProposal = "proposal" + QueryVotes = "votes" + QueryVote = "vote" + QueryTally = "tally" +) + +type QueryCommitteeParams struct { + CommitteeID uint64 +} + +func NewQueryCommitteeParams(committeeID uint64) QueryCommitteeParams { + return QueryCommitteeParams{ + CommitteeID: committeeID, + } +} + +type QueryProposalParams struct { + ProposalID uint64 +} + +func NewQueryProposalParams(proposalID uint64) QueryProposalParams { + return QueryProposalParams{ + ProposalID: proposalID, + } +} + +type QueryVoteParams struct { + ProposalID uint64 + Voter sdk.AccAddress +} + +func NewQueryVoteParams(proposalID uint64, voter sdk.AccAddress) QueryVoteParams { + return QueryVoteParams{ + ProposalID: proposalID, + Voter: voter, + } +} diff --git a/x/committee/types/types.go b/x/committee/types/types.go index deec6e68..fb4c0d42 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -1,6 +1,8 @@ package types import ( + "fmt" + "strings" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -65,6 +67,21 @@ func (p Proposal) HasExpiredBy(time time.Time) bool { return !time.Before(p.Deadline) } +// String implements the fmt.Stringer interface, and importantly overrides the String methods inherited from the embedded PubProposal type. +func (p Proposal) String() string { + return strings.TrimSpace(fmt.Sprintf(`Proposal: + PubProposal: +%s + ID: %d + Committee ID: %d + Deadline: %s`, + p.PubProposal, + p.ID, + p.CommitteeID, + p.Deadline, + )) +} + type Vote struct { ProposalID uint64 Voter sdk.AccAddress From 4ef5b7d56fc25527c2d51120c485d2bf2dc33a21 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 13 Mar 2020 23:13:42 +0000 Subject: [PATCH 17/54] add untested cli methods --- x/committee/alias.go | 50 ++-- x/committee/client/cli/query.go | 392 +++++++++++++++++++++++++++ x/committee/client/cli/tx.go | 177 ++++++++++++ x/committee/client/query_proposer.go | 58 ++++ x/committee/keeper/proposal.go | 15 +- x/committee/keeper/querier.go | 2 +- x/committee/module.go | 16 +- x/committee/types/events.go | 19 ++ x/committee/types/msg.go | 2 +- 9 files changed, 702 insertions(+), 29 deletions(-) create mode 100644 x/committee/client/cli/query.go create mode 100644 x/committee/client/cli/tx.go create mode 100644 x/committee/client/query_proposer.go create mode 100644 x/committee/types/events.go diff --git a/x/committee/alias.go b/x/committee/alias.go index cd23dfbf..d58482a1 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -8,27 +8,40 @@ import ( ) const ( - DefaultNextProposalID = types.DefaultNextProposalID - DefaultParamspace = types.DefaultParamspace - ModuleName = types.ModuleName - QuerierRoute = types.QuerierRoute - RouterKey = types.RouterKey - StoreKey = types.StoreKey - TypeMsgSubmitProposal = types.TypeMsgSubmitProposal - TypeMsgVote = types.TypeMsgVote + AttributeKeyProposalID = types.AttributeKeyProposalID + DefaultNextProposalID = types.DefaultNextProposalID + DefaultParamspace = types.DefaultParamspace + EventTypeSubmitProposal = types.EventTypeSubmitProposal + ModuleName = types.ModuleName + QuerierRoute = types.QuerierRoute + QueryCommittee = types.QueryCommittee + QueryCommittees = types.QueryCommittees + QueryProposal = types.QueryProposal + QueryProposals = types.QueryProposals + QueryTally = types.QueryTally + QueryVote = types.QueryVote + QueryVotes = types.QueryVotes + RouterKey = types.RouterKey + StoreKey = types.StoreKey + TypeMsgSubmitProposal = types.TypeMsgSubmitProposal + TypeMsgVote = types.TypeMsgVote ) var ( // function aliases - NewKeeper = keeper.NewKeeper - DefaultGenesisState = types.DefaultGenesisState - GetKeyFromID = types.GetKeyFromID - GetVoteKey = types.GetVoteKey - NewGenesisState = types.NewGenesisState - NewMsgSubmitProposal = types.NewMsgSubmitProposal - NewMsgVote = types.NewMsgVote - RegisterCodec = types.RegisterCodec - Uint64FromBytes = types.Uint64FromBytes + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + DefaultGenesisState = types.DefaultGenesisState + GetKeyFromID = types.GetKeyFromID + GetVoteKey = types.GetVoteKey + NewGenesisState = types.NewGenesisState + NewMsgSubmitProposal = types.NewMsgSubmitProposal + NewMsgVote = types.NewMsgVote + NewQueryCommitteeParams = types.NewQueryCommitteeParams + NewQueryProposalParams = types.NewQueryProposalParams + NewQueryVoteParams = types.NewQueryVoteParams + RegisterCodec = types.RegisterCodec + Uint64FromBytes = types.Uint64FromBytes // variable aliases CommitteeKeyPrefix = types.CommitteeKeyPrefix @@ -53,6 +66,9 @@ type ( Permission = types.Permission Proposal = types.Proposal PubProposal = types.PubProposal + QueryCommitteeParams = types.QueryCommitteeParams + QueryProposalParams = types.QueryProposalParams + QueryVoteParams = types.QueryVoteParams ShutdownCDPDepsitPermission = types.ShutdownCDPDepsitPermission Vote = types.Vote ) diff --git a/x/committee/client/cli/query.go b/x/committee/client/cli/query.go new file mode 100644 index 00000000..23deeb76 --- /dev/null +++ b/x/committee/client/cli/query.go @@ -0,0 +1,392 @@ +package cli + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + + comclient "github.com/kava-labs/kava/x/committee/client" + "github.com/kava-labs/kava/x/committee/types" +) + +// GetQueryCmd returns the cli query commands for this module +func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + // Group gov queries under a subcommand + govQueryCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Querying commands for the governance module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + govQueryCmd.AddCommand(client.GetCommands( + //GetCmdQueryCommittee(queryRoute, cdc), + GetCmdQueryCommittees(queryRoute, cdc), + GetCmdQueryProposal(queryRoute, cdc), + GetCmdQueryProposals(queryRoute, cdc), + //GetCmdQueryVote(queryRoute, cdc), + GetCmdQueryVotes(queryRoute, cdc), + //GetCmdQueryParams(queryRoute, cdc), + GetCmdQueryProposer(queryRoute, cdc), + GetCmdQueryTally(queryRoute, cdc))...) + + return govQueryCmd +} + +// GetCmdQueryProposals implements a query proposals command. +func GetCmdQueryCommittees(queryRoute string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "committees", + Short: "Query all committees", + Long: "", // TODO + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Query + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryCommittees), nil) + if err != nil { + return err + } + + // Decode and print result + committees := []types.Committee{} + if err = cdc.UnmarshalJSON(res, &committees); err != nil { + return err + } + return cliCtx.PrintOutput(committees) + }, + } + return cmd +} + +// GetCmdQueryProposal implements the query proposal command. +func GetCmdQueryProposal(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "proposal [proposal-id]", + Args: cobra.ExactArgs(1), + Short: "Query details of a single proposal", + // Long: strings.TrimSpace( + // fmt.Sprintf(`Query details for a proposal. You can find the + // proposal-id by running "%s query gov proposals". + + // Example: + // $ %s query gov proposal 1 + // `, + // version.ClientName, version.ClientName, + // ), + // ), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Prepare params for querier + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("proposal-id %s not a valid uint", args[0]) + } + bz, err := cdc.MarshalJSON(types.NewQueryCommitteeParams(proposalID)) + if err != nil { + return err + } + + // Query + //res, err := gcutils.QueryProposalByID(proposalID, cliCtx, queryRoute) + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryProposal), bz) + if err != nil { + return err + } + + // Decode and print results + var proposal types.Proposal + cdc.MustUnmarshalJSON(res, &proposal) + return cliCtx.PrintOutput(proposal) + }, + } +} + +// GetCmdQueryProposals implements a query proposals command. +func GetCmdQueryProposals(queryRoute string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "proposals [committee-id]", + Short: "Query proposals by committee.", + Args: cobra.ExactArgs(1), + // Long: strings.TrimSpace( + // fmt.Sprintf(`Query for a all proposals. You can filter the returns with the following flags. + + // Example: + // $ %s query gov proposals --depositor cosmos1skjwj5whet0lpe65qaq4rpq03hjxlwd9nf39lk + // $ %s query gov proposals --voter cosmos1skjwj5whet0lpe65qaq4rpq03hjxlwd9nf39lk + // $ %s query gov proposals --status (DepositPeriod|VotingPeriod|Passed|Rejected) + // `, + // version.ClientName, version.ClientName, version.ClientName, + // ), + // ), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Prepare params for querier + committeeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("committee-id %s not a valid uint", args[0]) + } + bz, err := cdc.MarshalJSON(types.NewQueryCommitteeParams(committeeID)) + if err != nil { + return err + } + + // Query + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/proposals", queryRoute), bz) + if err != nil { + return err + } + + // Decode and print results + proposals := []types.Proposal{} // using empty (not nil) slice so json returns [] instead of null when there's no data // TODO check + err = cdc.UnmarshalJSON(res, &proposals) + if err != nil { + return err + } + return cliCtx.PrintOutput(proposals) + }, + } + return cmd +} + +// // Command to Get a Proposal Information +// // GetCmdQueryVote implements the query proposal vote command. +// func GetCmdQueryVote(queryRoute string, cdc *codec.Codec) *cobra.Command { +// return &cobra.Command{ +// Use: "vote [proposal-id] [voter-addr]", +// Args: cobra.ExactArgs(2), +// Short: "Query details of a single vote", +// Long: strings.TrimSpace( +// fmt.Sprintf(`Query details for a single vote on a proposal given its identifier. + +// Example: +// $ %s query gov vote 1 cosmos1skjwj5whet0lpe65qaq4rpq03hjxlwd9nf39lk +// `, +// version.ClientName, +// ), +// ), +// RunE: func(cmd *cobra.Command, args []string) error { +// cliCtx := context.NewCLIContext().WithCodec(cdc) + +// // validate that the proposal id is a uint +// proposalID, err := strconv.ParseUint(args[0], 10, 64) +// if err != nil { +// return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0]) +// } + +// // check to see if the proposal is in the store +// _, err = gcutils.QueryProposalByID(proposalID, cliCtx, queryRoute) +// if err != nil { +// return fmt.Errorf("failed to fetch proposal-id %d: %s", proposalID, err) +// } + +// voterAddr, err := sdk.AccAddressFromBech32(args[1]) +// if err != nil { +// return err +// } + +// params := types.NewQueryVoteParams(proposalID, voterAddr) +// bz, err := cdc.MarshalJSON(params) +// if err != nil { +// return err +// } + +// res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/vote", queryRoute), bz) +// if err != nil { +// return err +// } + +// var vote types.Vote + +// // XXX: Allow the decoding to potentially fail as the vote may have been +// // pruned from state. If so, decoding will fail and so we need to check the +// // Empty() case. Consider updating Vote JSON decoding to not fail when empty. +// _ = cdc.UnmarshalJSON(res, &vote) + +// if vote.Empty() { +// res, err = gcutils.QueryVoteByTxQuery(cliCtx, params) +// if err != nil { +// return err +// } + +// if err := cdc.UnmarshalJSON(res, &vote); err != nil { +// return err +// } +// } + +// return cliCtx.PrintOutput(vote) +// }, +// } +// } + +// GetCmdQueryVotes implements the command to query for proposal votes. +func GetCmdQueryVotes(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "votes [proposal-id]", + Args: cobra.ExactArgs(1), + Short: "Query votes on a proposal", + // Long: strings.TrimSpace( + // fmt.Sprintf(`Query vote details for a single proposal by its identifier. + + // Example: + // $ %s query gov votes 1 + // `, + // version.ClientName, + // ), + // ), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Prepare params for querier + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("proposal-id %s not a valid int", args[0]) + } + bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID)) + if err != nil { + return err + } + + // Query + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryVotes), bz) + if err != nil { + return err + } + + // Decode and print results + votes := []types.Vote{} // using empty (not nil) slice so json returns [] instead of null when there's no data // TODO check + err = cdc.UnmarshalJSON(res, &votes) + if err != nil { + return err + } + return cliCtx.PrintOutput(votes) + }, + } +} + +// GetCmdQueryTally implements the command to query for proposal tally result. +func GetCmdQueryTally(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "tally [proposal-id]", + Args: cobra.ExactArgs(1), + Short: "Get the tally of a proposal vote", + // Long: strings.TrimSpace( + // fmt.Sprintf(`Query tally of votes on a proposal. You can find + // the proposal-id by running "%s query gov proposals". + + // Example: + // $ %s query gov tally 1 + // `, + // version.ClientName, version.ClientName, + // ), + // ), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Prepare params for querier + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("proposal-id %s not a valid int", args[0]) + } + bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID)) + if err != nil { + return err + } + + // Query + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/tally", queryRoute), bz) + if err != nil { + return err + } + + // Decode and print results + var tally bool + cdc.MustUnmarshalJSON(res, &tally) // TODO must or normal, what's the difference on the cli? + return cliCtx.PrintOutput(tally) + }, + } +} + +// // GetCmdQueryProposal implements the query proposal command. +// func GetCmdQueryParams(queryRoute string, cdc *codec.Codec) *cobra.Command { +// return &cobra.Command{ +// Use: "params", +// Short: "Query the parameters of the governance process", +// Long: strings.TrimSpace( +// fmt.Sprintf(`Query the all the parameters for the governance process. + +// Example: +// $ %s query gov params +// `, +// version.ClientName, +// ), +// ), +// Args: cobra.NoArgs, +// RunE: func(cmd *cobra.Command, args []string) error { +// cliCtx := context.NewCLIContext().WithCodec(cdc) +// tp, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/params/tallying", queryRoute), nil) +// if err != nil { +// return err +// } +// dp, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/params/deposit", queryRoute), nil) +// if err != nil { +// return err +// } +// vp, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/params/voting", queryRoute), nil) +// if err != nil { +// return err +// } + +// var tallyParams types.TallyParams +// cdc.MustUnmarshalJSON(tp, &tallyParams) +// var depositParams types.DepositParams +// cdc.MustUnmarshalJSON(dp, &depositParams) +// var votingParams types.VotingParams +// cdc.MustUnmarshalJSON(vp, &votingParams) + +// return cliCtx.PrintOutput(types.NewParams(votingParams, tallyParams, depositParams)) +// }, +// } +// } + +// GetCmdQueryProposer implements the query proposer command. +func GetCmdQueryProposer(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "proposer [proposal-id]", + Args: cobra.ExactArgs(1), + Short: "Query the proposer of a governance proposal", + // Long: strings.TrimSpace( + // fmt.Sprintf(`Query which address proposed a proposal with a given ID. + + // Example: + // $ %s query gov proposer 1 + // `, + // version.ClientName, + // ), + // ), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // validate that the proposalID is a uint + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("proposal-id %s is not a valid uint", args[0]) + } + + prop, err := comclient.QueryProposer(cliCtx, proposalID) + if err != nil { + return err + } + + return cliCtx.PrintOutput(prop) + }, + } +} diff --git a/x/committee/client/cli/tx.go b/x/committee/client/cli/tx.go new file mode 100644 index 00000000..46674426 --- /dev/null +++ b/x/committee/client/cli/tx.go @@ -0,0 +1,177 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "strconv" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + + "github.com/kava-labs/kava/x/committee/types" +) + +// // Proposal flags +// const ( +// FlagTitle = "title" +// FlagDescription = "description" +// flagProposalType = "type" +// FlagDeposit = "deposit" +// flagVoter = "voter" +// flagDepositor = "depositor" +// flagStatus = "status" +// flagNumLimit = "limit" +// FlagProposal = "proposal" +// ) + +// type proposal struct { +// Title string +// Description string +// Type string +// Deposit string +// } + +// // ProposalFlags defines the core required fields of a proposal. It is used to +// // verify that these values are not provided in conjunction with a JSON proposal +// // file. +// var ProposalFlags = []string{ +// FlagTitle, +// FlagDescription, +// flagProposalType, +// FlagDeposit, +// } + +// GetTxCmd returns the transaction commands for this module +// governance ModuleClient is slightly different from other ModuleClients in that +// it contains a slice of "proposal" child commands. These commands are respective +// to proposal type handlers that are implemented in other modules but are mounted +// under the governance CLI (eg. parameter change proposals). +func GetTxCmd(storeKey string, cdc *codec.Codec /*, pcmds []*cobra.Command*/) *cobra.Command { // TODO why is storeKey here? + txCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "committee governance transactions subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + cmdSubmitProp := GetCmdSubmitProposal(cdc) + // for _, pcmd := range pcmds { + // cmdSubmitProp.AddCommand(client.PostCommands(pcmd)[0]) + // } + + txCmd.AddCommand(client.PostCommands( + GetCmdVote(cdc), + cmdSubmitProp, + )...) + + return txCmd +} + +// // GetCmdSubmitProposal is the root command on which commands for submitting proposals are registered. +// func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { +// cmd := &cobra.Command{ +// Use: "submit-proposal [committee-id]", +// Short: "Submit a governance proposal to a particular committee.", // TODO +// DisableFlagParsing: true, +// SuggestionsMinimumDistance: 2, +// RunE: client.ValidateCmd, +// } + +// return cmd +// } + +// GetCmdSubmitProposal +func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "submit-proposal [committee-id] [proposal-file]", + Short: "Submit a governance proposal to a particular committee.", + Long: "", // TODO + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Get proposing address + proposer := cliCtx.GetFromAddress() + + // Get committee ID + committeeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("committee-id %s not a valid int", args[0]) + } + + // Get the proposal + bz, err := ioutil.ReadFile(args[1]) + if err != nil { + return err + } + var pubProposal types.PubProposal + if err := cdc.UnmarshalJSON(bz, &pubProposal); err != nil { + return err + } + if err = pubProposal.ValidateBasic(); err != nil { + return err + } + + // Build message and run basic validation + msg := types.NewMsgSubmitProposal(pubProposal, proposer, committeeID) + err = msg.ValidateBasic() + if err != nil { + return err + } + + // Sign and broadcast message + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return cmd +} + +// GetCmdVote implements creating a new vote command. +func GetCmdVote(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "vote [proposal-id]", + Args: cobra.ExactArgs(2), + Short: "Vote for an active proposal", // TODO + // Long: strings.TrimSpace( + // fmt.Sprintf(`Submit a vote for an active proposal. You can + // find the proposal-id by running "%s query gov proposals". + + // Example: + // $ %s tx gov vote 1 yes --from mykey + // `, + // version.ClientName, version.ClientName, + // ), + // ), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Get voting address + from := cliCtx.GetFromAddress() + + // validate that the proposal id is a uint + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0]) + } + + // Build vote message and run basic validation + msg := types.NewMsgVote(from, proposalID) + err = msg.ValidateBasic() + if err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} diff --git a/x/committee/client/query_proposer.go b/x/committee/client/query_proposer.go new file mode 100644 index 00000000..29e61552 --- /dev/null +++ b/x/committee/client/query_proposer.go @@ -0,0 +1,58 @@ +package client + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + + "github.com/kava-labs/kava/x/committee/types" +) + +const ( + defaultPage = 1 + defaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19 // TODO what is this? +) + +// Proposer contains metadata of a governance proposal used for querying a proposer. +type Proposer struct { + ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` + Proposer string `json:"proposer" yaml:"proposer"` +} + +// NewProposer returns a new Proposer given id and proposer +func NewProposer(proposalID uint64, proposer string) Proposer { + return Proposer{proposalID, proposer} +} + +func (p Proposer) String() string { + return fmt.Sprintf("Proposal with ID %d was proposed by %s", p.ProposalID, p.Proposer) +} + +// QueryProposer will query for a proposer of a governance proposal by ID. +func QueryProposer(cliCtx context.CLIContext, proposalID uint64) (Proposer, error) { + events := []string{ + fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeyAction, types.TypeMsgSubmitProposal), + fmt.Sprintf("%s.%s='%s'", types.EventTypeSubmitProposal, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", proposalID))), + } + + // NOTE: SearchTxs is used to facilitate the txs query which does not currently + // support configurable pagination. + searchResult, err := utils.QueryTxsByEvents(cliCtx, events, defaultPage, defaultLimit) + if err != nil { + return Proposer{}, err + } + + for _, info := range searchResult.Txs { + for _, msg := range info.Tx.GetMsgs() { + // there should only be a single proposal under the given conditions + if msg.Type() == types.TypeMsgSubmitProposal { + subMsg := msg.(types.MsgSubmitProposal) + return NewProposer(proposalID, subMsg.Proposer.String()), nil + } + } + } + + return Proposer{}, fmt.Errorf("failed to find the proposer for proposalID %d", proposalID) +} diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index 0bd65072..7500a91d 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -1,6 +1,8 @@ package keeper import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/committee/types" @@ -28,7 +30,18 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ // Get a new ID and store the proposal deadline := ctx.BlockTime().Add(types.MaxProposalDuration) - return k.StoreNewProposal(ctx, pubProposal, committeeID, deadline) + proposalID, err := k.StoreNewProposal(ctx, pubProposal, committeeID, deadline) + if err != nil { + return 0, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeSubmitProposal, + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)), + ), + ) + return proposalID, nil } func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) sdk.Error { diff --git a/x/committee/keeper/querier.go b/x/committee/keeper/querier.go index e0fa5439..44dfa877 100644 --- a/x/committee/keeper/querier.go +++ b/x/committee/keeper/querier.go @@ -40,7 +40,7 @@ func NewQuerier(keeper Keeper) sdk.Querier { // ---------- Committees ---------- -func queryCommittees(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { +func queryCommittees(ctx sdk.Context, path []string, _ abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { committees := []types.Committee{} keeper.IterateCommittees(ctx, func(com types.Committee) bool { diff --git a/x/committee/module.go b/x/committee/module.go index 97752b05..a1164407 100644 --- a/x/committee/module.go +++ b/x/committee/module.go @@ -11,6 +11,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/x/committee/client/cli" ) var ( @@ -54,14 +56,12 @@ func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router // GetTxCmd returns the root tx command for the module. func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { - //return cli.GetTxCmd(cdc) - return nil + return cli.GetTxCmd(StoreKey, cdc) } -// GetQueryCmd returns the root query command for the auction module. +// GetQueryCmd returns the root query command for the module. func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { - //return cli.GetQueryCmd(StoreKey, cdc) - return nil + return cli.GetQueryCmd(StoreKey, cdc) } //____________________________________________________________________________ @@ -118,8 +118,7 @@ func (AppModule) Route() string { // NewHandler module handler func (am AppModule) NewHandler() sdk.Handler { - //return NewHandler(am.keeper) - return nil + return NewHandler(am.keeper) } // QuerierRoute module querier route name @@ -129,8 +128,7 @@ func (AppModule) QuerierRoute() string { // NewQuerierHandler module querier func (am AppModule) NewQuerierHandler() sdk.Querier { - // return NewQuerier(am.keeper) - return nil + return NewQuerier(am.keeper) } // InitGenesis module init-genesis diff --git a/x/committee/types/events.go b/x/committee/types/events.go new file mode 100644 index 00000000..5037f606 --- /dev/null +++ b/x/committee/types/events.go @@ -0,0 +1,19 @@ +package types + +// Module event types +const ( + EventTypeSubmitProposal = "submit_proposal" + // EventTypeProposalVote = "proposal_vote" + // EventTypeInactiveProposal = "inactive_proposal" + // EventTypeActiveProposal = "active_proposal" + + // AttributeKeyProposalResult = "proposal_result" + // AttributeKeyOption = "option" + AttributeKeyProposalID = "proposal_id" + // AttributeKeyVotingPeriodStart = "voting_period_start" + // AttributeValueCategory = "governance" + // AttributeValueProposalDropped = "proposal_dropped" // didn't meet min deposit + // AttributeValueProposalPassed = "proposal_passed" // met vote quorum + // AttributeValueProposalRejected = "proposal_rejected" // didn't meet vote quorum + // AttributeValueProposalFailed = "proposal_failed" // error on proposal handler +) diff --git a/x/committee/types/msg.go b/x/committee/types/msg.go index e262260b..dd34e727 100644 --- a/x/committee/types/msg.go +++ b/x/committee/types/msg.go @@ -5,7 +5,7 @@ import ( ) const ( - TypeMsgSubmitProposal = "submit_proposal" + TypeMsgSubmitProposal = "submit_proposal" // TODO these are the same as the gov module, will there be collisions? TypeMsgVote = "vote" ) From 62da823314787642d494c11f5738e73f901197ba Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 14 Mar 2020 01:16:45 +0000 Subject: [PATCH 18/54] add untested rest --- x/committee/client/rest/query.go | 397 +++++++++++++++++++++++++++++++ x/committee/client/rest/rest.go | 29 +++ x/committee/client/rest/tx.go | 110 +++++++++ x/committee/module.go | 3 +- 4 files changed, 538 insertions(+), 1 deletion(-) create mode 100644 x/committee/client/rest/query.go create mode 100644 x/committee/client/rest/rest.go create mode 100644 x/committee/client/rest/tx.go diff --git a/x/committee/client/rest/query.go b/x/committee/client/rest/query.go new file mode 100644 index 00000000..e7c2be72 --- /dev/null +++ b/x/committee/client/rest/query.go @@ -0,0 +1,397 @@ +package rest + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/types/rest" + + "github.com/kava-labs/kava/x/committee/client" + "github.com/kava-labs/kava/x/committee/types" +) + +func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc(fmt.Sprintf("/%s/committees", types.ModuleName), queryCommitteesHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/committees/{%s}", types.ModuleName, RestCommitteeID), queryCommitteeHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/committees/{%s}/proposals", types.ModuleName, RestCommitteeID), queryProposalsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}", types.ModuleName, RestProposalID), queryProposalHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/proposer", types.ModuleName, RestProposalID), queryProposerHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/tally", types.ModuleName, RestProposalID), queryTallyOnProposalHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes", types.ModuleName, RestProposalID), queryVotesOnProposalHandlerFn(cliCtx)).Methods("GET") + //r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes/{%s}", types.ModuleName, RestProposalID, RestVoter), queryVoteHandlerFn(cliCtx)).Methods("GET") + //r.HandleFunc(fmt.Sprintf("/%s/parameters/{%s}", types.ModuleName, RestParamsType), queryParamsHandlerFn(cliCtx)).Methods("GET") +} + +// ---------- Committees ---------- + +func queryCommitteesHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + // Query + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryCommittees), nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Write response + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryCommitteeHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + // Prepare params for querier + vars := mux.Vars(r) + if len(vars[RestCommitteeID]) == 0 { + err := errors.New("committeeID required but not specified") + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + committeeID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestCommitteeID]) + if !ok { + return + } + bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(committeeID)) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Query + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryCommittee), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Write response + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +// ---------- Proposals ---------- + +func queryProposalsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + // Prepare params for querier + vars := mux.Vars(r) + if len(vars[RestCommitteeID]) == 0 { + err := errors.New("committeeID required but not specified") + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + committeeID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestCommitteeID]) + if !ok { + return + } + bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(committeeID)) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Query + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryProposals), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Write response + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + // Prepare params for querier + vars := mux.Vars(r) + if len(vars[RestProposalID]) == 0 { + err := errors.New("proposalID required but not specified") + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID]) + if !ok { + return + } + bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID)) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Query + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryProposals), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Write response + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryProposerHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + // Prepare params for querier + vars := mux.Vars(r) + proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID]) + if !ok { + return + } + + // Query + res, err := client.QueryProposer(cliCtx, proposalID) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Write response + rest.PostProcessResponse(w, cliCtx, res) + } +} + +// ---------- Votes ---------- + +func queryVotesOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + // Prepare params for querier + vars := mux.Vars(r) + if len(vars[RestProposalID]) == 0 { + err := errors.New("proposalID required but not specified") + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID]) + if !ok { + return + } + bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID)) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Query + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryVotes), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // TODO should add this feature back + // var proposal types.Proposal + // if err := cliCtx.Codec.UnmarshalJSON(res, &proposal); err != nil { + // rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + // return + // } + + // // For inactive proposals we must query the txs directly to get the votes + // // as they're no longer in state. + // propStatus := proposal.Status + // if !(propStatus == types.StatusVotingPeriod || propStatus == types.StatusDepositPeriod) { + // res, err = gcutils.QueryVotesByTxQuery(cliCtx, params) + // } else { + // res, _, err = cliCtx.QueryWithData("custom/gov/votes", bz) + // } + + // if err != nil { + // rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + // return + // } + + // Write response + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +// func queryVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) { +// vars := mux.Vars(r) +// strProposalID := vars[RestProposalID] +// bechVoterAddr := vars[RestVoter] + +// if len(strProposalID) == 0 { +// err := errors.New("proposalId required but not specified") +// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) +// return +// } + +// proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, strProposalID) +// if !ok { +// return +// } + +// if len(bechVoterAddr) == 0 { +// err := errors.New("voter address required but not specified") +// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) +// return +// } + +// voterAddr, err := sdk.AccAddressFromBech32(bechVoterAddr) +// if err != nil { +// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) +// return +// } + +// cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) +// if !ok { +// return +// } + +// params := types.NewQueryVoteParams(proposalID, voterAddr) + +// bz, err := cliCtx.Codec.MarshalJSON(params) +// if err != nil { +// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) +// return +// } + +// res, _, err := cliCtx.QueryWithData("custom/gov/vote", bz) +// if err != nil { +// rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) +// return +// } + +// var vote types.Vote +// if err := cliCtx.Codec.UnmarshalJSON(res, &vote); err != nil { +// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) +// return +// } + +// // For an empty vote, either the proposal does not exist or is inactive in +// // which case the vote would be removed from state and should be queried for +// // directly via a txs query. +// if vote.Empty() { +// bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID)) +// if err != nil { +// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) +// return +// } + +// res, _, err = cliCtx.QueryWithData("custom/gov/proposal", bz) +// if err != nil || len(res) == 0 { +// err := fmt.Errorf("proposalID %d does not exist", proposalID) +// rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) +// return +// } + +// res, err = gcutils.QueryVoteByTxQuery(cliCtx, params) +// if err != nil { +// rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) +// return +// } +// } + +// rest.PostProcessResponse(w, cliCtx, res) +// } +// } + +func queryTallyOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + // Prepare params for querier + vars := mux.Vars(r) + if len(vars[RestProposalID]) == 0 { + err := errors.New("proposalID required but not specified") + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID]) + if !ok { + return + } + bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID)) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Query + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryTally), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Write response + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +// ---------- Params ---------- + +// func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) { +// vars := mux.Vars(r) +// paramType := vars[RestParamsType] + +// cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) +// if !ok { +// return +// } + +// res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/gov/%s/%s", types.QueryParams, paramType), nil) +// if err != nil { +// rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) +// return +// } + +// cliCtx = cliCtx.WithHeight(height) +// rest.PostProcessResponse(w, cliCtx, res) +// } +// } diff --git a/x/committee/client/rest/rest.go b/x/committee/client/rest/rest.go new file mode 100644 index 00000000..8ff0a7b5 --- /dev/null +++ b/x/committee/client/rest/rest.go @@ -0,0 +1,29 @@ +package rest + +import ( + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" +) + +// REST Variable names +const ( + RestProposalID = "proposal-id" + RestCommitteeID = "committee-id" + RestVoter = "voter" + //RestProposalStatus = "status" + //RestNumLimit = "limit" +) + +// // ProposalRESTHandler defines a REST handler implemented in another module. The +// // sub-route is mounted on the governance REST handler. +// type ProposalRESTHandler struct { +// SubRoute string +// Handler func(http.ResponseWriter, *http.Request) +// } + +// RegisterRoutes - Central function to define routes that get registered by the main application +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router /*, phs []ProposalRESTHandler*/) { + registerQueryRoutes(cliCtx, r) + registerTxRoutes(cliCtx, r /* , phs*/) +} diff --git a/x/committee/client/rest/tx.go b/x/committee/client/rest/tx.go new file mode 100644 index 00000000..20be14e6 --- /dev/null +++ b/x/committee/client/rest/tx.go @@ -0,0 +1,110 @@ +package rest + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + + "github.com/kava-labs/kava/x/committee/types" +) + +func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router /*, phs []ProposalRESTHandler*/) { + // propSubRtr := r.PathPrefix("/gov/proposals").Subrouter() + // for _, ph := range phs { + // propSubRtr.HandleFunc(fmt.Sprintf("/%s", ph.SubRoute), ph.Handler).Methods("POST") + // } + + r.HandleFunc(fmt.Sprintf("/%s/committees/{%s}/proposals", types.ModuleName, RestCommitteeID), postProposalHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes", types.ModuleName, RestProposalID), postVoteHandlerFn(cliCtx)).Methods("POST") +} + +// PostProposalReq defines the properties of a proposal request's body. +type PostProposalReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + PubProposal types.PubProposal `json:"pub_proposal" yaml:"pub_proposal"` + Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` +} + +func postProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + // Parse and validate url params + vars := mux.Vars(r) + if len(vars[RestCommitteeID]) == 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, "committeeID required but not specified") + return + } + committeeID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestCommitteeID]) + if !ok { + return + } + + // Parse and validate http request body + var req PostProposalReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + if err := req.PubProposal.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Create and return a StdTx + msg := types.NewMsgSubmitProposal(req.PubProposal, req.Proposer, committeeID) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +// PostVoteReq defines the properties of a vote request's body. +type PostVoteReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Voter sdk.AccAddress `json:"voter" yaml:"voter"` +} + +func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + // Parse and validate url params + vars := mux.Vars(r) + if len(vars[RestProposalID]) == 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, "proposalID required but not specified") + return + } + proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID]) + if !ok { + return + } + + // Parse and validate http request body + var req PostVoteReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + // Create and return a StdTx + msg := types.NewMsgVote(req.Voter, proposalID) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/committee/module.go b/x/committee/module.go index a1164407..e188049b 100644 --- a/x/committee/module.go +++ b/x/committee/module.go @@ -13,6 +13,7 @@ import ( abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/x/committee/client/cli" + "github.com/kava-labs/kava/x/committee/client/rest" ) var ( @@ -51,7 +52,7 @@ func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { // RegisterRESTRoutes registers the REST routes for the module. func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { - //rest.RegisterRoutes(ctx, rtr) + rest.RegisterRoutes(ctx, rtr) } // GetTxCmd returns the root tx command for the module. From 18dfcd2a3dc0484e65ab36ba24e47b04ca76db0b Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sun, 15 Mar 2020 00:00:05 +0000 Subject: [PATCH 19/54] add genesis init/export/validation --- x/committee/genesis.go | 40 +++++++++++++++-- x/committee/genesis_test.go | 75 +++++++++++++++++++++++++++++++ x/committee/types/genesis.go | 75 +++++++++++++++++++++++++++---- x/committee/types/genesis_test.go | 36 +++++++++++++++ 4 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 x/committee/genesis_test.go create mode 100644 x/committee/types/genesis_test.go diff --git a/x/committee/genesis.go b/x/committee/genesis.go index fef015c9..cc0b9409 100644 --- a/x/committee/genesis.go +++ b/x/committee/genesis.go @@ -4,6 +4,7 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/committee/types" ) // InitGenesis initializes the store state from a genesis state. @@ -14,11 +15,44 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, gs GenesisState) { keeper.SetNextProposalID(ctx, gs.NextProposalID) - // TODO set votes, committee, proposals + for _, com := range gs.Committees { + keeper.SetCommittee(ctx, com) + } + for _, p := range gs.Proposals { + keeper.SetProposal(ctx, p) + } + for _, v := range gs.Votes { + keeper.SetVote(ctx, v) + } } // ExportGenesis returns a GenesisState for a given context and keeper. func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { - // TODO - return GenesisState{} + + nextID, err := keeper.GetNextProposalID(ctx) + if err != nil { + panic(err) + } + committees := []types.Committee{} + keeper.IterateCommittees(ctx, func(com types.Committee) bool { + committees = append(committees, com) + return false + }) + proposals := []types.Proposal{} + votes := []types.Vote{} + keeper.IterateProposals(ctx, func(p types.Proposal) bool { + proposals = append(proposals, p) + keeper.IterateVotes(ctx, p.ID, func(v types.Vote) bool { + votes = append(votes, v) + return false + }) + return false + }) + + return types.NewGenesisState( + nextID, + committees, + proposals, + votes, + ) } diff --git a/x/committee/genesis_test.go b/x/committee/genesis_test.go new file mode 100644 index 00000000..c64664eb --- /dev/null +++ b/x/committee/genesis_test.go @@ -0,0 +1,75 @@ +package committee_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/committee" + "github.com/kava-labs/kava/x/committee/types" +) + +type GenesisTestSuite struct { + suite.Suite + + app app.TestApp + ctx sdk.Context + keeper committee.Keeper +} + +func (suite *GenesisTestSuite) TestGenesis() { + testCases := []struct { + name string + genState types.GenesisState + expectPass bool + }{ + { + name: "normal", + genState: types.DefaultGenesisState(), + expectPass: true, + }, + { + name: "invalid", + genState: types.NewGenesisState( + 2, + []types.Committee{}, + []types.Proposal{{ID: 1, CommitteeID: 57}}, + []types.Vote{}, + ), + expectPass: false, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Setup (note: suite.SetupTest is not run before every suite.Run) + suite.app = app.NewTestApp() + suite.keeper = suite.app.GetCommitteeKeeper() + suite.ctx = suite.app.NewContext(true, abci.Header{}) + + // Run + var exportedGenState types.GenesisState + run := func() { + committee.InitGenesis(suite.ctx, suite.keeper, tc.genState) + exportedGenState = committee.ExportGenesis(suite.ctx, suite.keeper) + } + if tc.expectPass { + suite.NotPanics(run) + } else { + suite.Panics(run) + } + + // Check + if tc.expectPass { + suite.Equal(tc.genState, exportedGenState) + } + }) + } +} + +func TestGenesisTestSuite(t *testing.T) { + suite.Run(t, new(GenesisTestSuite)) +} diff --git a/x/committee/types/genesis.go b/x/committee/types/genesis.go index c44b14c6..a259f228 100644 --- a/x/committee/types/genesis.go +++ b/x/committee/types/genesis.go @@ -2,6 +2,7 @@ package types import ( "bytes" + "fmt" ) // DefaultNextProposalID is the starting poiint for proposal IDs. @@ -10,18 +11,18 @@ const DefaultNextProposalID uint64 = 1 // GenesisState is state that must be provided at chain genesis. type GenesisState struct { NextProposalID uint64 - Votes []Vote - Proposals []Proposal Committees []Committee + Proposals []Proposal + Votes []Vote } // NewGenesisState returns a new genesis state object for the module. -func NewGenesisState(nextProposalID uint64, votes []Vote, proposals []Proposal, committees []Committee) GenesisState { +func NewGenesisState(nextProposalID uint64, committees []Committee, proposals []Proposal, votes []Vote) GenesisState { return GenesisState{ NextProposalID: nextProposalID, - Votes: votes, - Proposals: proposals, Committees: committees, + Proposals: proposals, + Votes: votes, } } @@ -29,9 +30,9 @@ func NewGenesisState(nextProposalID uint64, votes []Vote, proposals []Proposal, func DefaultGenesisState() GenesisState { return NewGenesisState( DefaultNextProposalID, - []Vote{}, - []Proposal{}, []Committee{}, + []Proposal{}, + []Vote{}, ) } @@ -48,4 +49,62 @@ func (data GenesisState) IsEmpty() bool { } // Validate performs basic validation of genesis data. -func (gs GenesisState) Validate() error { return nil } +func (gs GenesisState) Validate() error { + // validate committees + committeeMap := make(map[uint64]bool, len(gs.Committees)) + for _, com := range gs.Committees { + // check there are no duplicate IDs + if _, ok := committeeMap[com.ID]; ok { + return fmt.Errorf("duplicate committee ID found in genesis state; id: %d", com.ID) + } + committeeMap[com.ID] = true + + // validate committee + if len(com.Members) == 0 { + return fmt.Errorf("committee %d invalid: cannot have zero members", com.ID) + } + for _, m := range com.Members { + if m.Empty() { + return fmt.Errorf("committee %d invalid: found empty member address", com.ID) + } + } + } + + // validate proposals - pp.Val, no duplicate IDs, no ids >= nextID, committee needs to exist + proposalMap := make(map[uint64]bool, len(gs.Proposals)) + for _, p := range gs.Proposals { + // check there are no duplicate IDs + if _, ok := proposalMap[p.ID]; ok { + return fmt.Errorf("duplicate proposal ID found in genesis state; id: %d", p.ID) + } + proposalMap[p.ID] = true + + // validate next proposal ID + if p.ID >= gs.NextProposalID { + return fmt.Errorf("NextProposalID is not greater than all proposal IDs; id: %d", p.ID) + } + + // check committee exists + if !committeeMap[p.CommitteeID] { + return fmt.Errorf("proposal refers to non existant committee; proposal: %+v", p) + } + + // validate pubProposal + if err := p.PubProposal.ValidateBasic(); err != nil { + return fmt.Errorf("proposal %d invalid: %w", p.ID, err) + } + } + + // validate votes + for _, v := range gs.Votes { + // check proposal exists + if !proposalMap[v.ProposalID] { + return fmt.Errorf("vote refers to non existant proposal; vote: %+v", v) + } + // validate address + if v.Voter.Empty() { + return fmt.Errorf("found empty voter address; vote: %+v", v) + } + } + return nil +} diff --git a/x/committee/types/genesis_test.go b/x/committee/types/genesis_test.go new file mode 100644 index 00000000..3a9835ee --- /dev/null +++ b/x/committee/types/genesis_test.go @@ -0,0 +1,36 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenesisState_Validate(t *testing.T) { + testCases := []struct { + name string + genState GenesisState + expectPass bool + }{ + { + name: "normal", + genState: DefaultGenesisState(), + expectPass: true, + }, + // TODO test failure cases + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + err := tc.genState.Validate() + + if tc.expectPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } + +} From fbf67b4527275511824c8d2d070dda07fd1dc3e9 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 21 Mar 2020 18:06:58 +0000 Subject: [PATCH 20/54] add committee change gov proposals --- x/committee/alias.go | 68 ++++---- x/committee/keeper/proposal.go | 19 ++- x/committee/proposal_handler.go | 66 ++++++- x/committee/proposal_handler_test.go | 247 +++++++++++++++++++++++++++ x/committee/types/codec.go | 18 +- x/committee/types/errors.go | 9 + x/committee/types/gov_proposal.go | 120 +++++++++++++ x/committee/types/proposal.go | 12 -- x/committee/types/types.go | 14 ++ 9 files changed, 522 insertions(+), 51 deletions(-) create mode 100644 x/committee/proposal_handler_test.go create mode 100644 x/committee/types/errors.go create mode 100644 x/committee/types/gov_proposal.go delete mode 100644 x/committee/types/proposal.go diff --git a/x/committee/alias.go b/x/committee/alias.go index d58482a1..b3e3cc2a 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -8,40 +8,45 @@ import ( ) const ( - AttributeKeyProposalID = types.AttributeKeyProposalID - DefaultNextProposalID = types.DefaultNextProposalID - DefaultParamspace = types.DefaultParamspace - EventTypeSubmitProposal = types.EventTypeSubmitProposal - ModuleName = types.ModuleName - QuerierRoute = types.QuerierRoute - QueryCommittee = types.QueryCommittee - QueryCommittees = types.QueryCommittees - QueryProposal = types.QueryProposal - QueryProposals = types.QueryProposals - QueryTally = types.QueryTally - QueryVote = types.QueryVote - QueryVotes = types.QueryVotes - RouterKey = types.RouterKey - StoreKey = types.StoreKey - TypeMsgSubmitProposal = types.TypeMsgSubmitProposal - TypeMsgVote = types.TypeMsgVote + AttributeKeyProposalID = types.AttributeKeyProposalID + DefaultCodespace = types.DefaultCodespace + DefaultNextProposalID = types.DefaultNextProposalID + DefaultParamspace = types.DefaultParamspace + EventTypeSubmitProposal = types.EventTypeSubmitProposal + ModuleName = types.ModuleName + ProposalTypeCommitteeChange = types.ProposalTypeCommitteeChange + ProposalTypeCommitteeDelete = types.ProposalTypeCommitteeDelete + QuerierRoute = types.QuerierRoute + QueryCommittee = types.QueryCommittee + QueryCommittees = types.QueryCommittees + QueryProposal = types.QueryProposal + QueryProposals = types.QueryProposals + QueryTally = types.QueryTally + QueryVote = types.QueryVote + QueryVotes = types.QueryVotes + RouterKey = types.RouterKey + StoreKey = types.StoreKey + TypeMsgSubmitProposal = types.TypeMsgSubmitProposal + TypeMsgVote = types.TypeMsgVote ) var ( // function aliases - NewKeeper = keeper.NewKeeper - NewQuerier = keeper.NewQuerier - DefaultGenesisState = types.DefaultGenesisState - GetKeyFromID = types.GetKeyFromID - GetVoteKey = types.GetVoteKey - NewGenesisState = types.NewGenesisState - NewMsgSubmitProposal = types.NewMsgSubmitProposal - NewMsgVote = types.NewMsgVote - NewQueryCommitteeParams = types.NewQueryCommitteeParams - NewQueryProposalParams = types.NewQueryProposalParams - NewQueryVoteParams = types.NewQueryVoteParams - RegisterCodec = types.RegisterCodec - Uint64FromBytes = types.Uint64FromBytes + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + DefaultGenesisState = types.DefaultGenesisState + GetKeyFromID = types.GetKeyFromID + GetVoteKey = types.GetVoteKey + NewCommitteeChangeProposal = types.NewCommitteeChangeProposal + NewCommitteeDeleteProposal = types.NewCommitteeDeleteProposal + NewGenesisState = types.NewGenesisState + NewMsgSubmitProposal = types.NewMsgSubmitProposal + NewMsgVote = types.NewMsgVote + NewQueryCommitteeParams = types.NewQueryCommitteeParams + NewQueryProposalParams = types.NewQueryProposalParams + NewQueryVoteParams = types.NewQueryVoteParams + RegisterCodec = types.RegisterCodec + Uint64FromBytes = types.Uint64FromBytes // variable aliases CommitteeKeyPrefix = types.CommitteeKeyPrefix @@ -56,10 +61,11 @@ var ( type ( Keeper = keeper.Keeper Committee = types.Committee + CommitteeChangeProposal = types.CommitteeChangeProposal + CommitteeDeleteProposal = types.CommitteeDeleteProposal GeneralShutdownPermission = types.GeneralShutdownPermission GenesisState = types.GenesisState GodPermission = types.GodPermission - GroupChangeProposal = types.GroupChangeProposal InflationRateChangePermission = types.InflationRateChangePermission MsgSubmitProposal = types.MsgSubmitProposal MsgVote = types.MsgVote diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index 7500a91d..f42be0f3 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -99,11 +99,7 @@ func (k Keeper) CloseOutProposal(ctx sdk.Context, proposalID uint64) sdk.Error { } if proposalPasses || pr.HasExpiredBy(ctx.BlockTime()) { - // delete proposal and votes - k.DeleteProposal(ctx, proposalID) - for _, v := range votes { - k.DeleteVote(ctx, v.ProposalID, v.Voter) - } + k.DeleteProposalAndVotes(ctx, proposalID) return nil } 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 } + +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) + } +} diff --git a/x/committee/proposal_handler.go b/x/committee/proposal_handler.go index 852a704f..45cf0554 100644 --- a/x/committee/proposal_handler.go +++ b/x/committee/proposal_handler.go @@ -1,4 +1,66 @@ package committee -// TODO create a GroupChangeProposalHandler, see params or distribution -// It will overwrite the Members of Permissions field of a group +import ( + "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 +} diff --git a/x/committee/proposal_handler_test.go b/x/committee/proposal_handler_test.go new file mode 100644 index 00000000..01a8ec75 --- /dev/null +++ b/x/committee/proposal_handler_test.go @@ -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)) +} diff --git a/x/committee/types/codec.go b/x/committee/types/codec.go index f44d1795..57d700b0 100644 --- a/x/committee/types/codec.go +++ b/x/committee/types/codec.go @@ -2,6 +2,9 @@ package types import ( "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 @@ -9,6 +12,13 @@ var ModuleCdc *codec.Codec func init() { 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) ModuleCdc = cdc.Seal() } @@ -16,9 +26,15 @@ func init() { // RegisterCodec registers the necessary types for the module 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.RegisterConcrete(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal", nil) + cdc.RegisterConcrete(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal", nil) + cdc.RegisterInterface((*Permission)(nil), nil) cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil) } diff --git a/x/committee/types/errors.go b/x/committee/types/errors.go new file mode 100644 index 00000000..57ccf762 --- /dev/null +++ b/x/committee/types/errors.go @@ -0,0 +1,9 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + DefaultCodespace sdk.CodespaceType = ModuleName +) diff --git a/x/committee/types/gov_proposal.go b/x/committee/types/gov_proposal.go new file mode 100644 index 00000000..6b2a94b6 --- /dev/null +++ b/x/committee/types/gov_proposal.go @@ -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) +} diff --git a/x/committee/types/proposal.go b/x/committee/types/proposal.go deleted file mode 100644 index 58403a17..00000000 --- a/x/committee/types/proposal.go +++ /dev/null @@ -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 -} diff --git a/x/committee/types/types.go b/x/committee/types/types.go index fb4c0d42..8ef97602 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -44,6 +44,20 @@ func (c Committee) HasPermissionsFor(proposal PubProposal) bool { 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. type Permission interface { Allows(PubProposal) bool From 66d368c722c96c15f94dde5f31be1c3d1378737e Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 21 Mar 2020 19:48:01 +0000 Subject: [PATCH 21/54] add genesis tests --- x/committee/proposal_handler.go | 4 +- x/committee/types/genesis.go | 9 +-- x/committee/types/genesis_test.go | 130 +++++++++++++++++++++++++++++- x/committee/types/gov_proposal.go | 2 +- x/committee/types/types.go | 14 +++- 5 files changed, 144 insertions(+), 15 deletions(-) diff --git a/x/committee/proposal_handler.go b/x/committee/proposal_handler.go index 45cf0554..e9d1eef8 100644 --- a/x/committee/proposal_handler.go +++ b/x/committee/proposal_handler.go @@ -24,7 +24,7 @@ func NewProposalHandler(k Keeper) govtypes.Handler { func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeChangeProposal) sdk.Error { if err := committeeProposal.ValidateBasic(); err != nil { - return err + return sdk.ErrInternal(err.Error()) } // Remove all committee's ongoing proposals @@ -46,7 +46,7 @@ func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeDeleteProposal) sdk.Error { if err := committeeProposal.ValidateBasic(); err != nil { - return err + return sdk.ErrInternal(err.Error()) } // Remove all committee's ongoing proposals diff --git a/x/committee/types/genesis.go b/x/committee/types/genesis.go index a259f228..fe9a85c0 100644 --- a/x/committee/types/genesis.go +++ b/x/committee/types/genesis.go @@ -60,13 +60,8 @@ func (gs GenesisState) Validate() error { committeeMap[com.ID] = true // validate committee - if len(com.Members) == 0 { - return fmt.Errorf("committee %d invalid: cannot have zero members", com.ID) - } - for _, m := range com.Members { - if m.Empty() { - return fmt.Errorf("committee %d invalid: found empty member address", com.ID) - } + if err := com.Validate(); err != nil { + return err } } diff --git a/x/committee/types/genesis_test.go b/x/committee/types/genesis_test.go index 3a9835ee..9b76d4f2 100644 --- a/x/committee/types/genesis_test.go +++ b/x/committee/types/genesis_test.go @@ -2,22 +2,148 @@ package types import ( "testing" + "time" "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/tendermint/tendermint/crypto" ) func TestGenesisState_Validate(t *testing.T) { + testTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC) + addresses := []sdk.AccAddress{ + sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))), + sdk.AccAddress(crypto.AddressHash([]byte("KavaTest2"))), + sdk.AccAddress(crypto.AddressHash([]byte("KavaTest3"))), + sdk.AccAddress(crypto.AddressHash([]byte("KavaTest4"))), + sdk.AccAddress(crypto.AddressHash([]byte("KavaTest5"))), + } + testGenesis := GenesisState{ + NextProposalID: 2, + Committees: []Committee{ + { + ID: 1, + Members: addresses[:3], + Permissions: []Permission{GodPermission{}}, + }, + { + ID: 2, + Members: addresses[2:], + Permissions: nil, + }, + }, + Proposals: []Proposal{ + {ID: 1, CommitteeID: 1, PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)}, + }, + Votes: []Vote{ + {ProposalID: 1, Voter: addresses[0]}, + {ProposalID: 1, Voter: addresses[1]}, + }, + } + testCases := []struct { name string genState GenesisState expectPass bool }{ { - name: "normal", + name: "default", genState: DefaultGenesisState(), expectPass: true, }, - // TODO test failure cases + { + name: "normal", + genState: testGenesis, + expectPass: true, + }, + { + name: "duplicate committee IDs", + genState: GenesisState{ + NextProposalID: testGenesis.NextProposalID, + Committees: append(testGenesis.Committees, testGenesis.Committees[0]), + Proposals: testGenesis.Proposals, + Votes: testGenesis.Votes, + }, + expectPass: false, + }, + { + name: "invalid committee", + genState: GenesisState{ + NextProposalID: testGenesis.NextProposalID, + Committees: append(testGenesis.Committees, Committee{}), + Proposals: testGenesis.Proposals, + Votes: testGenesis.Votes, + }, + expectPass: false, + }, + { + name: "duplicate proposal IDs", + genState: GenesisState{ + NextProposalID: testGenesis.NextProposalID, + Committees: testGenesis.Committees, + Proposals: append(testGenesis.Proposals, testGenesis.Proposals[0]), + Votes: testGenesis.Votes, + }, + expectPass: false, + }, + { + name: "invalid NextProposalID", + genState: GenesisState{ + NextProposalID: 0, + Committees: testGenesis.Committees, + Proposals: testGenesis.Proposals, + Votes: testGenesis.Votes, + }, + expectPass: false, + }, + { + name: "proposal without committee", + genState: GenesisState{ + NextProposalID: testGenesis.NextProposalID + 1, + Committees: testGenesis.Committees, + Proposals: append( + testGenesis.Proposals, + Proposal{ + ID: testGenesis.NextProposalID, + PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + CommitteeID: 247, // doesn't exist + }), + Votes: testGenesis.Votes, + }, + expectPass: false, + }, + { + name: "invalid proposal", + genState: GenesisState{ + NextProposalID: testGenesis.NextProposalID, + Committees: testGenesis.Committees, + Proposals: append(testGenesis.Proposals, Proposal{}), + Votes: testGenesis.Votes, + }, + expectPass: false, + }, + { + name: "vote without proposal", + genState: GenesisState{ + NextProposalID: testGenesis.NextProposalID, + Committees: testGenesis.Committees, + Proposals: nil, + Votes: testGenesis.Votes, + }, + expectPass: false, + }, + { + name: "invalid vote", + genState: GenesisState{ + NextProposalID: testGenesis.NextProposalID, + Committees: testGenesis.Committees, + Proposals: testGenesis.Proposals, + Votes: append(testGenesis.Votes, Vote{}), + }, + expectPass: false, + }, } for _, tc := range testCases { diff --git a/x/committee/types/gov_proposal.go b/x/committee/types/gov_proposal.go index 6b2a94b6..c444d16a 100644 --- a/x/committee/types/gov_proposal.go +++ b/x/committee/types/gov_proposal.go @@ -56,7 +56,7 @@ func (ccp CommitteeChangeProposal) ValidateBasic() sdk.Error { return err } if err := ccp.NewCommittee.Validate(); err != nil { - return err + return sdk.ErrInternal(err.Error()) } return nil } diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 8ef97602..0d91d979 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -44,17 +44,25 @@ func (c Committee) HasPermissionsFor(proposal PubProposal) bool { return false } -func (c Committee) Validate() sdk.Error { - // check for duplicate addresses +func (c Committee) Validate() error { + 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)) + return fmt.Errorf("duplicate member found in committee, %s", m) + } + // check for valid addresses + if m.Empty() { + return fmt.Errorf("committee %d invalid: found empty member address", c.ID) } addressMap[m.String()] = true } + + if len(c.Members) == 0 { + return fmt.Errorf("committee %d invalid: cannot have zero members", c.ID) + } return nil } From 0275b21173f159ab9485368827fb5c123704c4e2 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 21 Mar 2020 20:21:43 +0000 Subject: [PATCH 22/54] add struct tags everywhere --- x/committee/types/codec.go | 3 +++ x/committee/types/genesis.go | 8 ++++---- x/committee/types/gov_proposal.go | 12 ++++++------ x/committee/types/msg.go | 2 +- x/committee/types/permissions.go | 2 +- x/committee/types/querier.go | 8 ++++---- x/committee/types/types.go | 18 +++++++++--------- 7 files changed, 28 insertions(+), 25 deletions(-) diff --git a/x/committee/types/codec.go b/x/committee/types/codec.go index 57d700b0..294068df 100644 --- a/x/committee/types/codec.go +++ b/x/committee/types/codec.go @@ -37,4 +37,7 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterInterface((*Permission)(nil), nil) cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil) + + cdc.RegisterConcrete(MsgSubmitProposal{}, "kava/MsgSubmitProposal", nil) + cdc.RegisterConcrete(MsgVote{}, "kava/MsgVote", nil) } diff --git a/x/committee/types/genesis.go b/x/committee/types/genesis.go index fe9a85c0..fda51064 100644 --- a/x/committee/types/genesis.go +++ b/x/committee/types/genesis.go @@ -10,10 +10,10 @@ const DefaultNextProposalID uint64 = 1 // GenesisState is state that must be provided at chain genesis. type GenesisState struct { - NextProposalID uint64 - Committees []Committee - Proposals []Proposal - Votes []Vote + NextProposalID uint64 `json:"next_proposal_id" yaml:"next_proposal_id"` + Committees []Committee `json:"committees" yaml:"committees"` + Proposals []Proposal `json:"proposals" yaml:"proposals"` + Votes []Vote `json:"votes" yaml:"votes"` } // NewGenesisState returns a new genesis state object for the module. diff --git a/x/committee/types/gov_proposal.go b/x/committee/types/gov_proposal.go index c444d16a..ed7dfe86 100644 --- a/x/committee/types/gov_proposal.go +++ b/x/committee/types/gov_proposal.go @@ -14,9 +14,9 @@ const ( // CommitteeChangeProposal is a gov proposal for creating a new committee or modifying an existing one. type CommitteeChangeProposal struct { - Title string - Description string - NewCommittee Committee + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + NewCommittee Committee `json:"new_committee" yaml:"new_committee"` } var _ govtypes.Content = CommitteeChangeProposal{} @@ -69,9 +69,9 @@ func (ccp CommitteeChangeProposal) String() string { // CommitteeDeleteProposal is a gov proposal for removing a committee. type CommitteeDeleteProposal struct { - Title string - Description string - CommitteeID uint64 + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + CommitteeID uint64 `json:"committee_id" yaml:"committee_id"` } var _ govtypes.Content = CommitteeDeleteProposal{} diff --git a/x/committee/types/msg.go b/x/committee/types/msg.go index dd34e727..273a4654 100644 --- a/x/committee/types/msg.go +++ b/x/committee/types/msg.go @@ -15,7 +15,7 @@ var _, _ sdk.Msg = MsgSubmitProposal{}, MsgVote{} type MsgSubmitProposal struct { PubProposal PubProposal `json:"pub_proposal" yaml:"pub_proposal"` Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` - CommitteeID uint64 + CommitteeID uint64 `json:"committee_id" yaml:"committee_id"` } // NewMsgSubmitProposal creates a new MsgSubmitProposal instance diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index 068fadc3..807fc1f4 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -50,7 +50,7 @@ func (ShutdownCDPDepsitPermission) Allows(p gov.Content) bool { // Same as above but the route isn't static type GeneralShutdownPermission struct { - MsgRoute sdtypes.MsgRoute + MsgRoute sdtypes.MsgRoute `json:"msg_route" yaml:"msg_route"` } var _ Permission = GeneralShutdownPermission{} diff --git a/x/committee/types/querier.go b/x/committee/types/querier.go index 5f8a4988..f58ed338 100644 --- a/x/committee/types/querier.go +++ b/x/committee/types/querier.go @@ -17,7 +17,7 @@ const ( ) type QueryCommitteeParams struct { - CommitteeID uint64 + CommitteeID uint64 `json:"committee_id" yaml:"committee_id"` } func NewQueryCommitteeParams(committeeID uint64) QueryCommitteeParams { @@ -27,7 +27,7 @@ func NewQueryCommitteeParams(committeeID uint64) QueryCommitteeParams { } type QueryProposalParams struct { - ProposalID uint64 + ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` } func NewQueryProposalParams(proposalID uint64) QueryProposalParams { @@ -37,8 +37,8 @@ func NewQueryProposalParams(proposalID uint64) QueryProposalParams { } type QueryVoteParams struct { - ProposalID uint64 - Voter sdk.AccAddress + ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` + Voter sdk.AccAddress `json:"voter" yaml:"voter"` } func NewQueryVoteParams(proposalID uint64, voter sdk.AccAddress) QueryVoteParams { diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 0d91d979..391d3b69 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -19,9 +19,9 @@ var ( // A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. type Committee struct { - ID uint64 // TODO or a name? - Members []sdk.AccAddress - Permissions []Permission + ID uint64 `json:"id" yaml:"id"` // TODO or a name? + Members []sdk.AccAddress `json:"members" yaml:"members"` + Permissions []Permission `json:"permissions" yaml:"permissions"` } func (c Committee) HasMember(addr sdk.AccAddress) bool { @@ -77,10 +77,10 @@ type Permission interface { type PubProposal = gov.Content // TODO find a better name type Proposal struct { - PubProposal - ID uint64 - CommitteeID uint64 - Deadline time.Time + PubProposal `json:"pub_proposal" yaml:"pub_proposal"` + ID uint64 `json:"id" yaml:"id"` + CommitteeID uint64 `json:"committee_id" yaml:"committee_id"` + Deadline time.Time `json:"deadline" yaml:"deadline"` } // HasExpiredBy calculates if the proposal will have expired by a certain time. @@ -105,7 +105,7 @@ func (p Proposal) String() string { } type Vote struct { - ProposalID uint64 - Voter sdk.AccAddress + ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` + Voter sdk.AccAddress `json:"voter" yaml:"voter"` // Option byte // TODO for now don't need more than just a yes as options } From b31cfbe39b71eb842df40a14b9195c57fb670e0e Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 23 Mar 2020 14:32:50 +0000 Subject: [PATCH 23/54] add gov client handlers --- app/app.go | 25 +++++---- x/committee/alias.go | 2 + x/committee/client/cli/query.go | 4 +- x/committee/client/cli/tx.go | 51 ++++++++++++++++++- .../client/{ => common}/query_proposer.go | 2 +- x/committee/client/proposal_handler.go | 11 ++++ x/committee/client/rest/query.go | 4 +- x/committee/client/rest/tx.go | 45 ++++++++++++++++ x/committee/keeper/keeper.go | 10 +--- x/committee/proposal_handler_test.go | 3 +- 10 files changed, 132 insertions(+), 25 deletions(-) rename x/committee/client/{ => common}/query_proposer.go (99%) create mode 100644 x/committee/client/proposal_handler.go diff --git a/app/app.go b/app/app.go index bfc2d75e..c0ff9680 100644 --- a/app/app.go +++ b/app/app.go @@ -55,7 +55,7 @@ var ( staking.AppModuleBasic{}, mint.AppModuleBasic{}, distr.AppModuleBasic{}, - gov.NewAppModuleBasic(paramsclient.ProposalHandler, distr.ProposalHandler), + gov.NewAppModuleBasic(paramsclient.ProposalHandler, distr.ProposalHandler, committee.ProposalHandler), params.AppModuleBasic{}, crisis.AppModuleBasic{}, slashing.AppModuleBasic{}, @@ -207,11 +207,23 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, invCheckPeriod, app.supplyKeeper, auth.FeeCollectorName) + committeeGovRouter := gov.NewRouter() + committeeGovRouter. + AddRoute(gov.RouterKey, gov.ProposalHandler). + AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). + AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)) + // Note: the committee proposal handler is not registered on the committee router. This means committees cannot create or update other committees. + // Adding the committee proposal handler to the router is possible but awkward as the handler depends on the keeper which depends on the handler. + app.committeeKeeper = committee.NewKeeper( + app.cdc, + keys[committee.StoreKey], + committeeGovRouter) // TODO blacklist module addresses?) govRouter := gov.NewRouter() govRouter. AddRoute(gov.RouterKey, gov.ProposalHandler). AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). - AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)) + AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)). + AddRoute(committee.RouterKey, committee.NewProposalHandler(app.committeeKeeper)) app.govKeeper = gov.NewKeeper( app.cdc, keys[gov.StoreKey], @@ -246,12 +258,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.auctionKeeper, app.supplyKeeper, cdp.DefaultCodespace) - app.committeeKeeper = committee.NewKeeper( - app.cdc, - keys[committee.StoreKey], - govRouter, - // TODO blacklist module addresses? - ) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -330,7 +336,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // initialize the app app.SetInitChainer(app.InitChainer) app.SetBeginBlocker(app.BeginBlocker) - // app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.supplyKeeper, app.shutdownKeeper, auth.DefaultSigVerificationGasConsumer)) + // TODO app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.supplyKeeper, app.shutdownKeeper, auth.DefaultSigVerificationGasConsumer)) + app.SetAnteHandler(auth.NewAnteHandler(app.accountKeeper, app.supplyKeeper, auth.DefaultSigVerificationGasConsumer)) app.SetEndBlocker(app.EndBlocker) // load store diff --git a/x/committee/alias.go b/x/committee/alias.go index b3e3cc2a..c33b9b01 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -3,6 +3,7 @@ package committee import ( + "github.com/kava-labs/kava/x/committee/client" "github.com/kava-labs/kava/x/committee/keeper" "github.com/kava-labs/kava/x/committee/types" ) @@ -49,6 +50,7 @@ var ( Uint64FromBytes = types.Uint64FromBytes // variable aliases + ProposalHandler = client.ProposalHandler CommitteeKeyPrefix = types.CommitteeKeyPrefix MaxProposalDuration = types.MaxProposalDuration ModuleCdc = types.ModuleCdc diff --git a/x/committee/client/cli/query.go b/x/committee/client/cli/query.go index 23deeb76..4dfeea25 100644 --- a/x/committee/client/cli/query.go +++ b/x/committee/client/cli/query.go @@ -10,7 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/codec" - comclient "github.com/kava-labs/kava/x/committee/client" + "github.com/kava-labs/kava/x/committee/client/common" "github.com/kava-labs/kava/x/committee/types" ) @@ -381,7 +381,7 @@ func GetCmdQueryProposer(queryRoute string, cdc *codec.Codec) *cobra.Command { return fmt.Errorf("proposal-id %s is not a valid uint", args[0]) } - prop, err := comclient.QueryProposer(cliCtx, proposalID) + prop, err := common.QueryProposer(cliCtx, proposalID) if err != nil { return err } diff --git a/x/committee/client/cli/tx.go b/x/committee/client/cli/tx.go index 46674426..321c4e7e 100644 --- a/x/committee/client/cli/tx.go +++ b/x/committee/client/cli/tx.go @@ -13,6 +13,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/kava-labs/kava/x/committee/types" ) @@ -139,7 +140,7 @@ func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { func GetCmdVote(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "vote [proposal-id]", - Args: cobra.ExactArgs(2), + Args: cobra.ExactArgs(1), Short: "Vote for an active proposal", // TODO // Long: strings.TrimSpace( // fmt.Sprintf(`Submit a vote for an active proposal. You can @@ -175,3 +176,51 @@ func GetCmdVote(cdc *codec.Codec) *cobra.Command { }, } } + +// TODO this could replace the whole gov submit-proposal cmd, remove and replace the gov cmd in kvcli main.go +// would want the documentation/examples though +func GetGovCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "committee [proposal-file] [deposit]", + Short: "Submit a governance proposal to change a committee.", + Long: "This command will work with either CommitteeChange proposals or CommitteeDelete proposals.", // TODO + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Get proposing address + proposer := cliCtx.GetFromAddress() + + // Get the deposit + deposit, err := sdk.ParseCoins(args[0]) + if err != nil { + return err + } + + // Get the proposal + bz, err := ioutil.ReadFile(args[0]) + if err != nil { + return err + } + var content govtypes.Content + if err := cdc.UnmarshalJSON(bz, &content); err != nil { + return err + } + if err = content.ValidateBasic(); err != nil { + return err + } + + // Build message and run basic validation + msg := govtypes.NewMsgSubmitProposal(content, deposit, proposer) + err = msg.ValidateBasic() + if err != nil { + return err + } + + // Sign and broadcast message + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} diff --git a/x/committee/client/query_proposer.go b/x/committee/client/common/query_proposer.go similarity index 99% rename from x/committee/client/query_proposer.go rename to x/committee/client/common/query_proposer.go index 29e61552..0508f53d 100644 --- a/x/committee/client/query_proposer.go +++ b/x/committee/client/common/query_proposer.go @@ -1,4 +1,4 @@ -package client +package common import ( "fmt" diff --git a/x/committee/client/proposal_handler.go b/x/committee/client/proposal_handler.go new file mode 100644 index 00000000..0bbd31ea --- /dev/null +++ b/x/committee/client/proposal_handler.go @@ -0,0 +1,11 @@ +package client + +import ( + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + + "github.com/kava-labs/kava/x/committee/client/cli" + "github.com/kava-labs/kava/x/committee/client/rest" +) + +// ProposalHandler is a struct containing handler funcs for submiting CommitteeChange/Delete proposal txs to the gov module through the cli or rest. +var ProposalHandler = govclient.NewProposalHandler(cli.GetGovCmdSubmitProposal, rest.ProposalRESTHandler) diff --git a/x/committee/client/rest/query.go b/x/committee/client/rest/query.go index e7c2be72..4ec04e7d 100644 --- a/x/committee/client/rest/query.go +++ b/x/committee/client/rest/query.go @@ -10,7 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/types/rest" - "github.com/kava-labs/kava/x/committee/client" + "github.com/kava-labs/kava/x/committee/client/common" "github.com/kava-labs/kava/x/committee/types" ) @@ -181,7 +181,7 @@ func queryProposerHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } // Query - res, err := client.QueryProposer(cliCtx, proposalID) + res, err := common.QueryProposer(cliCtx, proposalID) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return diff --git a/x/committee/client/rest/tx.go b/x/committee/client/rest/tx.go index 20be14e6..983e3a55 100644 --- a/x/committee/client/rest/tx.go +++ b/x/committee/client/rest/tx.go @@ -10,6 +10,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/rest" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/kava-labs/kava/x/committee/types" ) @@ -108,3 +110,46 @@ func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) } } + +// -------- -------- +// TODO this could replace the POST gov/proposals endpoint, would need to overwrite routes in kvcli main, hacky +type PostGovProposalReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Content govtypes.Content `json:"content" yaml:"content"` //TODO use same PubProposal name? + Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` +} + +func ProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "committee", + Handler: postGovProposalHandlerFn(cliCtx), + } +} + +func postGovProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + // Parse and validate http request body + var req PostGovProposalReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + if err := req.Content.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Create and return a StdTx + msg := govtypes.NewMsgSubmitProposal(req.Content, req.Deposit, req.Proposer) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 3b01c33e..c1471fbb 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -22,8 +22,7 @@ type Keeper struct { func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) Keeper { // Logic in the keeper methods assume the set of gov handlers is fixed. // So the gov router must be sealed so no handlers can be added or removed after the keeper is created. - // Note: for some reason the gov router panics if it has already been sealed, so a helper func is used to make sealing idempotent. - sealGovRouterIdempotently(router) + router.Seal() return Keeper{ cdc: cdc, @@ -32,13 +31,6 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) } } -func sealGovRouterIdempotently(router govtypes.Router) { - defer func() { - recover() - }() - router.Seal() -} - // ---------- Committees ---------- // GetCommittee gets a committee from the store. diff --git a/x/committee/proposal_handler_test.go b/x/committee/proposal_handler_test.go index 01a8ec75..c5150462 100644 --- a/x/committee/proposal_handler_test.go +++ b/x/committee/proposal_handler_test.go @@ -69,7 +69,8 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { "A Title", "A proposal description.", committee.Committee{ - ID: 34, + ID: 34, + Members: suite.addresses[:1], }, ), expectPass: true, From eefda597f03650578df95aa5eb27525007e6a6d5 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 23 Mar 2020 20:57:15 +0000 Subject: [PATCH 24/54] tidy up and test client --- x/committee/client/cli/query.go | 224 +++++------------------------- x/committee/client/cli/tx.go | 159 ++++++++++----------- x/committee/client/rest/query.go | 92 +----------- x/committee/client/rest/rest.go | 13 +- x/committee/client/rest/tx.go | 7 +- x/committee/types/gov_proposal.go | 6 + x/committee/types/types.go | 8 ++ 7 files changed, 139 insertions(+), 370 deletions(-) diff --git a/x/committee/client/cli/query.go b/x/committee/client/cli/query.go index 4dfeea25..abcf6865 100644 --- a/x/committee/client/cli/query.go +++ b/x/committee/client/cli/query.go @@ -9,6 +9,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/version" "github.com/kava-labs/kava/x/committee/client/common" "github.com/kava-labs/kava/x/committee/types" @@ -26,13 +27,14 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { } govQueryCmd.AddCommand(client.GetCommands( - //GetCmdQueryCommittee(queryRoute, cdc), + // GetCmdQueryCommittee(queryRoute, cdc), // TODO is this needed? GetCmdQueryCommittees(queryRoute, cdc), + GetCmdQueryProposal(queryRoute, cdc), GetCmdQueryProposals(queryRoute, cdc), - //GetCmdQueryVote(queryRoute, cdc), + GetCmdQueryVotes(queryRoute, cdc), - //GetCmdQueryParams(queryRoute, cdc), + //TODO GetCmdQueryParams(queryRoute, cdc), GetCmdQueryProposer(queryRoute, cdc), GetCmdQueryTally(queryRoute, cdc))...) @@ -42,9 +44,10 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // GetCmdQueryProposals implements a query proposals command. func GetCmdQueryCommittees(queryRoute string, cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ - Use: "committees", - Short: "Query all committees", - Long: "", // TODO + Use: "committees", + Args: cobra.NoArgs, + Short: "Query all committees", + Example: fmt.Sprintf("%s query %s committees", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -55,7 +58,7 @@ func GetCmdQueryCommittees(queryRoute string, cdc *codec.Codec) *cobra.Command { } // Decode and print result - committees := []types.Committee{} + committees := []types.Committee{} // using empty (not nil) slice so json output returns "[]"" instead of "null" when there's no data if err = cdc.UnmarshalJSON(res, &committees); err != nil { return err } @@ -68,19 +71,10 @@ func GetCmdQueryCommittees(queryRoute string, cdc *codec.Codec) *cobra.Command { // GetCmdQueryProposal implements the query proposal command. func GetCmdQueryProposal(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "proposal [proposal-id]", - Args: cobra.ExactArgs(1), - Short: "Query details of a single proposal", - // Long: strings.TrimSpace( - // fmt.Sprintf(`Query details for a proposal. You can find the - // proposal-id by running "%s query gov proposals". - - // Example: - // $ %s query gov proposal 1 - // `, - // version.ClientName, version.ClientName, - // ), - // ), + Use: "proposal [proposal-id]", + Args: cobra.ExactArgs(1), + Short: "Query details of a single proposal", + Example: fmt.Sprintf("%s query %s proposal 2", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -95,7 +89,6 @@ func GetCmdQueryProposal(queryRoute string, cdc *codec.Codec) *cobra.Command { } // Query - //res, err := gcutils.QueryProposalByID(proposalID, cliCtx, queryRoute) res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryProposal), bz) if err != nil { return err @@ -112,20 +105,10 @@ func GetCmdQueryProposal(queryRoute string, cdc *codec.Codec) *cobra.Command { // GetCmdQueryProposals implements a query proposals command. func GetCmdQueryProposals(queryRoute string, cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ - Use: "proposals [committee-id]", - Short: "Query proposals by committee.", - Args: cobra.ExactArgs(1), - // Long: strings.TrimSpace( - // fmt.Sprintf(`Query for a all proposals. You can filter the returns with the following flags. - - // Example: - // $ %s query gov proposals --depositor cosmos1skjwj5whet0lpe65qaq4rpq03hjxlwd9nf39lk - // $ %s query gov proposals --voter cosmos1skjwj5whet0lpe65qaq4rpq03hjxlwd9nf39lk - // $ %s query gov proposals --status (DepositPeriod|VotingPeriod|Passed|Rejected) - // `, - // version.ClientName, version.ClientName, version.ClientName, - // ), - // ), + Use: "proposals [committee-id]", + Short: "Query all proposals for a committee", + Args: cobra.ExactArgs(1), + Example: fmt.Sprintf("%s query %s proposals 1", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -146,7 +129,7 @@ func GetCmdQueryProposals(queryRoute string, cdc *codec.Codec) *cobra.Command { } // Decode and print results - proposals := []types.Proposal{} // using empty (not nil) slice so json returns [] instead of null when there's no data // TODO check + proposals := []types.Proposal{} err = cdc.UnmarshalJSON(res, &proposals) if err != nil { return err @@ -157,91 +140,13 @@ func GetCmdQueryProposals(queryRoute string, cdc *codec.Codec) *cobra.Command { return cmd } -// // Command to Get a Proposal Information -// // GetCmdQueryVote implements the query proposal vote command. -// func GetCmdQueryVote(queryRoute string, cdc *codec.Codec) *cobra.Command { -// return &cobra.Command{ -// Use: "vote [proposal-id] [voter-addr]", -// Args: cobra.ExactArgs(2), -// Short: "Query details of a single vote", -// Long: strings.TrimSpace( -// fmt.Sprintf(`Query details for a single vote on a proposal given its identifier. - -// Example: -// $ %s query gov vote 1 cosmos1skjwj5whet0lpe65qaq4rpq03hjxlwd9nf39lk -// `, -// version.ClientName, -// ), -// ), -// RunE: func(cmd *cobra.Command, args []string) error { -// cliCtx := context.NewCLIContext().WithCodec(cdc) - -// // validate that the proposal id is a uint -// proposalID, err := strconv.ParseUint(args[0], 10, 64) -// if err != nil { -// return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0]) -// } - -// // check to see if the proposal is in the store -// _, err = gcutils.QueryProposalByID(proposalID, cliCtx, queryRoute) -// if err != nil { -// return fmt.Errorf("failed to fetch proposal-id %d: %s", proposalID, err) -// } - -// voterAddr, err := sdk.AccAddressFromBech32(args[1]) -// if err != nil { -// return err -// } - -// params := types.NewQueryVoteParams(proposalID, voterAddr) -// bz, err := cdc.MarshalJSON(params) -// if err != nil { -// return err -// } - -// res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/vote", queryRoute), bz) -// if err != nil { -// return err -// } - -// var vote types.Vote - -// // XXX: Allow the decoding to potentially fail as the vote may have been -// // pruned from state. If so, decoding will fail and so we need to check the -// // Empty() case. Consider updating Vote JSON decoding to not fail when empty. -// _ = cdc.UnmarshalJSON(res, &vote) - -// if vote.Empty() { -// res, err = gcutils.QueryVoteByTxQuery(cliCtx, params) -// if err != nil { -// return err -// } - -// if err := cdc.UnmarshalJSON(res, &vote); err != nil { -// return err -// } -// } - -// return cliCtx.PrintOutput(vote) -// }, -// } -// } - // GetCmdQueryVotes implements the command to query for proposal votes. func GetCmdQueryVotes(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "votes [proposal-id]", - Args: cobra.ExactArgs(1), - Short: "Query votes on a proposal", - // Long: strings.TrimSpace( - // fmt.Sprintf(`Query vote details for a single proposal by its identifier. - - // Example: - // $ %s query gov votes 1 - // `, - // version.ClientName, - // ), - // ), + Use: "votes [proposal-id]", + Args: cobra.ExactArgs(1), + Short: "Query votes on a proposal", + Example: fmt.Sprintf("%s query %s votes 2", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -272,22 +177,13 @@ func GetCmdQueryVotes(queryRoute string, cdc *codec.Codec) *cobra.Command { } } -// GetCmdQueryTally implements the command to query for proposal tally result. func GetCmdQueryTally(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "tally [proposal-id]", - Args: cobra.ExactArgs(1), - Short: "Get the tally of a proposal vote", - // Long: strings.TrimSpace( - // fmt.Sprintf(`Query tally of votes on a proposal. You can find - // the proposal-id by running "%s query gov proposals". - - // Example: - // $ %s query gov tally 1 - // `, - // version.ClientName, version.ClientName, - // ), - // ), + Use: "tally [proposal-id]", + Args: cobra.ExactArgs(1), + Short: "Get the current tally of votes on a proposal", + Long: "Query the current tally of votes on a proposal to see the progress of the voting.", + Example: fmt.Sprintf("%s query %s tally 2", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -309,69 +205,21 @@ func GetCmdQueryTally(queryRoute string, cdc *codec.Codec) *cobra.Command { // Decode and print results var tally bool - cdc.MustUnmarshalJSON(res, &tally) // TODO must or normal, what's the difference on the cli? + if err = cdc.UnmarshalJSON(res, &tally); err != nil { + return err + } return cliCtx.PrintOutput(tally) }, } } -// // GetCmdQueryProposal implements the query proposal command. -// func GetCmdQueryParams(queryRoute string, cdc *codec.Codec) *cobra.Command { -// return &cobra.Command{ -// Use: "params", -// Short: "Query the parameters of the governance process", -// Long: strings.TrimSpace( -// fmt.Sprintf(`Query the all the parameters for the governance process. - -// Example: -// $ %s query gov params -// `, -// version.ClientName, -// ), -// ), -// Args: cobra.NoArgs, -// RunE: func(cmd *cobra.Command, args []string) error { -// cliCtx := context.NewCLIContext().WithCodec(cdc) -// tp, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/params/tallying", queryRoute), nil) -// if err != nil { -// return err -// } -// dp, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/params/deposit", queryRoute), nil) -// if err != nil { -// return err -// } -// vp, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/params/voting", queryRoute), nil) -// if err != nil { -// return err -// } - -// var tallyParams types.TallyParams -// cdc.MustUnmarshalJSON(tp, &tallyParams) -// var depositParams types.DepositParams -// cdc.MustUnmarshalJSON(dp, &depositParams) -// var votingParams types.VotingParams -// cdc.MustUnmarshalJSON(vp, &votingParams) - -// return cliCtx.PrintOutput(types.NewParams(votingParams, tallyParams, depositParams)) -// }, -// } -// } - -// GetCmdQueryProposer implements the query proposer command. func GetCmdQueryProposer(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "proposer [proposal-id]", - Args: cobra.ExactArgs(1), - Short: "Query the proposer of a governance proposal", - // Long: strings.TrimSpace( - // fmt.Sprintf(`Query which address proposed a proposal with a given ID. - - // Example: - // $ %s query gov proposer 1 - // `, - // version.ClientName, - // ), - // ), + Use: "proposer [proposal-id]", + Args: cobra.ExactArgs(1), + Short: "Query the proposer of a governance proposal", + Long: "Query which address proposed a proposal with a given ID.", + Example: fmt.Sprintf("%s query %s proposer 2", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) diff --git a/x/committee/client/cli/tx.go b/x/committee/client/cli/tx.go index 321c4e7e..ce6bed5b 100644 --- a/x/committee/client/cli/tx.go +++ b/x/committee/client/cli/tx.go @@ -11,49 +11,17 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/tendermint/tendermint/crypto" "github.com/kava-labs/kava/x/committee/types" ) -// // Proposal flags -// const ( -// FlagTitle = "title" -// FlagDescription = "description" -// flagProposalType = "type" -// FlagDeposit = "deposit" -// flagVoter = "voter" -// flagDepositor = "depositor" -// flagStatus = "status" -// flagNumLimit = "limit" -// FlagProposal = "proposal" -// ) - -// type proposal struct { -// Title string -// Description string -// Type string -// Deposit string -// } - -// // ProposalFlags defines the core required fields of a proposal. It is used to -// // verify that these values are not provided in conjunction with a JSON proposal -// // file. -// var ProposalFlags = []string{ -// FlagTitle, -// FlagDescription, -// flagProposalType, -// FlagDeposit, -// } - -// GetTxCmd returns the transaction commands for this module -// governance ModuleClient is slightly different from other ModuleClients in that -// it contains a slice of "proposal" child commands. These commands are respective -// to proposal type handlers that are implemented in other modules but are mounted -// under the governance CLI (eg. parameter change proposals). -func GetTxCmd(storeKey string, cdc *codec.Codec /*, pcmds []*cobra.Command*/) *cobra.Command { // TODO why is storeKey here? +func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command { txCmd := &cobra.Command{ Use: types.ModuleName, Short: "committee governance transactions subcommands", @@ -62,39 +30,27 @@ func GetTxCmd(storeKey string, cdc *codec.Codec /*, pcmds []*cobra.Command*/) *c RunE: client.ValidateCmd, } - cmdSubmitProp := GetCmdSubmitProposal(cdc) - // for _, pcmd := range pcmds { - // cmdSubmitProp.AddCommand(client.PostCommands(pcmd)[0]) - // } - txCmd.AddCommand(client.PostCommands( GetCmdVote(cdc), - cmdSubmitProp, + GetCmdSubmitProposal(cdc), )...) return txCmd } -// // GetCmdSubmitProposal is the root command on which commands for submitting proposals are registered. -// func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { -// cmd := &cobra.Command{ -// Use: "submit-proposal [committee-id]", -// Short: "Submit a governance proposal to a particular committee.", // TODO -// DisableFlagParsing: true, -// SuggestionsMinimumDistance: 2, -// RunE: client.ValidateCmd, -// } - -// return cmd -// } - -// GetCmdSubmitProposal +// GetCmdSubmitProposal returns the command to submit a proposal to a committee func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "submit-proposal [committee-id] [proposal-file]", - Short: "Submit a governance proposal to a particular committee.", - Long: "", // TODO - Args: cobra.ExactArgs(2), + Short: "Submit a governance proposal to a particular committee", + Long: fmt.Sprintf(`Submit a proposal to a committee so they can vote on it. + +The proposal file must be the json encoded forms of the proposal type you want to submit. +For example: +%s +`, mustGetExampleParameterChangeProposal(cdc)), + Args: cobra.ExactArgs(2), + Example: fmt.Sprintf("%s tx %s submit-proposal 1 your-proposal.json", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -136,22 +92,14 @@ func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { return cmd } -// GetCmdVote implements creating a new vote command. +// GetCmdVote returns the command to vote on a proposal. func GetCmdVote(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "vote [proposal-id]", - Args: cobra.ExactArgs(1), - Short: "Vote for an active proposal", // TODO - // Long: strings.TrimSpace( - // fmt.Sprintf(`Submit a vote for an active proposal. You can - // find the proposal-id by running "%s query gov proposals". - - // Example: - // $ %s tx gov vote 1 yes --from mykey - // `, - // version.ClientName, version.ClientName, - // ), - // ), + Use: "vote [proposal-id]", + Args: cobra.ExactArgs(1), + Short: "Vote for an active proposal", + Long: "Submit a yes vote for the proposal with id [proposal-id].", + Example: fmt.Sprintf("%s tx %s vote 2", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -177,14 +125,23 @@ func GetCmdVote(cdc *codec.Codec) *cobra.Command { } } -// TODO this could replace the whole gov submit-proposal cmd, remove and replace the gov cmd in kvcli main.go -// would want the documentation/examples though +// TODO This could replace the whole gov submit-proposal cmd. It would align how it works with how submiting proposal to committees works. +// Requires removing and replacing the gov cmd in kvcli main.go +// GetGovCmdSubmitProposal returns a command to submit a proposal to the gov module. It is passed to the gov module for use on its command subtree. func GetGovCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "committee [proposal-file] [deposit]", Short: "Submit a governance proposal to change a committee.", - Long: "This command will work with either CommitteeChange proposals or CommitteeDelete proposals.", // TODO - Args: cobra.ExactArgs(2), + Long: fmt.Sprintf(`Submit a governance proposal to create, alter, or delete a committee. + +The proposal file must be the json encoded form of the proposal type you want to submit. +For example, to create or update a committee: +%s + +and to delete a committee: +%s +`, mustGetExampleCommitteeChangeProposal(cdc), mustGetExampleCommitteeDeleteProposal(cdc)), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -193,7 +150,7 @@ func GetGovCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { proposer := cliCtx.GetFromAddress() // Get the deposit - deposit, err := sdk.ParseCoins(args[0]) + deposit, err := sdk.ParseCoins(args[1]) if err != nil { return err } @@ -224,3 +181,49 @@ func GetGovCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { } return cmd } + +// mustGetExampleCommitteeChangeProposal is a helper function to return an example json proposal +func mustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string { + exampleChangeProposal := types.NewCommitteeChangeProposal( + "A Title", + "A description of this proposal.", + types.NewCommittee( + 1, + []sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddres")))}, + []types.Permission{}, // TODO permissions + ), + ) + exampleChangeProposalBz, err := cdc.MarshalJSONIndent(exampleChangeProposal, "", " ") + if err != nil { + panic(err) + } + return string(exampleChangeProposalBz) +} + +// mustGetExampleCommitteeDeleteProposal is a helper function to return an example json proposal +func mustGetExampleCommitteeDeleteProposal(cdc *codec.Codec) string { + exampleDeleteProposal := types.NewCommitteeDeleteProposal( + "A Title", + "A description of this proposal.", + 1, + ) + exampleDeleteProposalBz, err := cdc.MarshalJSONIndent(exampleDeleteProposal, "", " ") + if err != nil { + panic(err) + } + return string(exampleDeleteProposalBz) +} + +// mustGetExampleParameterChangeProposal is a helper function to return an example json proposal +func mustGetExampleParameterChangeProposal(cdc *codec.Codec) string { + exampleParameterChangeProposal := params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{params.NewParamChange("cdp", "SurplusAuctionThreshold", "1000000000")}, + ) + exampleParameterChangeProposalBz, err := cdc.MarshalJSONIndent(exampleParameterChangeProposal, "", " ") + if err != nil { + panic(err) + } + return string(exampleParameterChangeProposalBz) +} diff --git a/x/committee/client/rest/query.go b/x/committee/client/rest/query.go index 4ec04e7d..07ec7478 100644 --- a/x/committee/client/rest/query.go +++ b/x/committee/client/rest/query.go @@ -22,8 +22,7 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/proposer", types.ModuleName, RestProposalID), queryProposerHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/tally", types.ModuleName, RestProposalID), queryTallyOnProposalHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes", types.ModuleName, RestProposalID), queryVotesOnProposalHandlerFn(cliCtx)).Methods("GET") - //r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes/{%s}", types.ModuleName, RestProposalID, RestVoter), queryVoteHandlerFn(cliCtx)).Methods("GET") - //r.HandleFunc(fmt.Sprintf("/%s/parameters/{%s}", types.ModuleName, RestParamsType), queryParamsHandlerFn(cliCtx)).Methods("GET") + // TODO r.HandleFunc(fmt.Sprintf("/%s/parameters/{%s}", types.ModuleName, RestParamsType), queryParamsHandlerFn(cliCtx)).Methods("GET") } // ---------- Committees ---------- @@ -68,7 +67,7 @@ func queryCommitteeHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { if !ok { return } - bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(committeeID)) + bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryCommitteeParams(committeeID)) if err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return @@ -108,7 +107,7 @@ func queryProposalsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { if !ok { return } - bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(committeeID)) + bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryCommitteeParams(committeeID)) if err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return @@ -153,7 +152,7 @@ func queryProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } // Query - res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryProposals), bz) + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryProposal), bz) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return @@ -253,88 +252,6 @@ func queryVotesOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } -// func queryVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { -// return func(w http.ResponseWriter, r *http.Request) { -// vars := mux.Vars(r) -// strProposalID := vars[RestProposalID] -// bechVoterAddr := vars[RestVoter] - -// if len(strProposalID) == 0 { -// err := errors.New("proposalId required but not specified") -// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) -// return -// } - -// proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, strProposalID) -// if !ok { -// return -// } - -// if len(bechVoterAddr) == 0 { -// err := errors.New("voter address required but not specified") -// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) -// return -// } - -// voterAddr, err := sdk.AccAddressFromBech32(bechVoterAddr) -// if err != nil { -// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) -// return -// } - -// cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) -// if !ok { -// return -// } - -// params := types.NewQueryVoteParams(proposalID, voterAddr) - -// bz, err := cliCtx.Codec.MarshalJSON(params) -// if err != nil { -// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) -// return -// } - -// res, _, err := cliCtx.QueryWithData("custom/gov/vote", bz) -// if err != nil { -// rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) -// return -// } - -// var vote types.Vote -// if err := cliCtx.Codec.UnmarshalJSON(res, &vote); err != nil { -// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) -// return -// } - -// // For an empty vote, either the proposal does not exist or is inactive in -// // which case the vote would be removed from state and should be queried for -// // directly via a txs query. -// if vote.Empty() { -// bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID)) -// if err != nil { -// rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) -// return -// } - -// res, _, err = cliCtx.QueryWithData("custom/gov/proposal", bz) -// if err != nil || len(res) == 0 { -// err := fmt.Errorf("proposalID %d does not exist", proposalID) -// rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) -// return -// } - -// res, err = gcutils.QueryVoteByTxQuery(cliCtx, params) -// if err != nil { -// rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) -// return -// } -// } - -// rest.PostProcessResponse(w, cliCtx, res) -// } -// } - func queryTallyOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Parse the query height @@ -375,6 +292,7 @@ func queryTallyOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { // ---------- Params ---------- +// TODO // func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { // return func(w http.ResponseWriter, r *http.Request) { // vars := mux.Vars(r) diff --git a/x/committee/client/rest/rest.go b/x/committee/client/rest/rest.go index 8ff0a7b5..a172adba 100644 --- a/x/committee/client/rest/rest.go +++ b/x/committee/client/rest/rest.go @@ -11,19 +11,10 @@ const ( RestProposalID = "proposal-id" RestCommitteeID = "committee-id" RestVoter = "voter" - //RestProposalStatus = "status" - //RestNumLimit = "limit" ) -// // ProposalRESTHandler defines a REST handler implemented in another module. The -// // sub-route is mounted on the governance REST handler. -// type ProposalRESTHandler struct { -// SubRoute string -// Handler func(http.ResponseWriter, *http.Request) -// } - // RegisterRoutes - Central function to define routes that get registered by the main application -func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router /*, phs []ProposalRESTHandler*/) { +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { registerQueryRoutes(cliCtx, r) - registerTxRoutes(cliCtx, r /* , phs*/) + registerTxRoutes(cliCtx, r) } diff --git a/x/committee/client/rest/tx.go b/x/committee/client/rest/tx.go index 983e3a55..01e1b7c4 100644 --- a/x/committee/client/rest/tx.go +++ b/x/committee/client/rest/tx.go @@ -16,12 +16,7 @@ import ( "github.com/kava-labs/kava/x/committee/types" ) -func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router /*, phs []ProposalRESTHandler*/) { - // propSubRtr := r.PathPrefix("/gov/proposals").Subrouter() - // for _, ph := range phs { - // propSubRtr.HandleFunc(fmt.Sprintf("/%s", ph.SubRoute), ph.Handler).Methods("POST") - // } - +func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc(fmt.Sprintf("/%s/committees/{%s}/proposals", types.ModuleName, RestCommitteeID), postProposalHandlerFn(cliCtx)).Methods("POST") r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes", types.ModuleName, RestProposalID), postVoteHandlerFn(cliCtx)).Methods("POST") } diff --git a/x/committee/types/gov_proposal.go b/x/committee/types/gov_proposal.go index ed7dfe86..f80b5047 100644 --- a/x/committee/types/gov_proposal.go +++ b/x/committee/types/gov_proposal.go @@ -22,8 +22,14 @@ type CommitteeChangeProposal struct { var _ govtypes.Content = CommitteeChangeProposal{} func init() { + // Gov proposals need to be registered on gov's ModuleCdc so MsgSubmitProposal can be encoded. govtypes.RegisterProposalType(ProposalTypeCommitteeChange) govtypes.RegisterProposalTypeCodec(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal") + // Since these proposals include Permissions that needs to be registered as well (including the interface and concrete types) + govtypes.ModuleCdc.RegisterInterface((*Permission)(nil), nil) + govtypes.RegisterProposalTypeCodec(GodPermission{}, "kava/GodPermission") + // TODO register other permissions here + // TODO write these //RegisterProposalType(ProposalTypeCommitteeChange) //RegisterProposalTypeCodec(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal") diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 391d3b69..37c412fa 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -24,6 +24,14 @@ type Committee struct { Permissions []Permission `json:"permissions" yaml:"permissions"` } +func NewCommittee(id uint64, members []sdk.AccAddress, permissions []Permission) Committee { + return Committee{ + ID: id, + Members: members, + Permissions: permissions, + } +} + func (c Committee) HasMember(addr sdk.AccAddress) bool { for _, m := range c.Members { if m.Equals(addr) { From c50f6bc9fa3119e34c6363ff4b8f885f6b46fbb4 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 26 Mar 2020 20:17:49 +0000 Subject: [PATCH 25/54] refactor out vote tallying --- x/committee/abci.go | 5 +- x/committee/handler.go | 18 +++-- x/committee/keeper/proposal.go | 65 +++++++++------- x/committee/keeper/proposal_test.go | 110 +++++++++++++++++++++------- x/committee/keeper/querier.go | 17 +---- x/committee/keeper/querier_test.go | 19 ++--- 6 files changed, 141 insertions(+), 93 deletions(-) diff --git a/x/committee/abci.go b/x/committee/abci.go index 5ab25598..7b56d99e 100644 --- a/x/committee/abci.go +++ b/x/committee/abci.go @@ -11,12 +11,9 @@ import ( func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k Keeper) { // Close all expired proposals - // TODO optimize by using an index to avoid iterating over non expired proposals k.IterateProposals(ctx, func(proposal types.Proposal) bool { if proposal.HasExpiredBy(ctx.BlockTime()) { - if err := k.CloseOutProposal(ctx, proposal.ID); err != nil { - panic(err) // if an expired proposal does not close then something has gone very wrong - } + k.DeleteProposalAndVotes(ctx, proposal.ID) } return false }) diff --git a/x/committee/handler.go b/x/committee/handler.go index bbc0530c..e52989f7 100644 --- a/x/committee/handler.go +++ b/x/committee/handler.go @@ -52,14 +52,16 @@ func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Resu return err.Result() } - // Try closing proposal in case enough votes have been cast - _ = k.CloseOutProposal(ctx, msg.ProposalID) - // if err.Error() == "note enough votes to close proposal" { // TODO - // return nil // This is not a reason to error - // } - // if err != nil { - // return err - // } + // Enact a proposal if it has enough votes + passes, err := k.GetProposalResult(ctx, msg.ProposalID) + if err != nil { + return err.Result() + } + if passes { + _ = k.EnactProposal(ctx, msg.ProposalID) + // log err + k.DeleteProposalAndVotes(ctx, msg.ProposalID) + } ctx.EventManager().EmitEvent( sdk.NewEvent( diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index f42be0f3..6aeefeca 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -8,6 +8,7 @@ import ( "github.com/kava-labs/kava/x/committee/types" ) +// SubmitProposal adds a proposal to a committee so that it can be voted on. func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committeeID uint64, pubProposal types.PubProposal) (uint64, sdk.Error) { // Limit proposals to only be submitted by committee members com, found := k.GetCommittee(ctx, committeeID) @@ -44,6 +45,7 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ return proposalID, nil } +// AddVote submits a vote on a proposal. func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) sdk.Error { // Validate pr, found := k.GetProposal(ctx, proposalID) @@ -67,44 +69,55 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress return nil } -func (k Keeper) CloseOutProposal(ctx sdk.Context, proposalID uint64) sdk.Error { +// GetProposalResult calculates if a proposal currently has enough votes to pass. +func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64) (bool, sdk.Error) { pr, found := k.GetProposal(ctx, proposalID) if !found { - return sdk.ErrInternal("proposal not found") + return false, sdk.ErrInternal("proposal not found") } com, found := k.GetCommittee(ctx, pr.CommitteeID) if !found { - return sdk.ErrInternal("committee disbanded") + return false, sdk.ErrInternal("committee disbanded") } + numVotes := k.TallyVotes(ctx, proposalID) + + proposalResult := sdk.NewDec(numVotes).GTE(types.VoteThreshold.MulInt64(int64(len(com.Members)))) + + return proposalResult, nil +} + +// TallyVotes counts all the votes on a proposal +func (k Keeper) TallyVotes(ctx sdk.Context, proposalID uint64) int64 { + var votes []types.Vote k.IterateVotes(ctx, proposalID, func(vote types.Vote) bool { votes = append(votes, vote) return false }) - proposalPasses := sdk.NewDec(int64(len(votes))).GTE(types.VoteThreshold.MulInt64(int64(len(com.Members)))) - if proposalPasses { - // eneact vote - // The proposal handler may execute state mutating logic depending - // on the proposal content. If the handler fails, no state mutation - // is written and the error message is logged. - handler := k.router.GetRoute(pr.ProposalRoute()) - cacheCtx, writeCache := ctx.CacheContext() - err := handler(cacheCtx, pr.PubProposal) // need to pass pubProposal as the handlers type assert it into the concrete types - if err == nil { - // write state to the underlying multi-store - writeCache() - } // if handler returns error, then still delete the proposal - it's still over, but send an event - } - if proposalPasses || pr.HasExpiredBy(ctx.BlockTime()) { - - k.DeleteProposalAndVotes(ctx, proposalID) - return nil - } - return sdk.ErrInternal("note enough votes to close proposal") + return int64(len(votes)) } +// EnactProposal makes the changes proposed in a proposal. +func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) sdk.Error { + pr, found := k.GetProposal(ctx, proposalID) + if !found { + return sdk.ErrInternal("proposal not found") + } + + // Run the proposal's changes through the associated handler, but using a cached version of state to ensure changes are not permanent if an error occurs. + handler := k.router.GetRoute(pr.ProposalRoute()) + cacheCtx, writeCache := ctx.CacheContext() + if err := handler(cacheCtx, pr.PubProposal); err != nil { + return err + } + // write state to the underlying multi-store + writeCache() + return nil +} + +// ValidatePubProposal checks if a pubproposal is valid. func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error { if pubProposal == nil { return sdk.ErrInternal("proposal is empty") @@ -117,18 +130,16 @@ func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubPropos return sdk.ErrInternal("no handler found for proposal") } - // Execute the proposal content in a cache-wrapped context to validate the - // actual parameter changes before the proposal proceeds through the - // governance process. State is not persisted. + // Run the proposal's changes through the associated handler using a cached version of state to ensure changes are not permanent. cacheCtx, _ := ctx.CacheContext() handler := k.router.GetRoute(pubProposal.ProposalRoute()) if err := handler(cacheCtx, pubProposal); err != nil { return err } - return nil } +// DeleteProposalAndVotes removes a proposal and its associated votes. func (k Keeper) DeleteProposalAndVotes(ctx sdk.Context, proposalID uint64) { var votes []types.Vote k.IterateVotes(ctx, proposalID, func(vote types.Vote) bool { diff --git a/x/committee/keeper/proposal_test.go b/x/committee/keeper/proposal_test.go index 0d9edcc2..c61c3c54 100644 --- a/x/committee/keeper/proposal_test.go +++ b/x/committee/keeper/proposal_test.go @@ -4,11 +4,13 @@ import ( "reflect" "time" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov" 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" ) @@ -76,7 +78,8 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { for _, tc := range testcases { suite.Run(tc.name, func() { - // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. + // Create local testApp because suite doesn't run the SetupTest function for subtests, + // which would mean the app state is not be reset between subtests. tApp := app.NewTestApp() keeper := tApp.GetCommitteeKeeper() ctx := tApp.NewContext(true, abci.Header{}) @@ -170,36 +173,89 @@ func (suite *KeeperTestSuite) TestAddVote() { } } -func (suite *KeeperTestSuite) TestCloseOutProposal() { - // setup test - suite.app.InitializeFromGenesisStates() - // TODO replace below with genesis state - normalCom := types.Committee{ - ID: 12, - Members: suite.addresses[:2], - Permissions: []types.Permission{types.GodPermission{}}, +func (suite *KeeperTestSuite) TestGetProposalResult() { + var defaultID uint64 = 1 + firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC) + + testcases := []struct { + name string + committee types.Committee + votes []types.Vote + proposalPasses bool + expectPass bool + }{ + { + name: "enough votes", + committee: types.Committee{ + ID: 12, + Members: suite.addresses[:5], + Permissions: []types.Permission{types.GodPermission{}}, + }, + votes: []types.Vote{ + {ProposalID: defaultID, Voter: suite.addresses[0]}, + {ProposalID: defaultID, Voter: suite.addresses[1]}, + {ProposalID: defaultID, Voter: suite.addresses[2]}, + {ProposalID: defaultID, Voter: suite.addresses[3]}, + }, + proposalPasses: true, + expectPass: true, + }, + { + name: "not enough votes", + committee: types.Committee{ + ID: 12, + Members: suite.addresses[:5], + Permissions: []types.Permission{types.GodPermission{}}, + }, + votes: []types.Vote{ + {ProposalID: defaultID, Voter: suite.addresses[0]}, + }, + proposalPasses: false, + expectPass: true, + }, } - suite.keeper.SetCommittee(suite.ctx, normalCom) - pprop := gov.NewTextProposal("A Title", "A description of this proposal.") - id, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop) - suite.NoError(err) - err = suite.keeper.AddVote(suite.ctx, id, normalCom.Members[0]) - suite.NoError(err) - err = suite.keeper.AddVote(suite.ctx, id, normalCom.Members[1]) - suite.NoError(err) - // run test - err = suite.keeper.CloseOutProposal(suite.ctx, id) + for _, tc := range testcases { + suite.Run(tc.name, func() { + // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. + tApp := app.NewTestApp() + keeper := tApp.GetCommitteeKeeper() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime}) - // check - suite.NoError(err) - _, found := suite.keeper.GetProposal(suite.ctx, id) - suite.False(found) - suite.keeper.IterateVotes(suite.ctx, id, func(v types.Vote) bool { - suite.Fail("found vote when none should exist") - return false - }) + tApp.InitializeFromGenesisStates( + committeeGenState( + tApp.Codec(), + []types.Committee{tc.committee}, + []types.Proposal{{ + PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + ID: defaultID, + CommitteeID: tc.committee.ID, + Deadline: firstBlockTime.Add(time.Hour * 24 * 7), + }}, + tc.votes, + ), + ) + proposalPasses, err := keeper.GetProposalResult(ctx, defaultID) + + if tc.expectPass { + suite.NoError(err) + suite.Equal(tc.proposalPasses, proposalPasses) + } else { + suite.NotNil(err) + } + }) + } +} + +func committeeGenState(cdc *codec.Codec, committees []types.Committee, proposals []types.Proposal, votes []types.Vote) app.GenesisState { + gs := types.NewGenesisState( + uint64(len(proposals)+1), + committees, + proposals, + votes, + ) + return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)} } type UnregisteredProposal struct { diff --git a/x/committee/keeper/querier.go b/x/committee/keeper/querier.go index 44dfa877..c378d750 100644 --- a/x/committee/keeper/querier.go +++ b/x/committee/keeper/querier.go @@ -168,24 +168,13 @@ func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Ke return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) } - // TODO split tally and process result logic so tally logic can be used here - pr, found := keeper.GetProposal(ctx, params.ProposalID) + _, found := keeper.GetProposal(ctx, params.ProposalID) if !found { return nil, sdk.ErrInternal("proposal not found") } - com, found := keeper.GetCommittee(ctx, pr.CommitteeID) - if !found { - return nil, sdk.ErrInternal("committee disbanded") - } - votes := []types.Vote{} - keeper.IterateVotes(ctx, params.ProposalID, func(vote types.Vote) bool { - votes = append(votes, vote) - return false - }) - proposalPasses := sdk.NewDec(int64(len(votes))).GTE(types.VoteThreshold.MulInt64(int64(len(com.Members)))) - // TODO return some kind of tally object, rather than just a bool + numVotes := keeper.TallyVotes(ctx, params.ProposalID) - bz, err := codec.MarshalJSONIndent(keeper.cdc, proposalPasses) + bz, err := codec.MarshalJSONIndent(keeper.cdc, numVotes) if err != nil { return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } diff --git a/x/committee/keeper/querier_test.go b/x/committee/keeper/querier_test.go index a2d8fd7e..2d532d23 100644 --- a/x/committee/keeper/querier_test.go +++ b/x/committee/keeper/querier_test.go @@ -30,17 +30,13 @@ type QuerierTestSuite struct { querier sdk.Querier - addresses []sdk.AccAddress - committees []types.Committee - proposals []types.Proposal - votes map[uint64]([]types.Vote) - expectedTallyForTheFirstProposal bool // TODO replace once tallying has been refactored + addresses []sdk.AccAddress + committees []types.Committee + proposals []types.Proposal + votes map[uint64]([]types.Vote) } func (suite *QuerierTestSuite) SetupTest() { - // SetupTest function runs before every test, but a new suite is not created every time. - // So be careful about modifying data on suite as data from previous tests will still be there. - // For example, don't append proposal to suite.proposals, initialize a new slice value. suite.app = app.NewTestApp() suite.keeper = suite.app.GetCommitteeKeeper() suite.ctx = suite.app.NewContext(true, abci.Header{}) @@ -87,8 +83,6 @@ func (suite *QuerierTestSuite) SetupTest() { }) return false }) - suite.expectedTallyForTheFirstProposal = true // TODO replace once tallying has been refactored - } func (suite *QuerierTestSuite) TestQueryCommittees() { @@ -240,12 +234,11 @@ func (suite *QuerierTestSuite) TestQueryTally() { suite.NotNil(bz) // Unmarshal the bytes - var tally bool + var tally int64 suite.NoError(suite.cdc.UnmarshalJSON(bz, &tally)) // Check - expectedTally := suite.expectedTallyForTheFirstProposal - suite.Equal(expectedTally, tally) + suite.Equal(int64(len(suite.votes[propID])), tally) } func TestQuerierTestSuite(t *testing.T) { suite.Run(t, new(QuerierTestSuite)) From 77553ed2999d10fdc3d6e48b1a6f8d33821f1420 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 27 Mar 2020 15:27:45 +0000 Subject: [PATCH 26/54] improve permission types --- x/committee/alias.go | 42 ++++++------ x/committee/module.go | 2 +- x/committee/types/codec.go | 31 ++++++--- x/committee/types/gov_proposal.go | 14 +--- x/committee/types/permissions.go | 110 +++++++++++++++++++++--------- x/committee/types/types.go | 5 +- 6 files changed, 130 insertions(+), 74 deletions(-) diff --git a/x/committee/alias.go b/x/committee/alias.go index c33b9b01..9fe5aae4 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -38,6 +38,7 @@ var ( DefaultGenesisState = types.DefaultGenesisState GetKeyFromID = types.GetKeyFromID GetVoteKey = types.GetVoteKey + NewCommittee = types.NewCommittee NewCommitteeChangeProposal = types.NewCommitteeChangeProposal NewCommitteeDeleteProposal = types.NewCommitteeDeleteProposal NewGenesisState = types.NewGenesisState @@ -46,7 +47,9 @@ var ( NewQueryCommitteeParams = types.NewQueryCommitteeParams NewQueryProposalParams = types.NewQueryProposalParams NewQueryVoteParams = types.NewQueryVoteParams - RegisterCodec = types.RegisterCodec + RegisterAppCodec = types.RegisterAppCodec + RegisterModuleCodec = types.RegisterModuleCodec + RegisterProposalTypeCodec = types.RegisterProposalTypeCodec Uint64FromBytes = types.Uint64FromBytes // variable aliases @@ -61,22 +64,23 @@ var ( ) type ( - Keeper = keeper.Keeper - Committee = types.Committee - CommitteeChangeProposal = types.CommitteeChangeProposal - CommitteeDeleteProposal = types.CommitteeDeleteProposal - GeneralShutdownPermission = types.GeneralShutdownPermission - GenesisState = types.GenesisState - GodPermission = types.GodPermission - InflationRateChangePermission = types.InflationRateChangePermission - MsgSubmitProposal = types.MsgSubmitProposal - MsgVote = types.MsgVote - Permission = types.Permission - Proposal = types.Proposal - PubProposal = types.PubProposal - QueryCommitteeParams = types.QueryCommitteeParams - QueryProposalParams = types.QueryProposalParams - QueryVoteParams = types.QueryVoteParams - ShutdownCDPDepsitPermission = types.ShutdownCDPDepsitPermission - Vote = types.Vote + Keeper = keeper.Keeper + AllowedParam = types.AllowedParam + AllowedParams = types.AllowedParams + Committee = types.Committee + CommitteeChangeProposal = types.CommitteeChangeProposal + CommitteeDeleteProposal = types.CommitteeDeleteProposal + GenesisState = types.GenesisState + GodPermission = types.GodPermission + MsgSubmitProposal = types.MsgSubmitProposal + MsgVote = types.MsgVote + ParamChangePermission = types.ParamChangePermission + Permission = types.Permission + Proposal = types.Proposal + PubProposal = types.PubProposal + QueryCommitteeParams = types.QueryCommitteeParams + QueryProposalParams = types.QueryProposalParams + QueryVoteParams = types.QueryVoteParams + ShutdownPermission = types.ShutdownPermission + Vote = types.Vote ) diff --git a/x/committee/module.go b/x/committee/module.go index e188049b..12e95b56 100644 --- a/x/committee/module.go +++ b/x/committee/module.go @@ -32,7 +32,7 @@ func (AppModuleBasic) Name() string { // RegisterCodec register module codec func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { - RegisterCodec(cdc) + RegisterAppCodec(cdc) } // DefaultGenesis default genesis state diff --git a/x/committee/types/codec.go b/x/committee/types/codec.go index 294068df..8da0a9d3 100644 --- a/x/committee/types/codec.go +++ b/x/committee/types/codec.go @@ -12,32 +12,47 @@ var ModuleCdc *codec.Codec func init() { cdc := codec.New() + RegisterModuleCodec(cdc) + ModuleCdc = cdc.Seal() +} + +// TODO decide if not using gov's Content type would be better + +func RegisterModuleCodec(cdc *codec.Codec) { 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: + // 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) - ModuleCdc = cdc.Seal() + RegisterAppCodec(cdc) } // RegisterCodec registers the necessary types for the module -func RegisterCodec(cdc *codec.Codec) { - +func RegisterAppCodec(cdc *codec.Codec) { + // Proposals // 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) + // Permissions cdc.RegisterInterface((*Permission)(nil), nil) cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil) + cdc.RegisterConcrete(ParamChangePermission{}, "kava/ParamChangePermission", nil) + cdc.RegisterConcrete(ShutdownPermission{}, "kava/ShutdownPermission", nil) + // Msgs cdc.RegisterConcrete(MsgSubmitProposal{}, "kava/MsgSubmitProposal", nil) cdc.RegisterConcrete(MsgVote{}, "kava/MsgVote", nil) } + +// RegisterProposalTypeCodec registers an external proposal content type defined +// in another module for the internal ModuleCdc. This allows the MsgSubmitProposal +// to be correctly Amino encoded and decoded. +func RegisterProposalTypeCodec(o interface{}, name string) { + ModuleCdc.RegisterConcrete(o, name, nil) +} diff --git a/x/committee/types/gov_proposal.go b/x/committee/types/gov_proposal.go index f80b5047..b5938ad3 100644 --- a/x/committee/types/gov_proposal.go +++ b/x/committee/types/gov_proposal.go @@ -25,15 +25,6 @@ func init() { // Gov proposals need to be registered on gov's ModuleCdc so MsgSubmitProposal can be encoded. govtypes.RegisterProposalType(ProposalTypeCommitteeChange) govtypes.RegisterProposalTypeCodec(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal") - // Since these proposals include Permissions that needs to be registered as well (including the interface and concrete types) - govtypes.ModuleCdc.RegisterInterface((*Permission)(nil), nil) - govtypes.RegisterProposalTypeCodec(GodPermission{}, "kava/GodPermission") - // TODO register other permissions here - - // 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 { @@ -83,12 +74,9 @@ type CommitteeDeleteProposal struct { var _ govtypes.Content = CommitteeDeleteProposal{} func init() { + // Gov proposals need to be registered on gov's ModuleCdc so MsgSubmitProposal can be encoded. 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 { diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index 807fc1f4..2481587e 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -6,64 +6,110 @@ import ( sdtypes "github.com/kava-labs/kava/x/shutdown/types" ) -// EXAMPLE PERMISSIONS ------------------------------ +func init() { + // Gov proposals need to be registered on gov's ModuleCdc. + // But since proposals contain Permissions, those types also need registering. + gov.ModuleCdc.RegisterInterface((*Permission)(nil), nil) + gov.RegisterProposalTypeCodec(GodPermission{}, "kava/GodPermission") + gov.RegisterProposalTypeCodec(ParamChangePermission{}, "kava/ParamChangePermission") + gov.RegisterProposalTypeCodec(ShutdownPermission{}, "kava/ShutdownPermission") +} +// GodPermission allows any governance proposal. It is used mainly for testing. type GodPermission struct{} +var _ Permission = GodPermission{} + func (GodPermission) Allows(gov.Content) bool { return true } -// Allow only changes to inflation_rate -type InflationRateChangePermission struct{} +func (GodPermission) MarshalYAML() (interface{}, error) { + valueToMarshal := struct { + Type string `yaml:"type"` + }{ + Type: "god_permission", + } + return valueToMarshal, nil +} -var _ Permission = InflationRateChangePermission{} +// ParamChangeProposal only allows changes to certain params +type ParamChangePermission struct { + AllowedParams AllowedParams `json:"allowed_params" yaml:"allowed_params"` +} -func (InflationRateChangePermission) Allows(p gov.Content) bool { - pcp, ok := p.(params.ParameterChangeProposal) +var _ Permission = ParamChangePermission{} + +func (perm ParamChangePermission) Allows(p gov.Content) bool { + proposal, ok := p.(params.ParameterChangeProposal) if !ok { return false } - for _, pc := range pcp.Changes { - if pc.Key == "inflation_rate" { + for _, change := range proposal.Changes { + if !perm.AllowedParams.Contains(change) { + return false + } + } + return true +} + +func (perm ParamChangePermission) MarshalYAML() (interface{}, error) { + valueToMarshal := struct { + Type string `yaml:"type"` + AllowedParams AllowedParams `yaml:"allowed_params` + }{ + Type: "param_change_permission", + AllowedParams: perm.AllowedParams, + } + return valueToMarshal, nil +} + +type AllowedParam struct { + Subspace string `json:"subspace" yaml:"subspace"` + Key string `json:"key" yaml:"key"` + Subkey string `json:"subkey,omitempty" yaml:"subkey,omitempty"` +} +type AllowedParams []AllowedParam + +func (allowed AllowedParams) Contains(paramChange params.ParamChange) bool { + for _, p := range allowed { + if paramChange.Subspace == p.Subspace && paramChange.Key == p.Key && paramChange.Subkey == p.Subkey { return true } } return false } -// Allow only shutdown of the CDP Deposit msg -type ShutdownCDPDepsitPermission struct{} - -var _ Permission = ShutdownCDPDepsitPermission{} - -func (ShutdownCDPDepsitPermission) Allows(p gov.Content) bool { - sdp, ok := p.(sdtypes.ShutdownProposal) - if !ok { - return false - } - for _, r := range sdp.MsgRoutes { - if r.Route == "cdp" && r.Msg == "MsgCDPDeposit" { - return true - } - } - return false -} - -// Same as above but the route isn't static -type GeneralShutdownPermission struct { +// ShutdownPermission allows certain message types to be disabled +type ShutdownPermission struct { MsgRoute sdtypes.MsgRoute `json:"msg_route" yaml:"msg_route"` } -var _ Permission = GeneralShutdownPermission{} +var _ Permission = ShutdownPermission{} -func (perm GeneralShutdownPermission) Allows(p gov.Content) bool { - sdp, ok := p.(sdtypes.ShutdownProposal) +func (perm ShutdownPermission) Allows(p gov.Content) bool { + proposal, ok := p.(sdtypes.ShutdownProposal) if !ok { return false } - for _, r := range sdp.MsgRoutes { + for _, r := range proposal.MsgRoutes { if r == perm.MsgRoute { return true } } return false } + +func (perm ShutdownPermission) MarshalYAML() (interface{}, error) { + valueToMarshal := struct { + Type string `yaml:"type"` + MsgRoute sdtypes.MsgRoute `yaml:"msg_route"` + }{ + Type: "shutdown_permission", + MsgRoute: perm.MsgRoute, + } + return valueToMarshal, nil +} + +// TODO add more permissions? +// - limit parameter changes to bew withing small ranges or fixed sets +// - allow community spend proposals +// - allow committee change proposals diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 37c412fa..ab1c3ede 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -19,9 +19,12 @@ var ( // A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. type Committee struct { - ID uint64 `json:"id" yaml:"id"` // TODO or a name? + ID uint64 `json:"id" yaml:"id"` + //Description string `json:"description" yaml:"description"` Members []sdk.AccAddress `json:"members" yaml:"members"` Permissions []Permission `json:"permissions" yaml:"permissions"` + // VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` + // MaxProposalDuration time.Duration `json:"max_proposal_duration" yaml:"max_proposal_duration"` } func NewCommittee(id uint64, members []sdk.AccAddress, permissions []Permission) Committee { From 57f4ca7c9afeae39c4adc14f602435dcd90e262e Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 27 Mar 2020 18:34:03 +0000 Subject: [PATCH 27/54] add mre fields to committee type --- x/committee/abci_test.go | 12 ++-- x/committee/alias.go | 15 ++--- x/committee/client/cli/tx.go | 10 ++- x/committee/keeper/keeper_test.go | 19 ++++-- x/committee/keeper/proposal.go | 4 +- x/committee/keeper/proposal_test.go | 44 ++++++------- x/committee/keeper/querier_test.go | 98 +++++++++++++++------------- x/committee/proposal_handler_test.go | 43 +++++++----- x/committee/types/genesis_test.go | 19 ++++-- x/committee/types/permissions.go | 1 + x/committee/types/types.go | 43 +++++++----- 11 files changed, 183 insertions(+), 125 deletions(-) diff --git a/x/committee/abci_test.go b/x/committee/abci_test.go index 7c64b766..adfa501d 100644 --- a/x/committee/abci_test.go +++ b/x/committee/abci_test.go @@ -14,6 +14,8 @@ import ( "github.com/kava-labs/kava/x/committee" ) +func d(s string) sdk.Dec { return sdk.MustNewDecFromStr(s) } + type ModuleTestSuite struct { suite.Suite @@ -35,9 +37,11 @@ func (suite *ModuleTestSuite) TestBeginBlock() { suite.app.InitializeFromGenesisStates() // TODO replace below with genesis state normalCom := committee.Committee{ - ID: 12, - Members: suite.addresses[:2], - Permissions: []committee.Permission{committee.GodPermission{}}, + ID: 12, + Members: suite.addresses[:2], + Permissions: []committee.Permission{committee.GodPermission{}}, + VoteThreshold: d("0.8"), + MaxProposalDuration: time.Hour * 24 * 7, } suite.keeper.SetCommittee(suite.ctx, normalCom) @@ -51,7 +55,7 @@ func (suite *ModuleTestSuite) TestBeginBlock() { suite.NoError(err) // Run BeginBlocker - proposalDurationLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(committee.MaxProposalDuration)) + proposalDurationLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(normalCom.MaxProposalDuration)) suite.NotPanics(func() { committee.BeginBlocker(proposalDurationLaterCtx, abci.RequestBeginBlock{}, suite.keeper) }) diff --git a/x/committee/alias.go b/x/committee/alias.go index 9fe5aae4..1fe2eab5 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -14,6 +14,7 @@ const ( DefaultNextProposalID = types.DefaultNextProposalID DefaultParamspace = types.DefaultParamspace EventTypeSubmitProposal = types.EventTypeSubmitProposal + MaxDescriptionLength = types.MaxDescriptionLength ModuleName = types.ModuleName ProposalTypeCommitteeChange = types.ProposalTypeCommitteeChange ProposalTypeCommitteeDelete = types.ProposalTypeCommitteeDelete @@ -53,14 +54,12 @@ var ( Uint64FromBytes = types.Uint64FromBytes // variable aliases - ProposalHandler = client.ProposalHandler - CommitteeKeyPrefix = types.CommitteeKeyPrefix - MaxProposalDuration = types.MaxProposalDuration - ModuleCdc = types.ModuleCdc - NextProposalIDKey = types.NextProposalIDKey - ProposalKeyPrefix = types.ProposalKeyPrefix - VoteKeyPrefix = types.VoteKeyPrefix - VoteThreshold = types.VoteThreshold + ProposalHandler = client.ProposalHandler + CommitteeKeyPrefix = types.CommitteeKeyPrefix + ModuleCdc = types.ModuleCdc + NextProposalIDKey = types.NextProposalIDKey + ProposalKeyPrefix = types.ProposalKeyPrefix + VoteKeyPrefix = types.VoteKeyPrefix ) type ( diff --git a/x/committee/client/cli/tx.go b/x/committee/client/cli/tx.go index ce6bed5b..2261a9c1 100644 --- a/x/committee/client/cli/tx.go +++ b/x/committee/client/cli/tx.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "strconv" + "time" "github.com/spf13/cobra" @@ -189,8 +190,15 @@ func mustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string { "A description of this proposal.", types.NewCommittee( 1, + "The description of this committee.", []sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddres")))}, - []types.Permission{}, // TODO permissions + []types.Permission{ + types.ParamChangePermission{ + AllowedParams: types.AllowedParams{{Subspace: "cdp", Key: "CircuitBreaker"}}, + }, + }, + sdk.MustNewDecFromStr("0.8"), + time.Hour*24*7, ), ) exampleChangeProposalBz, err := cdc.MarshalJSONIndent(exampleChangeProposal, "", " ") diff --git a/x/committee/keeper/keeper_test.go b/x/committee/keeper/keeper_test.go index f57e380a..90f624a7 100644 --- a/x/committee/keeper/keeper_test.go +++ b/x/committee/keeper/keeper_test.go @@ -2,10 +2,12 @@ package keeper_test import ( "testing" + "time" "github.com/stretchr/testify/suite" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" @@ -13,6 +15,8 @@ import ( "github.com/kava-labs/kava/x/committee/types" ) +func d(s string) sdk.Dec { return sdk.MustNewDecFromStr(s) } + type KeeperTestSuite struct { suite.Suite @@ -33,8 +37,12 @@ func (suite *KeeperTestSuite) SetupTest() { func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() { // setup test com := types.Committee{ - ID: 12, - // TODO other fields + ID: 12, + Description: "This committee is for testing.", + Members: suite.addresses, + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + MaxProposalDuration: time.Hour * 24 * 7, } // write and read from store @@ -56,8 +64,10 @@ func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() { func (suite *KeeperTestSuite) TestGetSetProposal() { // test setup prop := types.Proposal{ - ID: 12, - // TODO other fields + ID: 12, + CommitteeID: 0, + PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + Deadline: time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), } // write and read from store @@ -81,7 +91,6 @@ func (suite *KeeperTestSuite) TestGetSetVote() { vote := types.Vote{ ProposalID: 12, Voter: suite.addresses[0], - // TODO other fields } // write and read from store diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index 6aeefeca..ad0e3ced 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -30,7 +30,7 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ } // Get a new ID and store the proposal - deadline := ctx.BlockTime().Add(types.MaxProposalDuration) + deadline := ctx.BlockTime().Add(com.MaxProposalDuration) proposalID, err := k.StoreNewProposal(ctx, pubProposal, committeeID, deadline) if err != nil { return 0, err @@ -82,7 +82,7 @@ func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64) (bool, sdk numVotes := k.TallyVotes(ctx, proposalID) - proposalResult := sdk.NewDec(numVotes).GTE(types.VoteThreshold.MulInt64(int64(len(com.Members)))) + proposalResult := sdk.NewDec(numVotes).GTE(com.VoteThreshold.MulInt64(int64(len(com.Members)))) return proposalResult, nil } diff --git a/x/committee/keeper/proposal_test.go b/x/committee/keeper/proposal_test.go index c61c3c54..6bf59141 100644 --- a/x/committee/keeper/proposal_test.go +++ b/x/committee/keeper/proposal_test.go @@ -16,15 +16,15 @@ import ( func (suite *KeeperTestSuite) TestSubmitProposal() { normalCom := types.Committee{ - ID: 12, - Members: suite.addresses[:2], - Permissions: []types.Permission{types.GodPermission{}}, - } - noPermissionsCom := types.Committee{ - ID: 12, - Members: suite.addresses[:2], - Permissions: []types.Permission{}, + ID: 12, + Description: "This committee is for testing.", + Members: suite.addresses[:2], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + MaxProposalDuration: time.Hour * 24 * 7, } + noPermissionsCom := normalCom + noPermissionsCom.Permissions = []types.Permission{} testcases := []struct { name string @@ -96,7 +96,7 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { pr, found := keeper.GetProposal(ctx, id) suite.True(found) suite.Equal(tc.committeeID, pr.CommitteeID) - suite.Equal(ctx.BlockTime().Add(types.MaxProposalDuration), pr.Deadline) + suite.Equal(ctx.BlockTime().Add(tc.committee.MaxProposalDuration), pr.Deadline) } else { suite.NotNil(err) } @@ -141,7 +141,7 @@ func (suite *KeeperTestSuite) TestAddVote() { name: "proposal expired", proposalID: types.DefaultNextProposalID, voter: normalCom.Members[0], - voteTime: firstBlockTime.Add(types.MaxProposalDuration), + voteTime: firstBlockTime.Add(normalCom.MaxProposalDuration), expectPass: false, }, } @@ -174,6 +174,14 @@ func (suite *KeeperTestSuite) TestAddVote() { } func (suite *KeeperTestSuite) TestGetProposalResult() { + normalCom := types.Committee{ + ID: 12, + Description: "This committee is for testing.", + Members: suite.addresses[:5], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + MaxProposalDuration: time.Hour * 24 * 7, + } var defaultID uint64 = 1 firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC) @@ -185,12 +193,8 @@ func (suite *KeeperTestSuite) TestGetProposalResult() { expectPass bool }{ { - name: "enough votes", - committee: types.Committee{ - ID: 12, - Members: suite.addresses[:5], - Permissions: []types.Permission{types.GodPermission{}}, - }, + name: "enough votes", + committee: normalCom, votes: []types.Vote{ {ProposalID: defaultID, Voter: suite.addresses[0]}, {ProposalID: defaultID, Voter: suite.addresses[1]}, @@ -201,12 +205,8 @@ func (suite *KeeperTestSuite) TestGetProposalResult() { expectPass: true, }, { - name: "not enough votes", - committee: types.Committee{ - ID: 12, - Members: suite.addresses[:5], - Permissions: []types.Permission{types.GodPermission{}}, - }, + name: "not enough votes", + committee: normalCom, votes: []types.Vote{ {ProposalID: defaultID, Voter: suite.addresses[0]}, }, diff --git a/x/committee/keeper/querier_test.go b/x/committee/keeper/querier_test.go index 2d532d23..92cfcb2f 100644 --- a/x/committee/keeper/querier_test.go +++ b/x/committee/keeper/querier_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "strings" "testing" + "time" "github.com/stretchr/testify/suite" @@ -12,6 +13,7 @@ import ( 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/keeper" "github.com/kava-labs/kava/x/committee/types" ) @@ -20,6 +22,12 @@ const ( custom = "custom" ) +var testTime time.Time = time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC) + +func NewCommitteeGenesisState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState { + return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)} +} + type QuerierTestSuite struct { suite.Suite @@ -30,10 +38,9 @@ type QuerierTestSuite struct { querier sdk.Querier - addresses []sdk.AccAddress - committees []types.Committee - proposals []types.Proposal - votes map[uint64]([]types.Vote) + addresses []sdk.AccAddress + testGenesis types.GenesisState + votes map[uint64]([]types.Vote) } func (suite *QuerierTestSuite) SetupTest() { @@ -44,37 +51,40 @@ func (suite *QuerierTestSuite) SetupTest() { suite.querier = keeper.NewQuerier(suite.keeper) _, suite.addresses = app.GeneratePrivKeyAddressPairs(5) - suite.app.InitializeFromGenesisStates() - // TODO replace below with genesis state - normalCom := types.Committee{ - ID: 12, - Members: suite.addresses[:2], - Permissions: []types.Permission{types.GodPermission{}}, - } - suite.keeper.SetCommittee(suite.ctx, normalCom) + suite.testGenesis = types.NewGenesisState( + 3, + []types.Committee{ + { + ID: 1, + Description: "This committee is for testing.", + Members: suite.addresses[:3], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + MaxProposalDuration: time.Hour * 24 * 7, + }, + { + ID: 2, + Members: suite.addresses[2:], + Permissions: nil, + VoteThreshold: d("0.667"), + MaxProposalDuration: time.Hour * 24 * 7, + }, + }, + []types.Proposal{ + {ID: 1, CommitteeID: 1, PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)}, + {ID: 2, CommitteeID: 1, PubProposal: gov.NewTextProposal("Another Title", "A description of this other proposal."), Deadline: testTime.Add(21 * 24 * time.Hour)}, + }, + []types.Vote{ + {ProposalID: 1, Voter: suite.addresses[0]}, + {ProposalID: 1, Voter: suite.addresses[1]}, + {ProposalID: 2, Voter: suite.addresses[2]}, + }, + ) + suite.app.InitializeFromGenesisStates( + NewCommitteeGenesisState(suite.cdc, suite.testGenesis), + ) - pprop1 := gov.NewTextProposal("1A Title", "A description of this proposal.") - id1, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop1) - suite.NoError(err) - - pprop2 := gov.NewTextProposal("2A Title", "A description of this proposal.") - id2, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop2) - suite.NoError(err) - - err = suite.keeper.AddVote(suite.ctx, id1, normalCom.Members[0]) - suite.NoError(err) - err = suite.keeper.AddVote(suite.ctx, id1, normalCom.Members[1]) - suite.NoError(err) - err = suite.keeper.AddVote(suite.ctx, id2, normalCom.Members[1]) - suite.NoError(err) - - suite.committees = []types.Committee{} - suite.committees = []types.Committee{normalCom} // TODO - suite.proposals = []types.Proposal{} - suite.keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { - suite.proposals = append(suite.proposals, p) - return false - }) + // Collect up votes into a map indexed by proposalID for convenience suite.votes = map[uint64]([]types.Vote){} suite.keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { suite.keeper.IterateVotes(suite.ctx, p.ID, func(v types.Vote) bool { @@ -102,7 +112,7 @@ func (suite *QuerierTestSuite) TestQueryCommittees() { suite.NoError(suite.cdc.UnmarshalJSON(bz, &committees)) // Check - suite.Equal(suite.committees, committees) + suite.Equal(suite.testGenesis.Committees, committees) } func (suite *QuerierTestSuite) TestQueryCommittee() { @@ -110,7 +120,7 @@ func (suite *QuerierTestSuite) TestQueryCommittee() { // Set up request query query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryCommittee}, "/"), - Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(suite.committees[0].ID)), + Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(suite.testGenesis.Committees[0].ID)), } // Execute query and check the []byte result @@ -123,13 +133,13 @@ func (suite *QuerierTestSuite) TestQueryCommittee() { suite.NoError(suite.cdc.UnmarshalJSON(bz, &committee)) // Check - suite.Equal(suite.committees[0], committee) + suite.Equal(suite.testGenesis.Committees[0], committee) } func (suite *QuerierTestSuite) TestQueryProposals() { ctx := suite.ctx.WithIsCheckTx(false) // Set up request query - comID := suite.proposals[0].CommitteeID + comID := suite.testGenesis.Proposals[0].CommitteeID query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposals}, "/"), Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(comID)), @@ -146,7 +156,7 @@ func (suite *QuerierTestSuite) TestQueryProposals() { // Check expectedProposals := []types.Proposal{} - for _, p := range suite.proposals { + for _, p := range suite.testGenesis.Proposals { if p.CommitteeID == comID { expectedProposals = append(expectedProposals, p) } @@ -159,7 +169,7 @@ func (suite *QuerierTestSuite) TestQueryProposal() { // Set up request query query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposal}, "/"), - Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(suite.proposals[0].ID)), + Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(suite.testGenesis.Proposals[0].ID)), } // Execute query and check the []byte result @@ -172,13 +182,13 @@ func (suite *QuerierTestSuite) TestQueryProposal() { suite.NoError(suite.cdc.UnmarshalJSON(bz, &proposal)) // Check - suite.Equal(suite.proposals[0], proposal) + suite.Equal(suite.testGenesis.Proposals[0], proposal) } func (suite *QuerierTestSuite) TestQueryVotes() { ctx := suite.ctx.WithIsCheckTx(false) // Set up request query - propID := suite.proposals[0].ID + propID := suite.testGenesis.Proposals[0].ID query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryVotes}, "/"), Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)), @@ -200,7 +210,7 @@ func (suite *QuerierTestSuite) TestQueryVotes() { func (suite *QuerierTestSuite) TestQueryVote() { ctx := suite.ctx.WithIsCheckTx(false) // ? // Set up request query - propID := suite.proposals[0].ID + propID := suite.testGenesis.Proposals[0].ID query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryVote}, "/"), Data: suite.cdc.MustMarshalJSON(types.NewQueryVoteParams(propID, suite.votes[propID][0].Voter)), @@ -222,7 +232,7 @@ func (suite *QuerierTestSuite) TestQueryVote() { func (suite *QuerierTestSuite) TestQueryTally() { ctx := suite.ctx.WithIsCheckTx(false) // ? // Set up request query - propID := suite.proposals[0].ID + propID := suite.testGenesis.Proposals[0].ID query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryTally}, "/"), Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)), diff --git a/x/committee/proposal_handler_test.go b/x/committee/proposal_handler_test.go index c5150462..f9c58263 100644 --- a/x/committee/proposal_handler_test.go +++ b/x/committee/proposal_handler_test.go @@ -38,14 +38,19 @@ func (suite *ProposalHandlerTestSuite) SetupTest() { 2, []committee.Committee{ { - ID: 1, - Members: suite.addresses[:3], - Permissions: []types.Permission{types.GodPermission{}}, + ID: 1, + Description: "This committee is for testing.", + Members: suite.addresses[:3], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + MaxProposalDuration: time.Hour * 24 * 7, }, { - ID: 2, - Members: suite.addresses[2:], - Permissions: nil, + ID: 2, + Members: suite.addresses[2:], + Permissions: nil, + VoteThreshold: d("0.667"), + MaxProposalDuration: time.Hour * 24 * 7, }, }, []committee.Proposal{ @@ -69,8 +74,10 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { "A Title", "A proposal description.", committee.Committee{ - ID: 34, - Members: suite.addresses[:1], + ID: 34, + Members: suite.addresses[:1], + VoteThreshold: d("1"), + MaxProposalDuration: time.Hour * 24, }, ), expectPass: true, @@ -81,9 +88,11 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { "A Title", "A proposal description.", committee.Committee{ - ID: 1, - Members: suite.addresses, - Permissions: suite.testGenesis.Committees[0].Permissions, + ID: suite.testGenesis.Committees[0].ID, + Members: suite.addresses, // add new members + Permissions: suite.testGenesis.Committees[0].Permissions, + VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold, + MaxProposalDuration: suite.testGenesis.Committees[0].MaxProposalDuration, }, ), expectPass: true, @@ -93,9 +102,7 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { 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, - }, + suite.testGenesis.Committees[0], ), expectPass: false, }, @@ -105,9 +112,11 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { "A Title", "A proposal description.", committee.Committee{ - ID: 1, - Members: append(suite.addresses, suite.addresses[0]), // duplicate address - Permissions: suite.testGenesis.Committees[0].Permissions, + ID: suite.testGenesis.Committees[0].ID, + Members: append(suite.addresses, suite.addresses[0]), // duplicate address + Permissions: suite.testGenesis.Committees[0].Permissions, + VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold, + MaxProposalDuration: suite.testGenesis.Committees[0].MaxProposalDuration, }, ), expectPass: false, diff --git a/x/committee/types/genesis_test.go b/x/committee/types/genesis_test.go index 9b76d4f2..009b5371 100644 --- a/x/committee/types/genesis_test.go +++ b/x/committee/types/genesis_test.go @@ -11,6 +11,7 @@ import ( "github.com/tendermint/tendermint/crypto" ) +func d(s string) sdk.Dec { return sdk.MustNewDecFromStr(s) } func TestGenesisState_Validate(t *testing.T) { testTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC) addresses := []sdk.AccAddress{ @@ -24,14 +25,20 @@ func TestGenesisState_Validate(t *testing.T) { NextProposalID: 2, Committees: []Committee{ { - ID: 1, - Members: addresses[:3], - Permissions: []Permission{GodPermission{}}, + ID: 1, + Description: "This committee is for testing.", + Members: addresses[:3], + Permissions: []Permission{GodPermission{}}, + VoteThreshold: d("0.667"), + MaxProposalDuration: time.Hour * 24 * 7, }, { - ID: 2, - Members: addresses[2:], - Permissions: nil, + ID: 2, + Description: "This committee is also for testing.", + Members: addresses[2:], + Permissions: nil, + VoteThreshold: d("0.8"), + MaxProposalDuration: time.Hour * 24 * 21, }, }, Proposals: []Proposal{ diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index 2481587e..f3ae5640 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -16,6 +16,7 @@ func init() { } // GodPermission allows any governance proposal. It is used mainly for testing. +// TODO better name? type GodPermission struct{} var _ Permission = GodPermission{} diff --git a/x/committee/types/types.go b/x/committee/types/types.go index ab1c3ede..4665a044 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -9,29 +9,28 @@ import ( "github.com/cosmos/cosmos-sdk/x/gov" ) -// TODO move these into params -var ( - VoteThreshold sdk.Dec = sdk.MustNewDecFromStr("0.75") - MaxProposalDuration time.Duration = time.Hour * 24 * 7 -) +const MaxCommitteeDescriptionLength int = 5000 // -------- Committees -------- // A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. type Committee struct { - ID uint64 `json:"id" yaml:"id"` - //Description string `json:"description" yaml:"description"` - Members []sdk.AccAddress `json:"members" yaml:"members"` - Permissions []Permission `json:"permissions" yaml:"permissions"` - // VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` - // MaxProposalDuration time.Duration `json:"max_proposal_duration" yaml:"max_proposal_duration"` + ID uint64 `json:"id" yaml:"id"` + Description string `json:"description" yaml:"description"` + Members []sdk.AccAddress `json:"members" yaml:"members"` + Permissions []Permission `json:"permissions" yaml:"permissions"` + VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` + MaxProposalDuration time.Duration `json:"max_proposal_duration" yaml:"max_proposal_duration"` } -func NewCommittee(id uint64, members []sdk.AccAddress, permissions []Permission) Committee { +func NewCommittee(id uint64, description string, members []sdk.AccAddress, permissions []Permission, threshold sdk.Dec, duration time.Duration) Committee { return Committee{ - ID: id, - Members: members, - Permissions: permissions, + ID: id, + Description: description, + Members: members, + Permissions: permissions, + VoteThreshold: threshold, + MaxProposalDuration: duration, } } @@ -74,6 +73,19 @@ func (c Committee) Validate() error { if len(c.Members) == 0 { return fmt.Errorf("committee %d invalid: cannot have zero members", c.ID) } + + if len(c.Description) > MaxCommitteeDescriptionLength { + return fmt.Errorf("invalid description") + } + + if c.VoteThreshold.IsNil() || c.VoteThreshold.IsNegative() || c.VoteThreshold.GT(sdk.NewDec(1)) { + return fmt.Errorf("invalid threshold") + } + + if c.MaxProposalDuration < 0 { + return fmt.Errorf("invalid time") + } + return nil } @@ -118,5 +130,4 @@ func (p Proposal) String() string { type Vote struct { ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` Voter sdk.AccAddress `json:"voter" yaml:"voter"` - // Option byte // TODO for now don't need more than just a yes as options } From e228aa6659d1021f6d11e27c7c36aa9d97d86bd8 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 27 Mar 2020 20:28:51 +0000 Subject: [PATCH 28/54] add events --- x/committee/abci.go | 12 +++++ x/committee/alias.go | 50 ++++++++++++--------- x/committee/client/common/query_proposer.go | 2 +- x/committee/handler.go | 26 +++++++++-- x/committee/keeper/proposal.go | 10 ++++- x/committee/types/events.go | 23 +++++----- 6 files changed, 83 insertions(+), 40 deletions(-) diff --git a/x/committee/abci.go b/x/committee/abci.go index 7b56d99e..3dc45499 100644 --- a/x/committee/abci.go +++ b/x/committee/abci.go @@ -1,6 +1,8 @@ package committee import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" abci "github.com/tendermint/tendermint/abci/types" @@ -13,7 +15,17 @@ func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k Keeper) { // Close all expired proposals k.IterateProposals(ctx, func(proposal types.Proposal) bool { if proposal.HasExpiredBy(ctx.BlockTime()) { + k.DeleteProposalAndVotes(ctx, proposal.ID) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeProposalClose, + sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)), + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)), + sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, types.AttributeValueProposalTimeout), + ), + ) } return false }) diff --git a/x/committee/alias.go b/x/committee/alias.go index 1fe2eab5..01d8188f 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -9,27 +9,35 @@ import ( ) const ( - AttributeKeyProposalID = types.AttributeKeyProposalID - DefaultCodespace = types.DefaultCodespace - DefaultNextProposalID = types.DefaultNextProposalID - DefaultParamspace = types.DefaultParamspace - EventTypeSubmitProposal = types.EventTypeSubmitProposal - MaxDescriptionLength = types.MaxDescriptionLength - ModuleName = types.ModuleName - ProposalTypeCommitteeChange = types.ProposalTypeCommitteeChange - ProposalTypeCommitteeDelete = types.ProposalTypeCommitteeDelete - QuerierRoute = types.QuerierRoute - QueryCommittee = types.QueryCommittee - QueryCommittees = types.QueryCommittees - QueryProposal = types.QueryProposal - QueryProposals = types.QueryProposals - QueryTally = types.QueryTally - QueryVote = types.QueryVote - QueryVotes = types.QueryVotes - RouterKey = types.RouterKey - StoreKey = types.StoreKey - TypeMsgSubmitProposal = types.TypeMsgSubmitProposal - TypeMsgVote = types.TypeMsgVote + AttributeKeyCommitteeID = types.AttributeKeyCommitteeID + AttributeKeyProposalCloseStatus = types.AttributeKeyProposalCloseStatus + AttributeKeyProposalID = types.AttributeKeyProposalID + AttributeValueCategory = types.AttributeValueCategory + AttributeValueProposalFailed = types.AttributeValueProposalFailed + AttributeValueProposalPassed = types.AttributeValueProposalPassed + AttributeValueProposalTimeout = types.AttributeValueProposalTimeout + DefaultCodespace = types.DefaultCodespace + DefaultNextProposalID = types.DefaultNextProposalID + DefaultParamspace = types.DefaultParamspace + EventTypeProposalClose = types.EventTypeProposalClose + EventTypeProposalSubmit = types.EventTypeProposalSubmit + EventTypeProposalVote = types.EventTypeProposalVote + MaxCommitteeDescriptionLength = types.MaxCommitteeDescriptionLength + ModuleName = types.ModuleName + ProposalTypeCommitteeChange = types.ProposalTypeCommitteeChange + ProposalTypeCommitteeDelete = types.ProposalTypeCommitteeDelete + QuerierRoute = types.QuerierRoute + QueryCommittee = types.QueryCommittee + QueryCommittees = types.QueryCommittees + QueryProposal = types.QueryProposal + QueryProposals = types.QueryProposals + QueryTally = types.QueryTally + QueryVote = types.QueryVote + QueryVotes = types.QueryVotes + RouterKey = types.RouterKey + StoreKey = types.StoreKey + TypeMsgSubmitProposal = types.TypeMsgSubmitProposal + TypeMsgVote = types.TypeMsgVote ) var ( diff --git a/x/committee/client/common/query_proposer.go b/x/committee/client/common/query_proposer.go index 0508f53d..5a44be42 100644 --- a/x/committee/client/common/query_proposer.go +++ b/x/committee/client/common/query_proposer.go @@ -34,7 +34,7 @@ func (p Proposer) String() string { func QueryProposer(cliCtx context.CLIContext, proposalID uint64) (Proposer, error) { events := []string{ fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeyAction, types.TypeMsgSubmitProposal), - fmt.Sprintf("%s.%s='%s'", types.EventTypeSubmitProposal, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", proposalID))), + fmt.Sprintf("%s.%s='%s'", types.EventTypeProposalSubmit, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", proposalID))), } // NOTE: SearchTxs is used to facilitate the txs query which does not currently diff --git a/x/committee/handler.go b/x/committee/handler.go index e52989f7..7233dbca 100644 --- a/x/committee/handler.go +++ b/x/committee/handler.go @@ -35,7 +35,7 @@ func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubm ctx.EventManager().EmitEvent( sdk.NewEvent( sdk.EventTypeMessage, - // TODO sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), sdk.NewAttribute(sdk.AttributeKeySender, msg.Proposer.String()), ), ) @@ -47,6 +47,12 @@ func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubm } func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Result { + // get the proposal just to add fields to the event + proposal, found := k.GetProposal(ctx, msg.ProposalID) + if !found { + return sdk.ErrInternal("proposal not found").Result() + } + err := k.AddVote(ctx, msg.ProposalID, msg.Voter) if err != nil { return err.Result() @@ -58,15 +64,27 @@ func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Resu return err.Result() } if passes { - _ = k.EnactProposal(ctx, msg.ProposalID) - // log err + err = k.EnactProposal(ctx, msg.ProposalID) + outcome := types.AttributeValueProposalPassed + if err != nil { + outcome = types.AttributeValueProposalFailed + } k.DeleteProposalAndVotes(ctx, msg.ProposalID) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeProposalClose, + sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)), + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)), + sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, outcome), + ), + ) } ctx.EventManager().EmitEvent( sdk.NewEvent( sdk.EventTypeMessage, - // TODO sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), sdk.NewAttribute(sdk.AttributeKeySender, msg.Voter.String()), ), ) diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index ad0e3ced..d0218777 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -38,7 +38,8 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ ctx.EventManager().EmitEvent( sdk.NewEvent( - types.EventTypeSubmitProposal, + types.EventTypeProposalSubmit, + sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.ID)), sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)), ), ) @@ -66,6 +67,13 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress // Store vote, overwriting any prior vote k.SetVote(ctx, types.Vote{ProposalID: proposalID, Voter: voter}) + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeProposalVote, + sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.ID)), + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", pr.ID)), + ), + ) return nil } diff --git a/x/committee/types/events.go b/x/committee/types/events.go index 5037f606..8ccbf667 100644 --- a/x/committee/types/events.go +++ b/x/committee/types/events.go @@ -2,18 +2,15 @@ package types // Module event types const ( - EventTypeSubmitProposal = "submit_proposal" - // EventTypeProposalVote = "proposal_vote" - // EventTypeInactiveProposal = "inactive_proposal" - // EventTypeActiveProposal = "active_proposal" + EventTypeProposalSubmit = "proposal_submit" + EventTypeProposalClose = "proposal_close" + EventTypeProposalVote = "proposal_vote" - // AttributeKeyProposalResult = "proposal_result" - // AttributeKeyOption = "option" - AttributeKeyProposalID = "proposal_id" - // AttributeKeyVotingPeriodStart = "voting_period_start" - // AttributeValueCategory = "governance" - // AttributeValueProposalDropped = "proposal_dropped" // didn't meet min deposit - // AttributeValueProposalPassed = "proposal_passed" // met vote quorum - // AttributeValueProposalRejected = "proposal_rejected" // didn't meet vote quorum - // AttributeValueProposalFailed = "proposal_failed" // error on proposal handler + AttributeValueCategory = "committee" + AttributeKeyCommitteeID = "committee_id" + AttributeKeyProposalID = "proposal_id" + AttributeKeyProposalCloseStatus = "status" + AttributeValueProposalPassed = "proposal_passed" + AttributeValueProposalTimeout = "proposal_timeout" + AttributeValueProposalFailed = "proposal_failed" ) From 074bb246a8b490f136b81e61c5a63c00f1e69fd1 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sun, 29 Mar 2020 20:43:25 +0100 Subject: [PATCH 29/54] add custom errors --- app/app.go | 3 ++- x/committee/alias.go | 13 ++++++++++ x/committee/handler.go | 2 +- x/committee/keeper/keeper.go | 16 ++++++------ x/committee/keeper/proposal.go | 24 +++++++++--------- x/committee/keeper/querier.go | 37 +++------------------------- x/committee/proposal_handler.go | 4 +-- x/committee/types/errors.go | 41 +++++++++++++++++++++++++++++++ x/committee/types/gov_proposal.go | 2 +- x/committee/types/msg.go | 2 +- 10 files changed, 86 insertions(+), 58 deletions(-) diff --git a/app/app.go b/app/app.go index c0ff9680..18ebdb7f 100644 --- a/app/app.go +++ b/app/app.go @@ -217,7 +217,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.committeeKeeper = committee.NewKeeper( app.cdc, keys[committee.StoreKey], - committeeGovRouter) // TODO blacklist module addresses?) + committeeGovRouter, + committee.DefaultCodespace) // TODO blacklist module addresses?) govRouter := gov.NewRouter() govRouter. AddRoute(gov.RouterKey, gov.ProposalHandler). diff --git a/x/committee/alias.go b/x/committee/alias.go index 01d8188f..baae2c18 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -16,6 +16,11 @@ const ( AttributeValueProposalFailed = types.AttributeValueProposalFailed AttributeValueProposalPassed = types.AttributeValueProposalPassed AttributeValueProposalTimeout = types.AttributeValueProposalTimeout + CodeInvalidCommittee = types.CodeInvalidCommittee + CodeInvalidGenesis = types.CodeInvalidGenesis + CodeInvalidProposal = types.CodeInvalidProposal + CodeProposalExpired = types.CodeProposalExpired + CodeUnknownItem = types.CodeUnknownItem DefaultCodespace = types.DefaultCodespace DefaultNextProposalID = types.DefaultNextProposalID DefaultParamspace = types.DefaultParamspace @@ -45,6 +50,14 @@ var ( NewKeeper = keeper.NewKeeper NewQuerier = keeper.NewQuerier DefaultGenesisState = types.DefaultGenesisState + ErrInvalidCommittee = types.ErrInvalidCommittee + ErrInvalidGenesis = types.ErrInvalidGenesis + ErrInvalidPubProposal = types.ErrInvalidPubProposal + ErrNoProposalHandlerExists = types.ErrNoProposalHandlerExists + ErrProposalExpired = types.ErrProposalExpired + ErrUnknownCommittee = types.ErrUnknownCommittee + ErrUnknownProposal = types.ErrUnknownProposal + ErrUnknownVote = types.ErrUnknownVote GetKeyFromID = types.GetKeyFromID GetVoteKey = types.GetVoteKey NewCommittee = types.NewCommittee diff --git a/x/committee/handler.go b/x/committee/handler.go index 7233dbca..23210d43 100644 --- a/x/committee/handler.go +++ b/x/committee/handler.go @@ -50,7 +50,7 @@ func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Resu // get the proposal just to add fields to the event proposal, found := k.GetProposal(ctx, msg.ProposalID) if !found { - return sdk.ErrInternal("proposal not found").Result() + return ErrUnknownProposal(DefaultCodespace, msg.ProposalID).Result() } err := k.AddVote(ctx, msg.ProposalID, msg.Voter) diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index c1471fbb..327b653a 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -12,22 +12,24 @@ import ( ) type Keeper struct { - cdc *codec.Codec - storeKey sdk.StoreKey + cdc *codec.Codec + storeKey sdk.StoreKey + codespace sdk.CodespaceType // Proposal router router govtypes.Router } -func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) Keeper { +func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router, codespace sdk.CodespaceType) Keeper { // Logic in the keeper methods assume the set of gov handlers is fixed. // So the gov router must be sealed so no handlers can be added or removed after the keeper is created. router.Seal() return Keeper{ - cdc: cdc, - storeKey: storeKey, - router: router, + cdc: cdc, + storeKey: storeKey, + codespace: codespace, + router: router, } } @@ -87,7 +89,7 @@ func (k Keeper) GetNextProposalID(ctx sdk.Context) (uint64, sdk.Error) { store := ctx.KVStore(k.storeKey) bz := store.Get(types.NextProposalIDKey) if bz == nil { - return 0, sdk.ErrInternal("proposal ID not set at genesis") + return 0, types.ErrInvalidGenesis(k.codespace, "next proposal ID not set at genesis") } return types.Uint64FromBytes(bz), nil } diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index d0218777..424bcc56 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -13,15 +13,15 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ // Limit proposals to only be submitted by committee members com, found := k.GetCommittee(ctx, committeeID) if !found { - return 0, sdk.ErrInternal("committee doesn't exist") + return 0, types.ErrUnknownCommittee(k.codespace, committeeID) } if !com.HasMember(proposer) { - return 0, sdk.ErrInternal("only member can propose proposals") + return 0, sdk.ErrUnauthorized("proposer not member of committee") } // Check committee has permissions to enact proposal. if !com.HasPermissionsFor(pubProposal) { - return 0, sdk.ErrInternal("committee does not have permissions to enact proposal") + return 0, sdk.ErrUnauthorized("committee does not have permissions to enact proposal") } // Check proposal is valid @@ -51,17 +51,17 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress // Validate pr, found := k.GetProposal(ctx, proposalID) if !found { - return sdk.ErrInternal("proposal not found") + return types.ErrUnknownProposal(k.codespace, proposalID) } if pr.HasExpiredBy(ctx.BlockTime()) { - return sdk.ErrInternal("proposal expired") + return types.ErrProposalExpired(k.codespace, ctx.BlockTime(), pr.Deadline) } com, found := k.GetCommittee(ctx, pr.CommitteeID) if !found { - return sdk.ErrInternal("committee disbanded") + return types.ErrUnknownCommittee(k.codespace, pr.CommitteeID) } if !com.HasMember(voter) { - return sdk.ErrInternal("not authorized to vote on proposal") + return sdk.ErrUnauthorized("voter must be a member of committee") } // Store vote, overwriting any prior vote @@ -81,11 +81,11 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64) (bool, sdk.Error) { pr, found := k.GetProposal(ctx, proposalID) if !found { - return false, sdk.ErrInternal("proposal not found") + return false, types.ErrUnknownProposal(k.codespace, proposalID) } com, found := k.GetCommittee(ctx, pr.CommitteeID) if !found { - return false, sdk.ErrInternal("committee disbanded") + return false, types.ErrUnknownCommittee(k.codespace, pr.CommitteeID) } numVotes := k.TallyVotes(ctx, proposalID) @@ -111,7 +111,7 @@ func (k Keeper) TallyVotes(ctx sdk.Context, proposalID uint64) int64 { func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) sdk.Error { pr, found := k.GetProposal(ctx, proposalID) if !found { - return sdk.ErrInternal("proposal not found") + return types.ErrUnknownProposal(k.codespace, proposalID) } // Run the proposal's changes through the associated handler, but using a cached version of state to ensure changes are not permanent if an error occurs. @@ -128,14 +128,14 @@ func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) sdk.Error { // ValidatePubProposal checks if a pubproposal is valid. func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error { if pubProposal == nil { - return sdk.ErrInternal("proposal is empty") + return types.ErrInvalidPubProposal(k.codespace, "pub proposal cannot be nil") } if err := pubProposal.ValidateBasic(); err != nil { return err } if !k.router.HasRoute(pubProposal.ProposalRoute()) { - return sdk.ErrInternal("no handler found for proposal") + return types.ErrNoProposalHandlerExists(k.codespace, pubProposal) } // Run the proposal's changes through the associated handler using a cached version of state to ensure changes are not permanent. diff --git a/x/committee/keeper/querier.go b/x/committee/keeper/querier.go index c378d750..54ede8b6 100644 --- a/x/committee/keeper/querier.go +++ b/x/committee/keeper/querier.go @@ -29,8 +29,6 @@ func NewQuerier(keeper Keeper) sdk.Querier { return queryVote(ctx, path[1:], req, keeper) case types.QueryTally: return queryTally(ctx, path[1:], req, keeper) - // case types.QueryParams: - // return queryParams(ctx, path[1:], req, keeper) default: return nil, sdk.ErrUnknownRequest(fmt.Sprintf("unknown %s query endpoint", types.ModuleName)) @@ -64,7 +62,7 @@ func queryCommittee(ctx sdk.Context, path []string, req abci.RequestQuery, keepe committee, found := keeper.GetCommittee(ctx, params.CommitteeID) if !found { - return nil, sdk.ErrInternal("not found") ///types.ErrUnknownProposal(types.DefaultCodespace, params.ProposalID) + return nil, types.ErrUnknownCommittee(types.DefaultCodespace, params.CommitteeID) } bz, err := codec.MarshalJSONIndent(keeper.cdc, committee) @@ -107,7 +105,7 @@ func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper proposal, found := keeper.GetProposal(ctx, params.ProposalID) if !found { - return nil, sdk.ErrInternal("not found") // TODO types.ErrUnknownProposal(types.DefaultCodespace, params.ProposalID) + return nil, types.ErrUnknownProposal(types.DefaultCodespace, params.ProposalID) } bz, err := codec.MarshalJSONIndent(keeper.cdc, proposal) @@ -149,7 +147,7 @@ func queryVote(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Kee vote, found := keeper.GetVote(ctx, params.ProposalID, params.Voter) if !found { - return nil, sdk.ErrInternal("not found") + return nil, types.ErrUnknownVote(types.DefaultCodespace, params.ProposalID, params.Voter) } bz, err := codec.MarshalJSONIndent(keeper.cdc, vote) @@ -170,7 +168,7 @@ func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Ke _, found := keeper.GetProposal(ctx, params.ProposalID) if !found { - return nil, sdk.ErrInternal("proposal not found") + return nil, types.ErrUnknownProposal(types.DefaultCodespace, params.ProposalID) } numVotes := keeper.TallyVotes(ctx, params.ProposalID) @@ -180,30 +178,3 @@ func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Ke } return bz, nil } - -// ---------- Params ---------- - -// func queryParams(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { -// switch path[0] { -// case types.ParamDeposit: -// bz, err := codec.MarshalJSONIndent(keeper.cdc, keeper.GetDepositParams(ctx)) -// if err != nil { -// return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) -// } -// return bz, nil -// case types.ParamVoting: -// bz, err := codec.MarshalJSONIndent(keeper.cdc, keeper.GetVotingParams(ctx)) -// if err != nil { -// return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) -// } -// return bz, nil -// case types.ParamTallying: -// bz, err := codec.MarshalJSONIndent(keeper.cdc, keeper.GetTallyParams(ctx)) -// if err != nil { -// return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) -// } -// return bz, nil -// default: -// return nil, sdk.ErrUnknownRequest(fmt.Sprintf("%s is not a valid query request path", req.Path)) -// } -// } diff --git a/x/committee/proposal_handler.go b/x/committee/proposal_handler.go index e9d1eef8..e48ada2c 100644 --- a/x/committee/proposal_handler.go +++ b/x/committee/proposal_handler.go @@ -24,7 +24,7 @@ func NewProposalHandler(k Keeper) govtypes.Handler { func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeChangeProposal) sdk.Error { if err := committeeProposal.ValidateBasic(); err != nil { - return sdk.ErrInternal(err.Error()) + return ErrInvalidCommittee(DefaultCodespace, err.Error()) } // Remove all committee's ongoing proposals @@ -46,7 +46,7 @@ func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeDeleteProposal) sdk.Error { if err := committeeProposal.ValidateBasic(); err != nil { - return sdk.ErrInternal(err.Error()) + return ErrInvalidPubProposal(DefaultCodespace, err.Error()) } // Remove all committee's ongoing proposals diff --git a/x/committee/types/errors.go b/x/committee/types/errors.go index 57ccf762..0fa66126 100644 --- a/x/committee/types/errors.go +++ b/x/committee/types/errors.go @@ -1,9 +1,50 @@ package types import ( + "fmt" + "time" + sdk "github.com/cosmos/cosmos-sdk/types" ) const ( DefaultCodespace sdk.CodespaceType = ModuleName + + CodeProposalExpired sdk.CodeType = 1 + CodeUnknownItem sdk.CodeType = 2 + CodeInvalidGenesis sdk.CodeType = 3 + CodeInvalidProposal sdk.CodeType = 4 + CodeInvalidCommittee sdk.CodeType = 5 ) + +func ErrUnknownCommittee(codespace sdk.CodespaceType, id uint64) sdk.Error { + return sdk.NewError(codespace, CodeUnknownItem, fmt.Sprintf("committee with id '%d' not found", id)) +} + +func ErrInvalidCommittee(codespace sdk.CodespaceType, msg string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidCommittee, msg) +} + +func ErrUnknownProposal(codespace sdk.CodespaceType, id uint64) sdk.Error { + return sdk.NewError(codespace, CodeUnknownItem, fmt.Sprintf("proposal with id '%d' not found", id)) +} + +func ErrProposalExpired(codespace sdk.CodespaceType, blockTime, expiry time.Time) sdk.Error { + return sdk.NewError(codespace, CodeProposalExpired, fmt.Sprintf("proposal expired at %s, current blocktime %s", expiry, blockTime)) +} + +func ErrInvalidPubProposal(codespace sdk.CodespaceType, msg string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidProposal, msg) +} + +func ErrUnknownVote(codespace sdk.CodespaceType, proposalID uint64, voter sdk.AccAddress) sdk.Error { + return sdk.NewError(codespace, CodeUnknownItem, fmt.Sprintf("vote with for proposal '%d' and voter %s not found", proposalID, voter)) +} + +func ErrInvalidGenesis(codespace sdk.CodespaceType, msg string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidGenesis, msg) +} + +func ErrNoProposalHandlerExists(codespace sdk.CodespaceType, content interface{}) sdk.Error { + return sdk.NewError(codespace, CodeUnknownItem, fmt.Sprintf("'%T' does not have a corresponding handler", content)) +} diff --git a/x/committee/types/gov_proposal.go b/x/committee/types/gov_proposal.go index b5938ad3..3609b90a 100644 --- a/x/committee/types/gov_proposal.go +++ b/x/committee/types/gov_proposal.go @@ -53,7 +53,7 @@ func (ccp CommitteeChangeProposal) ValidateBasic() sdk.Error { return err } if err := ccp.NewCommittee.Validate(); err != nil { - return sdk.ErrInternal(err.Error()) + return ErrInvalidCommittee(DefaultCodespace, err.Error()) } return nil } diff --git a/x/committee/types/msg.go b/x/committee/types/msg.go index 273a4654..173d9564 100644 --- a/x/committee/types/msg.go +++ b/x/committee/types/msg.go @@ -36,7 +36,7 @@ func (msg MsgSubmitProposal) Type() string { return TypeMsgSubmitProposal } // ValidateBasic does a simple validation check that doesn't require access to any other information. func (msg MsgSubmitProposal) ValidateBasic() sdk.Error { if msg.PubProposal == nil { - return sdk.ErrInternal("no proposal") + return ErrInvalidPubProposal(DefaultCodespace, "pub proposal cannot be nil") } if msg.Proposer.Empty() { return sdk.ErrInvalidAddress(msg.Proposer.String()) From 058e3981c5cfb91c0fb5861018a262ef5a17d2a9 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sun, 29 Mar 2020 20:50:32 +0100 Subject: [PATCH 30/54] add simulation stubs to make tests pass --- x/committee/module.go | 40 ++++++++++++++++--------------- x/committee/simulation/decoder.go | 12 ++++++++++ x/committee/simulation/genesis.go | 22 +++++++++++++++++ x/committee/simulation/params.go | 14 +++++++++++ 4 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 x/committee/simulation/decoder.go create mode 100644 x/committee/simulation/genesis.go create mode 100644 x/committee/simulation/params.go diff --git a/x/committee/module.go b/x/committee/module.go index 12e95b56..f39e8c44 100644 --- a/x/committee/module.go +++ b/x/committee/module.go @@ -2,6 +2,7 @@ package committee import ( "encoding/json" + "math/rand" "github.com/gorilla/mux" "github.com/spf13/cobra" @@ -10,16 +11,18 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + sim "github.com/cosmos/cosmos-sdk/x/simulation" abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/x/committee/client/cli" "github.com/kava-labs/kava/x/committee/client/rest" + "github.com/kava-labs/kava/x/committee/simulation" ) var ( - _ module.AppModule = AppModule{} - _ module.AppModuleBasic = AppModuleBasic{} - // TODO_ module.AppModuleSimulation = AppModuleSimulation{} + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + _ module.AppModuleSimulation = AppModuleSimulation{} ) // AppModuleBasic app module basics object @@ -67,31 +70,30 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { //____________________________________________________________________________ -// TODO -// // AppModuleSimulation defines the module simulation functions used by the module. -// type AppModuleSimulation struct{} +// AppModuleSimulation defines the module simulation functions used by the module. +type AppModuleSimulation struct{} -// // RegisterStoreDecoder registers a decoder for the module's types -// func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { -// sdr[StoreKey] = simulation.DecodeStore -// } +// RegisterStoreDecoder registers a decoder for the module's types +func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[StoreKey] = simulation.DecodeStore +} -// // GenerateGenesisState creates a randomized GenState of the module -// func (AppModuleSimulation) GenerateGenesisState(simState *module.SimulationState) { -// simulation.RandomizedGenState(simState) -// } +// GenerateGenesisState creates a randomized GenState of the module +func (AppModuleSimulation) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} -// // RandomizedParams creates randomized param changes for the simulator. -// func (AppModuleSimulation) RandomizedParams(r *rand.Rand) []sim.ParamChange { -// return simulation.ParamChanges(r) -// } +// RandomizedParams creates randomized param changes for the simulator. +func (AppModuleSimulation) RandomizedParams(r *rand.Rand) []sim.ParamChange { + return simulation.ParamChanges(r) +} //____________________________________________________________________________ // AppModule app module type type AppModule struct { AppModuleBasic - // TODO AppModuleSimulation + AppModuleSimulation keeper Keeper } diff --git a/x/committee/simulation/decoder.go b/x/committee/simulation/decoder.go new file mode 100644 index 00000000..2cdd439b --- /dev/null +++ b/x/committee/simulation/decoder.go @@ -0,0 +1,12 @@ +package simulation + +import ( + "github.com/cosmos/cosmos-sdk/codec" + cmn "github.com/tendermint/tendermint/libs/common" +) + +// DecodeStore unmarshals the KVPair's Value to the corresponding module type +func DecodeStore(cdc *codec.Codec, kvA, kvB cmn.KVPair) string { + // TODO implement this + return "" +} diff --git a/x/committee/simulation/genesis.go b/x/committee/simulation/genesis.go new file mode 100644 index 00000000..5f11ea53 --- /dev/null +++ b/x/committee/simulation/genesis.go @@ -0,0 +1,22 @@ +package simulation + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/kava-labs/kava/x/auction/types" +) + +// RandomizedGenState generates a random GenesisState for the module +func RandomizedGenState(simState *module.SimulationState) { + + // TODO implement this fully + // - randomly generating the genesis params + // - overwriting with genesis provided to simulation + genesisState := types.DefaultGenesisState() + + fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, genesisState)) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesisState) +} diff --git a/x/committee/simulation/params.go b/x/committee/simulation/params.go new file mode 100644 index 00000000..8c1f7aff --- /dev/null +++ b/x/committee/simulation/params.go @@ -0,0 +1,14 @@ +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{} +} From ace9a2363e532dc1873c3975f2edcb1c6113cacf Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sun, 29 Mar 2020 21:05:08 +0100 Subject: [PATCH 31/54] address TODOs --- x/committee/abci_test.go | 2 +- x/committee/client/cli/query.go | 5 ++--- x/committee/client/common/query_proposer.go | 4 +++- x/committee/client/rest/query.go | 25 --------------------- x/committee/types/gov_proposal.go | 4 ++-- x/committee/types/permissions.go | 1 - x/committee/types/types.go | 15 +++---------- 7 files changed, 11 insertions(+), 45 deletions(-) diff --git a/x/committee/abci_test.go b/x/committee/abci_test.go index adfa501d..f990dd43 100644 --- a/x/committee/abci_test.go +++ b/x/committee/abci_test.go @@ -35,7 +35,7 @@ func (suite *ModuleTestSuite) SetupTest() { func (suite *ModuleTestSuite) TestBeginBlock() { suite.app.InitializeFromGenesisStates() - // TODO replace below with genesis state + normalCom := committee.Committee{ ID: 12, Members: suite.addresses[:2], diff --git a/x/committee/client/cli/query.go b/x/committee/client/cli/query.go index abcf6865..12add284 100644 --- a/x/committee/client/cli/query.go +++ b/x/committee/client/cli/query.go @@ -27,14 +27,13 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { } govQueryCmd.AddCommand(client.GetCommands( - // GetCmdQueryCommittee(queryRoute, cdc), // TODO is this needed? GetCmdQueryCommittees(queryRoute, cdc), GetCmdQueryProposal(queryRoute, cdc), GetCmdQueryProposals(queryRoute, cdc), GetCmdQueryVotes(queryRoute, cdc), - //TODO GetCmdQueryParams(queryRoute, cdc), + GetCmdQueryProposer(queryRoute, cdc), GetCmdQueryTally(queryRoute, cdc))...) @@ -167,7 +166,7 @@ func GetCmdQueryVotes(queryRoute string, cdc *codec.Codec) *cobra.Command { } // Decode and print results - votes := []types.Vote{} // using empty (not nil) slice so json returns [] instead of null when there's no data // TODO check + votes := []types.Vote{} // using empty (not nil) slice so json returns [] instead of null when there's no data err = cdc.UnmarshalJSON(res, &votes) if err != nil { return err diff --git a/x/committee/client/common/query_proposer.go b/x/committee/client/common/query_proposer.go index 5a44be42..d6f12a56 100644 --- a/x/committee/client/common/query_proposer.go +++ b/x/committee/client/common/query_proposer.go @@ -10,9 +10,11 @@ import ( "github.com/kava-labs/kava/x/committee/types" ) +// Note: QueryProposer is copied in from the gov module + const ( defaultPage = 1 - defaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19 // TODO what is this? + defaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19 ) // Proposer contains metadata of a governance proposal used for querying a proposer. diff --git a/x/committee/client/rest/query.go b/x/committee/client/rest/query.go index 07ec7478..75034884 100644 --- a/x/committee/client/rest/query.go +++ b/x/committee/client/rest/query.go @@ -22,7 +22,6 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/proposer", types.ModuleName, RestProposalID), queryProposerHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/tally", types.ModuleName, RestProposalID), queryTallyOnProposalHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes", types.ModuleName, RestProposalID), queryVotesOnProposalHandlerFn(cliCtx)).Methods("GET") - // TODO r.HandleFunc(fmt.Sprintf("/%s/parameters/{%s}", types.ModuleName, RestParamsType), queryParamsHandlerFn(cliCtx)).Methods("GET") } // ---------- Committees ---------- @@ -289,27 +288,3 @@ func queryTallyOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { rest.PostProcessResponse(w, cliCtx, res) } } - -// ---------- Params ---------- - -// TODO -// func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { -// return func(w http.ResponseWriter, r *http.Request) { -// vars := mux.Vars(r) -// paramType := vars[RestParamsType] - -// cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) -// if !ok { -// return -// } - -// res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/gov/%s/%s", types.QueryParams, paramType), nil) -// if err != nil { -// rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) -// return -// } - -// cliCtx = cliCtx.WithHeight(height) -// rest.PostProcessResponse(w, cliCtx, res) -// } -// } diff --git a/x/committee/types/gov_proposal.go b/x/committee/types/gov_proposal.go index 3609b90a..aa201660 100644 --- a/x/committee/types/gov_proposal.go +++ b/x/committee/types/gov_proposal.go @@ -60,7 +60,7 @@ func (ccp CommitteeChangeProposal) ValidateBasic() sdk.Error { // String implements the Stringer interface. func (ccp CommitteeChangeProposal) String() string { - bz, _ := yaml.Marshal(ccp) // TODO test + bz, _ := yaml.Marshal(ccp) return string(bz) } @@ -109,6 +109,6 @@ func (cdp CommitteeDeleteProposal) ValidateBasic() sdk.Error { // String implements the Stringer interface. func (cdp CommitteeDeleteProposal) String() string { - bz, _ := yaml.Marshal(cdp) // TODO test + bz, _ := yaml.Marshal(cdp) return string(bz) } diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index f3ae5640..2481587e 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -16,7 +16,6 @@ func init() { } // GodPermission allows any governance proposal. It is used mainly for testing. -// TODO better name? type GodPermission struct{} var _ Permission = GodPermission{} diff --git a/x/committee/types/types.go b/x/committee/types/types.go index 4665a044..a575848e 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -2,11 +2,11 @@ package types import ( "fmt" - "strings" "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov" + "gopkg.in/yaml.v2" ) const MaxCommitteeDescriptionLength int = 5000 @@ -114,17 +114,8 @@ func (p Proposal) HasExpiredBy(time time.Time) bool { // String implements the fmt.Stringer interface, and importantly overrides the String methods inherited from the embedded PubProposal type. func (p Proposal) String() string { - return strings.TrimSpace(fmt.Sprintf(`Proposal: - PubProposal: -%s - ID: %d - Committee ID: %d - Deadline: %s`, - p.PubProposal, - p.ID, - p.CommitteeID, - p.Deadline, - )) + bz, _ := yaml.Marshal(p) + return string(bz) } type Vote struct { From 4298564096855d06bd93856ed98c86bc7c506636 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 30 Mar 2020 14:06:31 +0100 Subject: [PATCH 32/54] address minor TODOs --- x/committee/client/cli/tx.go | 2 -- x/committee/client/rest/query.go | 33 ++++++++--------------------- x/committee/client/rest/tx.go | 5 ++--- x/committee/keeper/keeper.go | 12 ++++++++--- x/committee/keeper/proposal_test.go | 24 ++++++++++++++++----- x/committee/keeper/querier.go | 16 ++++++++++---- x/committee/types/msg.go | 8 ++----- x/committee/types/permissions.go | 2 +- x/committee/types/types.go | 14 +++++++++--- 9 files changed, 65 insertions(+), 51 deletions(-) diff --git a/x/committee/client/cli/tx.go b/x/committee/client/cli/tx.go index 2261a9c1..2e142e8d 100644 --- a/x/committee/client/cli/tx.go +++ b/x/committee/client/cli/tx.go @@ -126,8 +126,6 @@ func GetCmdVote(cdc *codec.Codec) *cobra.Command { } } -// TODO This could replace the whole gov submit-proposal cmd. It would align how it works with how submiting proposal to committees works. -// Requires removing and replacing the gov cmd in kvcli main.go // GetGovCmdSubmitProposal returns a command to submit a proposal to the gov module. It is passed to the gov module for use on its command subtree. func GetGovCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ diff --git a/x/committee/client/rest/query.go b/x/committee/client/rest/query.go index 75034884..da80ec5c 100644 --- a/x/committee/client/rest/query.go +++ b/x/committee/client/rest/query.go @@ -24,7 +24,9 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes", types.ModuleName, RestProposalID), queryVotesOnProposalHandlerFn(cliCtx)).Methods("GET") } -// ---------- Committees ---------- +// ------------------------------------------ +// Committees +// ------------------------------------------ func queryCommitteesHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -85,7 +87,9 @@ func queryCommitteeHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } -// ---------- Proposals ---------- +// ------------------------------------------ +// Proposals +// ------------------------------------------ func queryProposalsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -190,7 +194,9 @@ func queryProposerHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } -// ---------- Votes ---------- +// ------------------------------------------ +// Votes +// ------------------------------------------ func queryVotesOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -224,27 +230,6 @@ func queryVotesOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return } - // TODO should add this feature back - // var proposal types.Proposal - // if err := cliCtx.Codec.UnmarshalJSON(res, &proposal); err != nil { - // rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) - // return - // } - - // // For inactive proposals we must query the txs directly to get the votes - // // as they're no longer in state. - // propStatus := proposal.Status - // if !(propStatus == types.StatusVotingPeriod || propStatus == types.StatusDepositPeriod) { - // res, err = gcutils.QueryVotesByTxQuery(cliCtx, params) - // } else { - // res, _, err = cliCtx.QueryWithData("custom/gov/votes", bz) - // } - - // if err != nil { - // rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) - // return - // } - // Write response cliCtx = cliCtx.WithHeight(height) rest.PostProcessResponse(w, cliCtx, res) diff --git a/x/committee/client/rest/tx.go b/x/committee/client/rest/tx.go index 01e1b7c4..46447123 100644 --- a/x/committee/client/rest/tx.go +++ b/x/committee/client/rest/tx.go @@ -106,11 +106,10 @@ func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } -// -------- -------- -// TODO this could replace the POST gov/proposals endpoint, would need to overwrite routes in kvcli main, hacky +// This is a rest handler for for the gov module, that handles committee change/delete proposals. type PostGovProposalReq struct { BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` - Content govtypes.Content `json:"content" yaml:"content"` //TODO use same PubProposal name? + Content govtypes.Content `json:"content" yaml:"content"` Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` Deposit sdk.Coins `json:"deposit" yaml:"deposit"` } diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 327b653a..6b2f9f7c 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -33,7 +33,9 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router, } } -// ---------- Committees ---------- +// ------------------------------------------ +// Committees +// ------------------------------------------ // GetCommittee gets a committee from the store. func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committee, bool) { @@ -76,7 +78,9 @@ func (k Keeper) IterateCommittees(ctx sdk.Context, cb func(committee types.Commi } } -// ---------- Proposals ---------- +// ------------------------------------------ +// Proposals +// ------------------------------------------ // SetNextProposalID stores an ID to be used for the next created proposal func (k Keeper) SetNextProposalID(ctx sdk.Context, id uint64) { @@ -167,7 +171,9 @@ func (k Keeper) IterateProposals(ctx sdk.Context, cb func(proposal types.Proposa } } -// ---------- Votes ---------- +// ------------------------------------------ +// Votes +// ------------------------------------------ // GetVote gets a vote from the store. func (k Keeper) GetVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) (types.Vote, bool) { diff --git a/x/committee/keeper/proposal_test.go b/x/committee/keeper/proposal_test.go index 6bf59141..2ff91cdc 100644 --- a/x/committee/keeper/proposal_test.go +++ b/x/committee/keeper/proposal_test.go @@ -7,6 +7,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/params" abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" @@ -78,8 +79,7 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { for _, tc := range testcases { suite.Run(tc.name, func() { - // Create local testApp because suite doesn't run the SetupTest function for subtests, - // which would mean the app state is not be reset between subtests. + // Create local testApp because suite doesn't run the SetupTest function for subtests tApp := app.NewTestApp() keeper := tApp.GetCommitteeKeeper() ctx := tApp.NewContext(true, abci.Header{}) @@ -148,7 +148,7 @@ func (suite *KeeperTestSuite) TestAddVote() { for _, tc := range testcases { suite.Run(tc.name, func() { - // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. + // Create local testApp because suite doesn't run the SetupTest function for subtests tApp := app.NewTestApp() keeper := tApp.GetCommitteeKeeper() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime}) @@ -217,7 +217,7 @@ func (suite *KeeperTestSuite) TestGetProposalResult() { for _, tc := range testcases { suite.Run(tc.name, func() { - // Create local testApp because suite doesn't run the SetupTest function for subtests, which would mean the app state is not be reset between subtests. + // Create local testApp because suite doesn't run the SetupTest function for subtests tApp := app.NewTestApp() keeper := tApp.GetCommitteeKeeper() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime}) @@ -294,7 +294,21 @@ func (suite *KeeperTestSuite) TestValidatePubProposal() { pubProposal: nil, expectPass: false, }, - // TODO test case when the handler fails + { + name: "invalid (proposal handler fails)", + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{{ + Subspace: "non existant", + Key: "non existant", + Value: "nonsense", + }}, + ), + expectPass: false, + }, + // Some proposals can cause the proposal handler to panic. + // However panics will be caught when the proposal is first submitted so should avoid making it onto the chain. } for _, tc := range testcases { diff --git a/x/committee/keeper/querier.go b/x/committee/keeper/querier.go index 54ede8b6..5981f945 100644 --- a/x/committee/keeper/querier.go +++ b/x/committee/keeper/querier.go @@ -36,7 +36,9 @@ func NewQuerier(keeper Keeper) sdk.Querier { } } -// ---------- Committees ---------- +// ------------------------------------------ +// Committees +// ------------------------------------------ func queryCommittees(ctx sdk.Context, path []string, _ abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { @@ -72,7 +74,9 @@ func queryCommittee(ctx sdk.Context, path []string, req abci.RequestQuery, keepe return bz, nil } -// ---------- Proposals ---------- +// ------------------------------------------ +// Proposals +// ------------------------------------------ func queryProposals(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { var params types.QueryCommitteeParams @@ -115,7 +119,9 @@ func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper return bz, nil } -// ---------- Votes ---------- +// ------------------------------------------ +// Votes +// ------------------------------------------ func queryVotes(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { var params types.QueryProposalParams @@ -157,7 +163,9 @@ func queryVote(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Kee return bz, nil } -// ---------- Tally ---------- +// ------------------------------------------ +// Tally +// ------------------------------------------ func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { var params types.QueryProposalParams diff --git a/x/committee/types/msg.go b/x/committee/types/msg.go index 173d9564..5e2b138b 100644 --- a/x/committee/types/msg.go +++ b/x/committee/types/msg.go @@ -5,8 +5,8 @@ import ( ) const ( - TypeMsgSubmitProposal = "submit_proposal" // TODO these are the same as the gov module, will there be collisions? - TypeMsgVote = "vote" + TypeMsgSubmitProposal = "commmittee_submit_proposal" // 'committee' prefix appended to avoid potential conflicts with gov msg types + TypeMsgVote = "committee_vote" ) var _, _ sdk.Msg = MsgSubmitProposal{}, MsgVote{} @@ -41,10 +41,6 @@ func (msg MsgSubmitProposal) ValidateBasic() sdk.Error { if msg.Proposer.Empty() { return sdk.ErrInvalidAddress(msg.Proposer.String()) } - // TODO - // if !IsValidProposalType(msg.Content.ProposalType()) { - // return ErrInvalidProposalType(DefaultCodespace, msg.Content.ProposalType()) - // } return msg.PubProposal.ValidateBasic() } diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index 2481587e..28a1d534 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -110,6 +110,6 @@ func (perm ShutdownPermission) MarshalYAML() (interface{}, error) { } // TODO add more permissions? -// - limit parameter changes to bew withing small ranges or fixed sets +// - limit parameter changes to be within small ranges // - allow community spend proposals // - allow committee change proposals diff --git a/x/committee/types/types.go b/x/committee/types/types.go index a575848e..b5e46211 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -11,7 +11,9 @@ import ( const MaxCommitteeDescriptionLength int = 5000 -// -------- Committees -------- +// ------------------------------------------ +// Committees +// ------------------------------------------ // A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. type Committee struct { @@ -94,10 +96,12 @@ type Permission interface { Allows(PubProposal) bool } -// -------- Proposals -------- +// ------------------------------------------ +// Proposals +// ------------------------------------------ // PubProposal is an interface that all gov proposals defined in other modules must satisfy. -type PubProposal = gov.Content // TODO find a better name +type PubProposal = gov.Content type Proposal struct { PubProposal `json:"pub_proposal" yaml:"pub_proposal"` @@ -118,6 +122,10 @@ func (p Proposal) String() string { return string(bz) } +// ------------------------------------------ +// Votes +// ------------------------------------------ + type Vote struct { ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` Voter sdk.AccAddress `json:"voter" yaml:"voter"` From 98a044d7af156da2c33365f2fab902a2d42b6492 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 30 Mar 2020 14:18:02 +0100 Subject: [PATCH 33/54] remove shutdown module --- app/app.go | 18 ---------- x/shutdown/ante/ante.go | 35 ------------------- x/shutdown/keeper/keeper.go | 19 ----------- x/shutdown/proposal_handler.go | 30 ---------------- x/shutdown/spec/README.md | 18 ---------- x/shutdown/types/types.go | 62 ---------------------------------- 6 files changed, 182 deletions(-) delete mode 100644 x/shutdown/ante/ante.go delete mode 100644 x/shutdown/keeper/keeper.go delete mode 100644 x/shutdown/proposal_handler.go delete mode 100644 x/shutdown/spec/README.md delete mode 100644 x/shutdown/types/types.go diff --git a/app/app.go b/app/app.go index 18ebdb7f..d1da18b4 100644 --- a/app/app.go +++ b/app/app.go @@ -337,7 +337,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // initialize the app app.SetInitChainer(app.InitChainer) app.SetBeginBlocker(app.BeginBlocker) - // TODO app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.supplyKeeper, app.shutdownKeeper, auth.DefaultSigVerificationGasConsumer)) app.SetAnteHandler(auth.NewAnteHandler(app.accountKeeper, app.supplyKeeper, auth.DefaultSigVerificationGasConsumer)) app.SetEndBlocker(app.EndBlocker) @@ -352,23 +351,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, return app } -// func NewAnteHandler(ak auth.AccountKeeper, supplyKeeper supply.Keeper, shutdownKeeper shutdown.Keeper, sigGasConsumer SignatureVerificationGasConsumer) sdk.AnteHandler { -// return sdk.ChainAnteDecorators( -// auth.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first -// shutdownAnte.NewDisableMsgDecorator(shutdownKeeper), -// auth.NewMempoolFeeDecorator(), -// auth.NewValidateBasicDecorator(), -// auth.NewValidateMemoDecorator(ak), -// auth.NewConsumeGasForTxSizeDecorator(ak), -// auth.NewSetPubKeyDecorator(ak), // SetPubKeyDecorator must be called before all signature verification decorators -// auth.NewValidateSigCountDecorator(ak), -// auth.NewDeductFeeDecorator(ak, supplyKeeper), -// auth.NewSigGasConsumeDecorator(ak, sigGasConsumer), -// auth.NewSigVerificationDecorator(ak), -// auth.NewIncrementSequenceDecorator(ak), // innermost AnteDecorator -// ) -// } - // custom tx codec func MakeCodec() *codec.Codec { var cdc = codec.New() diff --git a/x/shutdown/ante/ante.go b/x/shutdown/ante/ante.go deleted file mode 100644 index a19da955..00000000 --- a/x/shutdown/ante/ante.go +++ /dev/null @@ -1,35 +0,0 @@ -package ante - -import ( - "fmt" - - "github.com/kava-labs/kava/x/shutdown/keeper" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// DisableMsgDecorator errors if a tx contains a disallowed msg type and calls the next AnteHandler if all msgs are allowed -type DisableMsgDecorator struct { - shutdownKeeper keeper.Keeper -} - -func NewDisableMsgDecorator(shutdownKeeper keeper.Keeper) DisableMsgDecorator { - return DisableMsgDecorator{ - shutdownKeeper: shutdownKeeper, - } -} - -func (dmd DisableMsgDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { - - // get msg route, error if not allowed - disallowedRoutes := dmd.shutdownKeeper.GetMsgRoutes(ctx) - for _, m := range tx.GetMsgs() { - for _, r := range disallowedRoutes { - if r.Route == m.Route() && r.Msg == m.Type() { - return ctx, fmt.Errorf("route %s has been disabled, tx rejected", r) - } - } - } - // otherwise continue to next antehandler decorator - return next(ctx, tx, simulate) -} diff --git a/x/shutdown/keeper/keeper.go b/x/shutdown/keeper/keeper.go deleted file mode 100644 index 50118606..00000000 --- a/x/shutdown/keeper/keeper.go +++ /dev/null @@ -1,19 +0,0 @@ -package keeper - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/kava-labs/kava/x/shutdown/types" -) - -// Keeper stores routes that have been "broken" -type Keeper struct { -} - -func (k Keeper) GetMsgRoutes(ctx sdk.Context) []types.MsgRoute { - // TODO - return []types.MsgRoute{} -} - -func (k Keeper) SetMsgRoutes(ctx sdk.Context, routes []types.MsgRoute) { - // TODO -} diff --git a/x/shutdown/proposal_handler.go b/x/shutdown/proposal_handler.go deleted file mode 100644 index 97d2a251..00000000 --- a/x/shutdown/proposal_handler.go +++ /dev/null @@ -1,30 +0,0 @@ -package shutdown - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/gov" - - "github.com/kava-labs/kava/x/shutdown/keeper" - "github.com/kava-labs/kava/x/shutdown/types" -) - -func NewShutdownProposalHandler(k keeper.Keeper) gov.Handler { - return func(ctx sdk.Context, content gov.Content) sdk.Error { - switch c := content.(type) { - case types.ShutdownProposal: - return handleShutdownProposal(ctx, k, c) - - default: - errMsg := fmt.Sprintf("unrecognized %s proposal content type: %T", types.ModuleName, c) - return sdk.ErrUnknownRequest(errMsg) - } - } -} - -func handleShutdownProposal(ctx sdk.Context, k keeper.Keeper, c types.ShutdownProposal) sdk.Error { - // TODO validate proposal - k.SetMsgRoutes(ctx, c.MsgRoutes) - return nil -} diff --git a/x/shutdown/spec/README.md b/x/shutdown/spec/README.md deleted file mode 100644 index 2bfe0107..00000000 --- a/x/shutdown/spec/README.md +++ /dev/null @@ -1,18 +0,0 @@ - -# `shutdown` - -## Table of Contents - -## Overview - -The `x/shutdown` module allows certain message types to be disabled based on governance votes. - -Msgs and routes are disabled via an antehandler decorator. The decorator checks incoming all txs and rejects them if they contain a disallowed msg type. -Disallowed msg types are stored in a circuit breaker keeper. - -The list of disallowed msg types is updated via a custom governance proposal and handler. - -Design Alternatives: - -- store list of disallowed msg types in params, then we don't need the custom gov proposal -- replace the app Router with a custom one to avoid using the antehandler - can't be done with current baseapp, but v0.38.x enables this. (https://github.com/cosmos/cosmos-sdk/issues/5455) \ No newline at end of file diff --git a/x/shutdown/types/types.go b/x/shutdown/types/types.go deleted file mode 100644 index 110b5349..00000000 --- a/x/shutdown/types/types.go +++ /dev/null @@ -1,62 +0,0 @@ -package types - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" -) - -type MsgRoute struct { - Route string - Msg string // how best to store a Msg type? -} - -const ( - ProposalTypeShutdown = "Shutdown" -) - -// Assert ShutdownProposal implements govtypes.Content at compile-time -var _ govtypes.Content = ShutdownProposal{} - -type ShutdownProposal struct { - Title string - Description string - MsgRoutes []MsgRoute -} - -// GetTitle returns the title of a community pool spend proposal. -func (sp ShutdownProposal) GetTitle() string { return sp.Title } - -// GetDescription returns the description of a community pool spend proposal. -func (sp ShutdownProposal) GetDescription() string { return sp.Description } - -// GetDescription returns the routing key of a community pool spend proposal. -func (sp ShutdownProposal) ProposalRoute() string { return RouterKey } - -// ProposalType returns the type of a community pool spend proposal. -func (sp ShutdownProposal) ProposalType() string { return ProposalTypeShutdown } - -// ValidateBasic runs basic stateless validity checks -func (sp ShutdownProposal) ValidateBasic() sdk.Error { - err := govtypes.ValidateAbstract(DefaultCodespace, sp) - if err != nil { - return err - } - // TODO - return nil -} - -// String implements the Stringer interface. -func (sp ShutdownProposal) String() string { - // TODO - return "" -} - -const ( - DefaultCodespace sdk.CodespaceType = ModuleName - - // ModuleName is the module name constant used in many places - ModuleName = "shutdown" - - // RouterKey is the message route for distribution - RouterKey = ModuleName -) From 699ee59bd1c92ec17d2b4bf07b839ac5319bf7df Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 30 Mar 2020 14:38:57 +0100 Subject: [PATCH 34/54] move shutdown permission to own branch --- x/committee/types/codec.go | 2 +- x/committee/types/permissions.go | 37 ++------------------------------ 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/x/committee/types/codec.go b/x/committee/types/codec.go index 8da0a9d3..95c0e7f2 100644 --- a/x/committee/types/codec.go +++ b/x/committee/types/codec.go @@ -20,7 +20,7 @@ func init() { func RegisterModuleCodec(cdc *codec.Codec) { cdc.RegisterInterface((*gov.Content)(nil), nil) // registering the Content interface on the ModuleCdc will not conflict with gov. - // Ideally dist and params would register their proposals on here at their init. However can't change them so: + // Ideally dist and params would register their proposals on here at their init. However don't want to fork 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) diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index 28a1d534..fcb1c0a4 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -3,16 +3,14 @@ package types import ( "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/params" - sdtypes "github.com/kava-labs/kava/x/shutdown/types" ) func init() { - // Gov proposals need to be registered on gov's ModuleCdc. - // But since proposals contain Permissions, those types also need registering. + // CommitteeChange/Delete proposals need to be registered on gov's ModuleCdc. + // But since these proposals contain Permissions, these types also need registering: gov.ModuleCdc.RegisterInterface((*Permission)(nil), nil) gov.RegisterProposalTypeCodec(GodPermission{}, "kava/GodPermission") gov.RegisterProposalTypeCodec(ParamChangePermission{}, "kava/ParamChangePermission") - gov.RegisterProposalTypeCodec(ShutdownPermission{}, "kava/ShutdownPermission") } // GodPermission allows any governance proposal. It is used mainly for testing. @@ -78,37 +76,6 @@ func (allowed AllowedParams) Contains(paramChange params.ParamChange) bool { return false } -// ShutdownPermission allows certain message types to be disabled -type ShutdownPermission struct { - MsgRoute sdtypes.MsgRoute `json:"msg_route" yaml:"msg_route"` -} - -var _ Permission = ShutdownPermission{} - -func (perm ShutdownPermission) Allows(p gov.Content) bool { - proposal, ok := p.(sdtypes.ShutdownProposal) - if !ok { - return false - } - for _, r := range proposal.MsgRoutes { - if r == perm.MsgRoute { - return true - } - } - return false -} - -func (perm ShutdownPermission) MarshalYAML() (interface{}, error) { - valueToMarshal := struct { - Type string `yaml:"type"` - MsgRoute sdtypes.MsgRoute `yaml:"msg_route"` - }{ - Type: "shutdown_permission", - MsgRoute: perm.MsgRoute, - } - return valueToMarshal, nil -} - // TODO add more permissions? // - limit parameter changes to be within small ranges // - allow community spend proposals From 7407a38adb1d014a4f7ec6c03a9382519a52f6a2 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 30 Mar 2020 14:39:13 +0100 Subject: [PATCH 35/54] update spec overview --- x/committee/spec/README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/x/committee/spec/README.md b/x/committee/spec/README.md index f084da68..bcde3f69 100644 --- a/x/committee/spec/README.md +++ b/x/committee/spec/README.md @@ -7,18 +7,20 @@ The `x/committee` module is an additional governance module to `cosmos-sdk/x/gov`. -It allows groups of accounts to vote on and enact proposals, mainly to allow certain proposal types to be decided on quickly in emergency situations, or to delegate low risk parameter updates to a smaller group of individuals. +It allows groups of accounts to vote on and enact proposals without a full chain governance vote. Certain proposal types can then be decided on quickly in emergency situations, or low risk parameter updates can be delegated to a smaller group of individuals. + +Committees work with "proposals", using the same type from the `gov` module so they are compatible with all existing proposal types such as param changes, or community pool spend, or text proposals. Committees have members and permissions. Members vote on proposals, with just simple one vote per member, no deposits or slashing. More sophisticated voting could be added. -A permission acts as a filter for incoming gov proposals, rejecting them if they do not pass. A permission can be anything with a method `Allows(p Proposal) bool`. They reject all proposals that they don't explicitly allow. +Permissions scope the allowed set of proposals a committee can enact. For example: -This allows permissions to be parameterized to allow fine grained control specified at runtime. For example a generic parameter permission type can allow a group to only change a particular param, or only change params within a certain percentage. +- allow the committee to only change the cdp `CircuitBreaker` param. +- allow the committee to change auction bid increments, but only within the range [0, 0.1] +- allow the committee to only disable cdp msg types, but not staking or gov -Design Alternatives +A permission acts as a filter for incoming gov proposals, rejecting them if they do not pass. A permission can be any type with a method `Allows(p Proposal) bool`. They reject all proposals that they don't explicitly allow. -- Should this define its own gov types, or reuse those from gov module? -- Should we push changes to sdk gov to make it more general purpose? -- Could use params more instead of custom gov proposals +This allows permissions to be parameterized to allow fine grained control specified at runtime. For example a generic parameter permission type can allow a committee to only change a particular param, or only change params within a certain percentage. From 5dcbe73c623c14f399c423c817404fe17a317a59 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 30 Mar 2020 14:49:41 +0100 Subject: [PATCH 36/54] remove missed shutdown module references --- x/committee/alias.go | 1 - x/committee/types/codec.go | 1 - 2 files changed, 2 deletions(-) diff --git a/x/committee/alias.go b/x/committee/alias.go index baae2c18..996dddfc 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -101,6 +101,5 @@ type ( QueryCommitteeParams = types.QueryCommitteeParams QueryProposalParams = types.QueryProposalParams QueryVoteParams = types.QueryVoteParams - ShutdownPermission = types.ShutdownPermission Vote = types.Vote ) diff --git a/x/committee/types/codec.go b/x/committee/types/codec.go index 95c0e7f2..a904d40e 100644 --- a/x/committee/types/codec.go +++ b/x/committee/types/codec.go @@ -43,7 +43,6 @@ func RegisterAppCodec(cdc *codec.Codec) { cdc.RegisterInterface((*Permission)(nil), nil) cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil) cdc.RegisterConcrete(ParamChangePermission{}, "kava/ParamChangePermission", nil) - cdc.RegisterConcrete(ShutdownPermission{}, "kava/ShutdownPermission", nil) // Msgs cdc.RegisterConcrete(MsgSubmitProposal{}, "kava/MsgSubmitProposal", nil) From 73dc488239f4799954931edb19c148d3b21a9436 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Fri, 24 Apr 2020 19:15:57 +0100 Subject: [PATCH 37/54] Apply suggestions from code review Co-Authored-By: Denali Marsh Co-Authored-By: Kevin Davis --- x/committee/client/cli/query.go | 2 +- x/committee/client/cli/tx.go | 2 +- x/committee/client/rest/query.go | 5 +++-- x/committee/client/rest/tx.go | 4 ++-- x/committee/keeper/keeper_test.go | 4 ++-- x/committee/types/codec.go | 2 +- x/committee/types/gov_proposal.go | 4 ++-- x/committee/types/querier.go | 1 - 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/x/committee/client/cli/query.go b/x/committee/client/cli/query.go index 12add284..e0318fba 100644 --- a/x/committee/client/cli/query.go +++ b/x/committee/client/cli/query.go @@ -40,7 +40,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return govQueryCmd } -// GetCmdQueryProposals implements a query proposals command. +// GetCmdQueryCommittees implements a query committees command. func GetCmdQueryCommittees(queryRoute string, cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "committees", diff --git a/x/committee/client/cli/tx.go b/x/committee/client/cli/tx.go index 2e142e8d..292a8476 100644 --- a/x/committee/client/cli/tx.go +++ b/x/committee/client/cli/tx.go @@ -189,7 +189,7 @@ func mustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string { types.NewCommittee( 1, "The description of this committee.", - []sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddres")))}, + []sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddress")))}, []types.Permission{ types.ParamChangePermission{ AllowedParams: types.AllowedParams{{Subspace: "cdp", Key: "CircuitBreaker"}}, diff --git a/x/committee/client/rest/query.go b/x/committee/client/rest/query.go index da80ec5c..1ae72928 100644 --- a/x/committee/client/rest/query.go +++ b/x/committee/client/rest/query.go @@ -190,6 +190,7 @@ func queryProposerHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } // Write response + cliCtx = cliCtx.WithHeight(height) rest.PostProcessResponse(w, cliCtx, res) } } @@ -209,7 +210,7 @@ func queryVotesOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { // Prepare params for querier vars := mux.Vars(r) if len(vars[RestProposalID]) == 0 { - err := errors.New("proposalID required but not specified") + err := errors.New(fmt.Sprintf("%s required but not specified", RestProposalID)) rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -247,7 +248,7 @@ func queryTallyOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { // Prepare params for querier vars := mux.Vars(r) if len(vars[RestProposalID]) == 0 { - err := errors.New("proposalID required but not specified") + err := errors.New(fmt.Sprintf("%s required but not specified", RestProposalID)) rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } diff --git a/x/committee/client/rest/tx.go b/x/committee/client/rest/tx.go index 46447123..263acf51 100644 --- a/x/committee/client/rest/tx.go +++ b/x/committee/client/rest/tx.go @@ -34,7 +34,7 @@ func postProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { // Parse and validate url params vars := mux.Vars(r) if len(vars[RestCommitteeID]) == 0 { - rest.WriteErrorResponse(w, http.StatusBadRequest, "committeeID required but not specified") + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("%s required but not specified", RestCommitteeID)) return } committeeID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestCommitteeID]) @@ -78,7 +78,7 @@ func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { // Parse and validate url params vars := mux.Vars(r) if len(vars[RestProposalID]) == 0 { - rest.WriteErrorResponse(w, http.StatusBadRequest, "proposalID required but not specified") + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("%s required but not specified", RestProposalID)) return } proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID]) diff --git a/x/committee/keeper/keeper_test.go b/x/committee/keeper/keeper_test.go index 90f624a7..c317d8d7 100644 --- a/x/committee/keeper/keeper_test.go +++ b/x/committee/keeper/keeper_test.go @@ -61,7 +61,7 @@ func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() { suite.False(found) } -func (suite *KeeperTestSuite) TestGetSetProposal() { +func (suite *KeeperTestSuite) TestGetSetDeleteProposal() { // test setup prop := types.Proposal{ ID: 12, @@ -86,7 +86,7 @@ func (suite *KeeperTestSuite) TestGetSetProposal() { suite.False(found) } -func (suite *KeeperTestSuite) TestGetSetVote() { +func (suite *KeeperTestSuite) TestGetSetDeleteVote() { // test setup vote := types.Vote{ ProposalID: 12, diff --git a/x/committee/types/codec.go b/x/committee/types/codec.go index a904d40e..bdac0489 100644 --- a/x/committee/types/codec.go +++ b/x/committee/types/codec.go @@ -29,7 +29,7 @@ func RegisterModuleCodec(cdc *codec.Codec) { RegisterAppCodec(cdc) } -// RegisterCodec registers the necessary types for the module +// RegisterAppCodec registers the necessary types for the module func RegisterAppCodec(cdc *codec.Codec) { // Proposals // The app codec needs the gov.Content type registered. This is done by the gov module. diff --git a/x/committee/types/gov_proposal.go b/x/committee/types/gov_proposal.go index aa201660..45bfe0a4 100644 --- a/x/committee/types/gov_proposal.go +++ b/x/committee/types/gov_proposal.go @@ -41,7 +41,7 @@ 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. +// ProposalRoute returns the routing key of the proposal. func (ccp CommitteeChangeProposal) ProposalRoute() string { return RouterKey } // ProposalType returns the type of the proposal. @@ -93,7 +93,7 @@ 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. +// ProposalRoute returns the routing key of the proposal. func (cdp CommitteeDeleteProposal) ProposalRoute() string { return RouterKey } // ProposalType returns the type of the proposal. diff --git a/x/committee/types/querier.go b/x/committee/types/querier.go index f58ed338..af1df4f2 100644 --- a/x/committee/types/querier.go +++ b/x/committee/types/querier.go @@ -6,7 +6,6 @@ import ( // Query endpoints supported by the Querier const ( - //QueryParams = "params" QueryCommittees = "committees" QueryCommittee = "committee" QueryProposals = "proposals" From 5c280696fb7d0dab79ca088fc4d62cff16d1b2c1 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 24 Apr 2020 23:15:51 +0100 Subject: [PATCH 38/54] refactor begin blocker --- x/committee/abci.go | 22 +------ x/committee/keeper/integration_test.go | 21 +++++++ x/committee/keeper/proposal.go | 21 +++++++ x/committee/keeper/proposal_test.go | 84 ++++++++++++++++++++++++++ x/committee/keeper/querier_test.go | 10 +-- 5 files changed, 128 insertions(+), 30 deletions(-) create mode 100644 x/committee/keeper/integration_test.go diff --git a/x/committee/abci.go b/x/committee/abci.go index 3dc45499..8e1c220e 100644 --- a/x/committee/abci.go +++ b/x/committee/abci.go @@ -1,32 +1,12 @@ package committee import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" abci "github.com/tendermint/tendermint/abci/types" - - "github.com/kava-labs/kava/x/committee/types" ) // BeginBlocker runs at the start of every block. func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k Keeper) { - // Close all expired proposals - k.IterateProposals(ctx, func(proposal types.Proposal) bool { - if proposal.HasExpiredBy(ctx.BlockTime()) { - - k.DeleteProposalAndVotes(ctx, proposal.ID) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeProposalClose, - sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)), - sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)), - sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, types.AttributeValueProposalTimeout), - ), - ) - } - return false - }) + k.CloseExpiredProposals(ctx) } diff --git a/x/committee/keeper/integration_test.go b/x/committee/keeper/integration_test.go new file mode 100644 index 00000000..47185ea0 --- /dev/null +++ b/x/committee/keeper/integration_test.go @@ -0,0 +1,21 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/committee/types" +) + +// proposalVoteMap collects up votes into a map indexed by proposalID +func getProposalVoteMap(k keeper.Keeper, ctx sdk.Context) map[uint64]([]types.Vote) { + + proposalVoteMap = map[uint64]([]types.Vote){} + + keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { + keeper.IterateVotes(suite.ctx, p.ID, func(v types.Vote) bool { + proposalVoteMap[p.ID] = append(proposalVoteMap[p.ID], v) + return false + }) + return false + }) + return proposalVoteMap +} diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index 424bcc56..f93fa78f 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -125,6 +125,27 @@ func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) sdk.Error { return nil } +// CloseExpiredProposals removes proposals (and associated votes) that have past their deadline. +func (k Keeper) CloseExpiredProposals(ctx sdk.Context) { + + k.IterateProposals(ctx, func(proposal types.Proposal) bool { + if proposal.HasExpiredBy(ctx.BlockTime()) { + + k.DeleteProposalAndVotes(ctx, proposal.ID) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeProposalClose, + sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)), + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)), + sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, types.AttributeValueProposalTimeout), + ), + ) + } + return false + }) +} + // ValidatePubProposal checks if a pubproposal is valid. func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error { if pubProposal == nil { diff --git a/x/committee/keeper/proposal_test.go b/x/committee/keeper/proposal_test.go index 2ff91cdc..91754d5e 100644 --- a/x/committee/keeper/proposal_test.go +++ b/x/committee/keeper/proposal_test.go @@ -322,3 +322,87 @@ func (suite *KeeperTestSuite) TestValidatePubProposal() { }) } } + +func (suite *KeeperTestSuite) TestCloseExpiredProposals() { + + // Setup test state + firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC) + testGenesis = types.NewGenesisState( + 3, + []types.Committee{ + { + ID: 1, + Description: "This committee is for testing.", + Members: suite.addresses[:3], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + MaxProposalDuration: time.Hour * 24 * 7, + }, + { + ID: 2, + Members: suite.addresses[2:], + Permissions: nil, + VoteThreshold: d("0.667"), + MaxProposalDuration: time.Hour * 24 * 7, + }, + }, + []types.Proposal{ + { + ID: 1, + CommitteeID: 1, + PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + Deadline: firstBlockTime.Add(7 * 24 * time.Hour), + }, + { + ID: 2, + CommitteeID: 1, + PubProposal: gov.NewTextProposal("Another Title", "A description of this other proposal."), + Deadline: firstBlockTime.Add(21 * 24 * time.Hour), + }, + }, + []types.Vote{ + {ProposalID: 1, Voter: suite.addresses[0]}, + {ProposalID: 1, Voter: suite.addresses[1]}, + {ProposalID: 2, Voter: suite.addresses[2]}, + }, + ) + suite.app.InitializeFromGenesisStates( + NewCommitteeGenesisState(suite.cdc, testGenesis), + ) + + // close proposals + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime}) + suite.keeper.CloseExpiredProposals(ctx) + + // check + for _, p := range testGenesis.Proposals { + _, found := k.GetProposal(ctx, p.ID) + votes := getProposalVoteMap(suite.keeper, ctx) + + if ctx.BlockTime().After(p.Deadline) { + suite.False(found) + suite.Empty(votes[p.ID]) + } else { + suite.True(found) + suite.NotEmpty(votes[p.ID]) + } + } + + // close (later time) + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime.Add(7 * 24 * time.Hour)}) + suite.keeper.CloseExpiredProposals(ctx) + + // check + for _, p := range testGenesis.Proposals { + _, found := k.GetProposal(ctx, p.ID) + votes := getProposalVoteMap(suite.keeper, ctx) + + if ctx.BlockTime().After(p.Deadline) { + suite.False(found) + suite.Empty(votes[p.ID]) + } else { + suite.True(found) + suite.NotEmpty(votes[p.ID]) + } + } +} diff --git a/x/committee/keeper/querier_test.go b/x/committee/keeper/querier_test.go index 92cfcb2f..383fb4d1 100644 --- a/x/committee/keeper/querier_test.go +++ b/x/committee/keeper/querier_test.go @@ -84,15 +84,7 @@ func (suite *QuerierTestSuite) SetupTest() { NewCommitteeGenesisState(suite.cdc, suite.testGenesis), ) - // Collect up votes into a map indexed by proposalID for convenience - suite.votes = map[uint64]([]types.Vote){} - suite.keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { - suite.keeper.IterateVotes(suite.ctx, p.ID, func(v types.Vote) bool { - suite.votes[p.ID] = append(suite.votes[p.ID], v) - return false - }) - return false - }) + suite.votes = getProposalVoteMap(suite.keeper, suite.ctx) } func (suite *QuerierTestSuite) TestQueryCommittees() { From 733711c88c9a8eaf64d42c81ac0ce8900b903f29 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 24 Apr 2020 23:16:04 +0100 Subject: [PATCH 39/54] add query committee cmd --- x/committee/client/cli/query.go | 74 +++++++++++++++++++++++++++----- x/committee/client/rest/query.go | 1 - 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/x/committee/client/cli/query.go b/x/committee/client/cli/query.go index e0318fba..e97a8499 100644 --- a/x/committee/client/cli/query.go +++ b/x/committee/client/cli/query.go @@ -17,27 +17,69 @@ import ( // GetQueryCmd returns the cli query commands for this module func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { - // Group gov queries under a subcommand - govQueryCmd := &cobra.Command{ + queryCmd := &cobra.Command{ Use: types.ModuleName, - Short: "Querying commands for the governance module", + Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName), DisableFlagParsing: true, SuggestionsMinimumDistance: 2, RunE: client.ValidateCmd, } - govQueryCmd.AddCommand(client.GetCommands( + queryCmd.AddCommand(client.GetCommands( + // committees + GetCmdQueryCommittee(queryRoute, cdc), GetCmdQueryCommittees(queryRoute, cdc), - + // proposals GetCmdQueryProposal(queryRoute, cdc), GetCmdQueryProposals(queryRoute, cdc), - + // votes GetCmdQueryVotes(queryRoute, cdc), - + // other GetCmdQueryProposer(queryRoute, cdc), GetCmdQueryTally(queryRoute, cdc))...) - return govQueryCmd + return queryCmd +} + +// ------------------------------------------ +// Committees +// ------------------------------------------ + +// GetCmdQueryCommittee implements a query committee command. +func GetCmdQueryCommittee(queryRoute string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "committee [committee-id]", + Args: cobra.ExactArgs(1), + Short: "Query details of a single committee", + Example: fmt.Sprintf("%s query %s committee 1", version.ClientName, types.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Prepare params for querier + committeeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("committee-id %s not a valid uint", args[0]) + } + bz, err := cdc.MarshalJSON(types.NewQueryCommitteeParams(committeeID)) + if err != nil { + return err + } + + // Query + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryCommittee), bz) + if err != nil { + return err + } + + // Decode and print result + committee := types.Committee{} + if err = cdc.UnmarshalJSON(res, &committee); err != nil { + return err + } + return cliCtx.PrintOutput(committee) + }, + } + return cmd } // GetCmdQueryCommittees implements a query committees command. @@ -67,6 +109,10 @@ func GetCmdQueryCommittees(queryRoute string, cdc *codec.Codec) *cobra.Command { return cmd } +// ------------------------------------------ +// Proposals +// ------------------------------------------ + // GetCmdQueryProposal implements the query proposal command. func GetCmdQueryProposal(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ @@ -82,7 +128,7 @@ func GetCmdQueryProposal(queryRoute string, cdc *codec.Codec) *cobra.Command { if err != nil { return fmt.Errorf("proposal-id %s not a valid uint", args[0]) } - bz, err := cdc.MarshalJSON(types.NewQueryCommitteeParams(proposalID)) + bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID)) if err != nil { return err } @@ -122,7 +168,7 @@ func GetCmdQueryProposals(queryRoute string, cdc *codec.Codec) *cobra.Command { } // Query - res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/proposals", queryRoute), bz) + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryProposals), bz) if err != nil { return err } @@ -139,6 +185,10 @@ func GetCmdQueryProposals(queryRoute string, cdc *codec.Codec) *cobra.Command { return cmd } +// ------------------------------------------ +// Votes +// ------------------------------------------ + // GetCmdQueryVotes implements the command to query for proposal votes. func GetCmdQueryVotes(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ @@ -176,6 +226,10 @@ func GetCmdQueryVotes(queryRoute string, cdc *codec.Codec) *cobra.Command { } } +// ------------------------------------------ +// Other +// ------------------------------------------ + func GetCmdQueryTally(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "tally [proposal-id]", diff --git a/x/committee/client/rest/query.go b/x/committee/client/rest/query.go index 1ae72928..5e6b4590 100644 --- a/x/committee/client/rest/query.go +++ b/x/committee/client/rest/query.go @@ -190,7 +190,6 @@ func queryProposerHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } // Write response - cliCtx = cliCtx.WithHeight(height) rest.PostProcessResponse(w, cliCtx, res) } } From 114097edb3c7ec0bf74ea8f6c720e11be6e2c518 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 24 Apr 2020 23:36:08 +0100 Subject: [PATCH 40/54] add cli help text test --- x/committee/client/cli/cli_test.go | 36 ++++++++++++++++++++++++++++++ x/committee/client/cli/tx.go | 16 ++++++------- 2 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 x/committee/client/cli/cli_test.go diff --git a/x/committee/client/cli/cli_test.go b/x/committee/client/cli/cli_test.go new file mode 100644 index 00000000..95e776ae --- /dev/null +++ b/x/committee/client/cli/cli_test.go @@ -0,0 +1,36 @@ +package cli_test + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/stretchr/testify/suite" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/committee/client/cli" +) + +type CLITestSuite struct { + suite.Suite + cdc *codec.Codec +} + +func (suite *CLITestSuite) SetupTest() { + ahpp := app.NewTestApp() + suite.cdc = ahpp.Codec() +} + +func (suite *CLITestSuite) TestExampleCommitteeChangeProposal() { + suite.NotPanics(func() { cli.MustGetExampleCommitteeChangeProposal(suite.cdc) }) +} + +func (suite *CLITestSuite) TestExampleCommitteeDeleteProposal() { + suite.NotPanics(func() { cli.MustGetExampleCommitteeDeleteProposal(suite.cdc) }) +} +func (suite *CLITestSuite) TestExampleParameterChangeProposal() { + suite.NotPanics(func() { cli.MustGetExampleParameterChangeProposal(suite.cdc) }) +} + +func TestCLITestSuite(t *testing.T) { + suite.Run(t, new(CLITestSuite)) +} diff --git a/x/committee/client/cli/tx.go b/x/committee/client/cli/tx.go index 292a8476..e3fbbd94 100644 --- a/x/committee/client/cli/tx.go +++ b/x/committee/client/cli/tx.go @@ -49,7 +49,7 @@ func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { The proposal file must be the json encoded forms of the proposal type you want to submit. For example: %s -`, mustGetExampleParameterChangeProposal(cdc)), +`, MustGetExampleParameterChangeProposal(cdc)), Args: cobra.ExactArgs(2), Example: fmt.Sprintf("%s tx %s submit-proposal 1 your-proposal.json", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { @@ -139,7 +139,7 @@ For example, to create or update a committee: and to delete a committee: %s -`, mustGetExampleCommitteeChangeProposal(cdc), mustGetExampleCommitteeDeleteProposal(cdc)), +`, MustGetExampleCommitteeChangeProposal(cdc), MustGetExampleCommitteeDeleteProposal(cdc)), Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) @@ -181,8 +181,8 @@ and to delete a committee: return cmd } -// mustGetExampleCommitteeChangeProposal is a helper function to return an example json proposal -func mustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string { +// MustGetExampleCommitteeChangeProposal is a helper function to return an example json proposal +func MustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string { exampleChangeProposal := types.NewCommitteeChangeProposal( "A Title", "A description of this proposal.", @@ -206,8 +206,8 @@ func mustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string { return string(exampleChangeProposalBz) } -// mustGetExampleCommitteeDeleteProposal is a helper function to return an example json proposal -func mustGetExampleCommitteeDeleteProposal(cdc *codec.Codec) string { +// MustGetExampleCommitteeDeleteProposal is a helper function to return an example json proposal +func MustGetExampleCommitteeDeleteProposal(cdc *codec.Codec) string { exampleDeleteProposal := types.NewCommitteeDeleteProposal( "A Title", "A description of this proposal.", @@ -220,8 +220,8 @@ func mustGetExampleCommitteeDeleteProposal(cdc *codec.Codec) string { return string(exampleDeleteProposalBz) } -// mustGetExampleParameterChangeProposal is a helper function to return an example json proposal -func mustGetExampleParameterChangeProposal(cdc *codec.Codec) string { +// MustGetExampleParameterChangeProposal is a helper function to return an example json proposal +func MustGetExampleParameterChangeProposal(cdc *codec.Codec) string { exampleParameterChangeProposal := params.NewParameterChangeProposal( "A Title", "A description of this proposal.", From ccad1f82e23f2c98b570ba9fd8358c16f5777b40 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 25 Apr 2020 00:05:54 +0100 Subject: [PATCH 41/54] switch to non length prefixed marshalling --- x/committee/keeper/keeper.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 6b2f9f7c..dbc3a31c 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -45,14 +45,14 @@ func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committ return types.Committee{}, false } var committee types.Committee - k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &committee) + k.cdc.MustUnmarshalBinaryBare(bz, &committee) return committee, true } // SetCommittee puts a committee into the store. func (k Keeper) SetCommittee(ctx sdk.Context, committee types.Committee) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix) - bz := k.cdc.MustMarshalBinaryLengthPrefixed(committee) + bz := k.cdc.MustMarshalBinaryBare(committee) store.Set(types.GetKeyFromID(committee.ID), bz) } @@ -70,7 +70,7 @@ func (k Keeper) IterateCommittees(ctx sdk.Context, cb func(committee types.Commi defer iterator.Close() for ; iterator.Valid(); iterator.Next() { var committee types.Committee - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &committee) + k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &committee) if cb(committee) { break @@ -114,12 +114,12 @@ func (k Keeper) StoreNewProposal(ctx sdk.Context, pubProposal types.PubProposal, if err != nil { return 0, err } - proposal := types.Proposal{ - PubProposal: pubProposal, - ID: newProposalID, - CommitteeID: committeeID, - Deadline: deadline, - } + proposal := types.NewProposal( + pubProposal, + newProposalID, + committeeID, + deadline, + ) k.SetProposal(ctx, proposal) @@ -138,14 +138,14 @@ func (k Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (types.Proposal, return types.Proposal{}, false } var proposal types.Proposal - k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &proposal) + k.cdc.MustUnmarshalBinaryBare(bz, &proposal) return proposal, true } // SetProposal puts a proposal into the store. func (k Keeper) SetProposal(ctx sdk.Context, proposal types.Proposal) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix) - bz := k.cdc.MustMarshalBinaryLengthPrefixed(proposal) + bz := k.cdc.MustMarshalBinaryBare(proposal) store.Set(types.GetKeyFromID(proposal.ID), bz) } @@ -163,7 +163,7 @@ func (k Keeper) IterateProposals(ctx sdk.Context, cb func(proposal types.Proposa defer iterator.Close() for ; iterator.Valid(); iterator.Next() { var proposal types.Proposal - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &proposal) + k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &proposal) if cb(proposal) { break @@ -183,14 +183,14 @@ func (k Keeper) GetVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress return types.Vote{}, false } var vote types.Vote - k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &vote) + k.cdc.MustUnmarshalBinaryBare(bz, &vote) return vote, true } // SetVote puts a vote into the store. func (k Keeper) SetVote(ctx sdk.Context, vote types.Vote) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix) - bz := k.cdc.MustMarshalBinaryLengthPrefixed(vote) + bz := k.cdc.MustMarshalBinaryBare(vote) store.Set(types.GetVoteKey(vote.ProposalID, vote.Voter), bz) } @@ -209,7 +209,7 @@ func (k Keeper) IterateVotes(ctx sdk.Context, proposalID uint64, cb func(vote ty defer iterator.Close() for ; iterator.Valid(); iterator.Next() { var vote types.Vote - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &vote) + k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &vote) if cb(vote) { break From ebb6366837b4ce5c9234a84bb61e4fbde286f624 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 25 Apr 2020 00:22:56 +0100 Subject: [PATCH 42/54] address various pr comments --- x/committee/abci_test.go | 12 ++--- x/committee/client/rest/rest.go | 1 - x/committee/keeper/integration_test.go | 8 ++-- x/committee/keeper/keeper_test.go | 12 ++--- x/committee/keeper/proposal.go | 2 +- x/committee/keeper/proposal_test.go | 64 +++++++++++++------------- x/committee/keeper/querier_test.go | 22 ++++----- x/committee/proposal_handler_test.go | 50 ++++++++++---------- x/committee/types/genesis_test.go | 24 +++++----- x/committee/types/types.go | 35 ++++++++------ 10 files changed, 120 insertions(+), 110 deletions(-) diff --git a/x/committee/abci_test.go b/x/committee/abci_test.go index f990dd43..21e2af68 100644 --- a/x/committee/abci_test.go +++ b/x/committee/abci_test.go @@ -37,11 +37,11 @@ func (suite *ModuleTestSuite) TestBeginBlock() { suite.app.InitializeFromGenesisStates() normalCom := committee.Committee{ - ID: 12, - Members: suite.addresses[:2], - Permissions: []committee.Permission{committee.GodPermission{}}, - VoteThreshold: d("0.8"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 12, + Members: suite.addresses[:2], + Permissions: []committee.Permission{committee.GodPermission{}}, + VoteThreshold: d("0.8"), + ProposalDuration: time.Hour * 24 * 7, } suite.keeper.SetCommittee(suite.ctx, normalCom) @@ -55,7 +55,7 @@ func (suite *ModuleTestSuite) TestBeginBlock() { suite.NoError(err) // Run BeginBlocker - proposalDurationLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(normalCom.MaxProposalDuration)) + proposalDurationLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(normalCom.ProposalDuration)) suite.NotPanics(func() { committee.BeginBlocker(proposalDurationLaterCtx, abci.RequestBeginBlock{}, suite.keeper) }) diff --git a/x/committee/client/rest/rest.go b/x/committee/client/rest/rest.go index a172adba..96849d09 100644 --- a/x/committee/client/rest/rest.go +++ b/x/committee/client/rest/rest.go @@ -10,7 +10,6 @@ import ( const ( RestProposalID = "proposal-id" RestCommitteeID = "committee-id" - RestVoter = "voter" ) // RegisterRoutes - Central function to define routes that get registered by the main application diff --git a/x/committee/keeper/integration_test.go b/x/committee/keeper/integration_test.go index 47185ea0..e681c6c8 100644 --- a/x/committee/keeper/integration_test.go +++ b/x/committee/keeper/integration_test.go @@ -2,16 +2,18 @@ package keeper_test import ( sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/committee/keeper" "github.com/kava-labs/kava/x/committee/types" ) // proposalVoteMap collects up votes into a map indexed by proposalID func getProposalVoteMap(k keeper.Keeper, ctx sdk.Context) map[uint64]([]types.Vote) { - proposalVoteMap = map[uint64]([]types.Vote){} + proposalVoteMap := map[uint64]([]types.Vote){} - keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { - keeper.IterateVotes(suite.ctx, p.ID, func(v types.Vote) bool { + k.IterateProposals(ctx, func(p types.Proposal) bool { + k.IterateVotes(ctx, p.ID, func(v types.Vote) bool { proposalVoteMap[p.ID] = append(proposalVoteMap[p.ID], v) return false }) diff --git a/x/committee/keeper/keeper_test.go b/x/committee/keeper/keeper_test.go index c317d8d7..9144f449 100644 --- a/x/committee/keeper/keeper_test.go +++ b/x/committee/keeper/keeper_test.go @@ -37,12 +37,12 @@ func (suite *KeeperTestSuite) SetupTest() { func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() { // setup test com := types.Committee{ - ID: 12, - Description: "This committee is for testing.", - Members: suite.addresses, - Permissions: []types.Permission{types.GodPermission{}}, - VoteThreshold: d("0.667"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 12, + Description: "This committee is for testing.", + Members: suite.addresses, + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, } // write and read from store diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index f93fa78f..66cfddb4 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -30,7 +30,7 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ } // Get a new ID and store the proposal - deadline := ctx.BlockTime().Add(com.MaxProposalDuration) + deadline := ctx.BlockTime().Add(com.ProposalDuration) proposalID, err := k.StoreNewProposal(ctx, pubProposal, committeeID, deadline) if err != nil { return 0, err diff --git a/x/committee/keeper/proposal_test.go b/x/committee/keeper/proposal_test.go index 91754d5e..7f77c847 100644 --- a/x/committee/keeper/proposal_test.go +++ b/x/committee/keeper/proposal_test.go @@ -17,12 +17,12 @@ import ( func (suite *KeeperTestSuite) TestSubmitProposal() { normalCom := types.Committee{ - ID: 12, - Description: "This committee is for testing.", - Members: suite.addresses[:2], - Permissions: []types.Permission{types.GodPermission{}}, - VoteThreshold: d("0.667"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 12, + Description: "This committee is for testing.", + Members: suite.addresses[:2], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, } noPermissionsCom := normalCom noPermissionsCom.Permissions = []types.Permission{} @@ -96,7 +96,7 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { pr, found := keeper.GetProposal(ctx, id) suite.True(found) suite.Equal(tc.committeeID, pr.CommitteeID) - suite.Equal(ctx.BlockTime().Add(tc.committee.MaxProposalDuration), pr.Deadline) + suite.Equal(ctx.BlockTime().Add(tc.committee.ProposalDuration), pr.Deadline) } else { suite.NotNil(err) } @@ -141,7 +141,7 @@ func (suite *KeeperTestSuite) TestAddVote() { name: "proposal expired", proposalID: types.DefaultNextProposalID, voter: normalCom.Members[0], - voteTime: firstBlockTime.Add(normalCom.MaxProposalDuration), + voteTime: firstBlockTime.Add(normalCom.ProposalDuration), expectPass: false, }, } @@ -175,12 +175,12 @@ func (suite *KeeperTestSuite) TestAddVote() { func (suite *KeeperTestSuite) TestGetProposalResult() { normalCom := types.Committee{ - ID: 12, - Description: "This committee is for testing.", - Members: suite.addresses[:5], - Permissions: []types.Permission{types.GodPermission{}}, - VoteThreshold: d("0.667"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 12, + Description: "This committee is for testing.", + Members: suite.addresses[:5], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, } var defaultID uint64 = 1 firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC) @@ -327,23 +327,23 @@ func (suite *KeeperTestSuite) TestCloseExpiredProposals() { // Setup test state firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC) - testGenesis = types.NewGenesisState( + testGenesis := types.NewGenesisState( 3, []types.Committee{ { - ID: 1, - Description: "This committee is for testing.", - Members: suite.addresses[:3], - Permissions: []types.Permission{types.GodPermission{}}, - VoteThreshold: d("0.667"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 1, + Description: "This committee is for testing.", + Members: suite.addresses[:3], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, }, { - ID: 2, - Members: suite.addresses[2:], - Permissions: nil, - VoteThreshold: d("0.667"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 2, + Members: suite.addresses[2:], + Permissions: nil, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, }, }, []types.Proposal{ @@ -367,16 +367,16 @@ func (suite *KeeperTestSuite) TestCloseExpiredProposals() { }, ) suite.app.InitializeFromGenesisStates( - NewCommitteeGenesisState(suite.cdc, testGenesis), + NewCommitteeGenesisState(suite.app.Codec(), testGenesis), ) // close proposals - ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime}) + ctx := suite.app.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime}) suite.keeper.CloseExpiredProposals(ctx) // check for _, p := range testGenesis.Proposals { - _, found := k.GetProposal(ctx, p.ID) + _, found := suite.keeper.GetProposal(ctx, p.ID) votes := getProposalVoteMap(suite.keeper, ctx) if ctx.BlockTime().After(p.Deadline) { @@ -389,15 +389,15 @@ func (suite *KeeperTestSuite) TestCloseExpiredProposals() { } // close (later time) - ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime.Add(7 * 24 * time.Hour)}) + ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime.Add(7 * 24 * time.Hour)}) suite.keeper.CloseExpiredProposals(ctx) // check for _, p := range testGenesis.Proposals { - _, found := k.GetProposal(ctx, p.ID) + _, found := suite.keeper.GetProposal(ctx, p.ID) votes := getProposalVoteMap(suite.keeper, ctx) - if ctx.BlockTime().After(p.Deadline) { + if ctx.BlockTime().Equal(p.Deadline) || ctx.BlockTime().After(p.Deadline) { suite.False(found) suite.Empty(votes[p.ID]) } else { diff --git a/x/committee/keeper/querier_test.go b/x/committee/keeper/querier_test.go index 383fb4d1..0514151a 100644 --- a/x/committee/keeper/querier_test.go +++ b/x/committee/keeper/querier_test.go @@ -55,19 +55,19 @@ func (suite *QuerierTestSuite) SetupTest() { 3, []types.Committee{ { - ID: 1, - Description: "This committee is for testing.", - Members: suite.addresses[:3], - Permissions: []types.Permission{types.GodPermission{}}, - VoteThreshold: d("0.667"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 1, + Description: "This committee is for testing.", + Members: suite.addresses[:3], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, }, { - ID: 2, - Members: suite.addresses[2:], - Permissions: nil, - VoteThreshold: d("0.667"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 2, + Members: suite.addresses[2:], + Permissions: nil, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, }, }, []types.Proposal{ diff --git a/x/committee/proposal_handler_test.go b/x/committee/proposal_handler_test.go index f9c58263..43da7eb2 100644 --- a/x/committee/proposal_handler_test.go +++ b/x/committee/proposal_handler_test.go @@ -38,19 +38,19 @@ func (suite *ProposalHandlerTestSuite) SetupTest() { 2, []committee.Committee{ { - ID: 1, - Description: "This committee is for testing.", - Members: suite.addresses[:3], - Permissions: []types.Permission{types.GodPermission{}}, - VoteThreshold: d("0.667"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 1, + Description: "This committee is for testing.", + Members: suite.addresses[:3], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, }, { - ID: 2, - Members: suite.addresses[2:], - Permissions: nil, - VoteThreshold: d("0.667"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 2, + Members: suite.addresses[2:], + Permissions: nil, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, }, }, []committee.Proposal{ @@ -74,10 +74,10 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { "A Title", "A proposal description.", committee.Committee{ - ID: 34, - Members: suite.addresses[:1], - VoteThreshold: d("1"), - MaxProposalDuration: time.Hour * 24, + ID: 34, + Members: suite.addresses[:1], + VoteThreshold: d("1"), + ProposalDuration: time.Hour * 24, }, ), expectPass: true, @@ -88,11 +88,11 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { "A Title", "A proposal description.", committee.Committee{ - ID: suite.testGenesis.Committees[0].ID, - Members: suite.addresses, // add new members - Permissions: suite.testGenesis.Committees[0].Permissions, - VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold, - MaxProposalDuration: suite.testGenesis.Committees[0].MaxProposalDuration, + ID: suite.testGenesis.Committees[0].ID, + Members: suite.addresses, // add new members + Permissions: suite.testGenesis.Committees[0].Permissions, + VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold, + ProposalDuration: suite.testGenesis.Committees[0].ProposalDuration, }, ), expectPass: true, @@ -112,11 +112,11 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { "A Title", "A proposal description.", committee.Committee{ - ID: suite.testGenesis.Committees[0].ID, - Members: append(suite.addresses, suite.addresses[0]), // duplicate address - Permissions: suite.testGenesis.Committees[0].Permissions, - VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold, - MaxProposalDuration: suite.testGenesis.Committees[0].MaxProposalDuration, + ID: suite.testGenesis.Committees[0].ID, + Members: append(suite.addresses, suite.addresses[0]), // duplicate address + Permissions: suite.testGenesis.Committees[0].Permissions, + VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold, + ProposalDuration: suite.testGenesis.Committees[0].ProposalDuration, }, ), expectPass: false, diff --git a/x/committee/types/genesis_test.go b/x/committee/types/genesis_test.go index 009b5371..7fde2202 100644 --- a/x/committee/types/genesis_test.go +++ b/x/committee/types/genesis_test.go @@ -25,20 +25,20 @@ func TestGenesisState_Validate(t *testing.T) { NextProposalID: 2, Committees: []Committee{ { - ID: 1, - Description: "This committee is for testing.", - Members: addresses[:3], - Permissions: []Permission{GodPermission{}}, - VoteThreshold: d("0.667"), - MaxProposalDuration: time.Hour * 24 * 7, + ID: 1, + Description: "This committee is for testing.", + Members: addresses[:3], + Permissions: []Permission{GodPermission{}}, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, }, { - ID: 2, - Description: "This committee is also for testing.", - Members: addresses[2:], - Permissions: nil, - VoteThreshold: d("0.8"), - MaxProposalDuration: time.Hour * 24 * 21, + ID: 2, + Description: "This committee is also for testing.", + Members: addresses[2:], + Permissions: nil, + VoteThreshold: d("0.8"), + ProposalDuration: time.Hour * 24 * 21, }, }, Proposals: []Proposal{ diff --git a/x/committee/types/types.go b/x/committee/types/types.go index b5e46211..4f705a22 100644 --- a/x/committee/types/types.go +++ b/x/committee/types/types.go @@ -17,22 +17,22 @@ const MaxCommitteeDescriptionLength int = 5000 // A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. type Committee struct { - ID uint64 `json:"id" yaml:"id"` - Description string `json:"description" yaml:"description"` - Members []sdk.AccAddress `json:"members" yaml:"members"` - Permissions []Permission `json:"permissions" yaml:"permissions"` - VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` - MaxProposalDuration time.Duration `json:"max_proposal_duration" yaml:"max_proposal_duration"` + ID uint64 `json:"id" yaml:"id"` + Description string `json:"description" yaml:"description"` + Members []sdk.AccAddress `json:"members" yaml:"members"` + Permissions []Permission `json:"permissions" yaml:"permissions"` + VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` + ProposalDuration time.Duration `json:"proposal_duration" yaml:"proposal_duration"` } func NewCommittee(id uint64, description string, members []sdk.AccAddress, permissions []Permission, threshold sdk.Dec, duration time.Duration) Committee { return Committee{ - ID: id, - Description: description, - Members: members, - Permissions: permissions, - VoteThreshold: threshold, - MaxProposalDuration: duration, + ID: id, + Description: description, + Members: members, + Permissions: permissions, + VoteThreshold: threshold, + ProposalDuration: duration, } } @@ -84,7 +84,7 @@ func (c Committee) Validate() error { return fmt.Errorf("invalid threshold") } - if c.MaxProposalDuration < 0 { + if c.ProposalDuration < 0 { return fmt.Errorf("invalid time") } @@ -110,6 +110,15 @@ type Proposal struct { Deadline time.Time `json:"deadline" yaml:"deadline"` } +func NewProposal(pubProposal PubProposal, id uint64, committeeID uint64, deadline time.Time) Proposal { + return Proposal{ + PubProposal: pubProposal, + ID: id, + CommitteeID: committeeID, + Deadline: deadline, + } +} + // HasExpiredBy calculates if the proposal will have expired by a certain time. // All votes must be cast before deadline, those cast at time == deadline are not valid func (p Proposal) HasExpiredBy(time time.Time) bool { From 196ecf7f30d82769ddae15645d072ec8f5c10cd8 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 25 Apr 2020 17:39:59 +0100 Subject: [PATCH 43/54] improve proposal tests --- x/committee/keeper/integration_test.go | 6 ++ x/committee/keeper/keeper_test.go | 2 - x/committee/keeper/proposal.go | 28 +++++-- x/committee/keeper/proposal_test.go | 101 +++++++++++++++---------- 4 files changed, 90 insertions(+), 47 deletions(-) diff --git a/x/committee/keeper/integration_test.go b/x/committee/keeper/integration_test.go index e681c6c8..29a23fcc 100644 --- a/x/committee/keeper/integration_test.go +++ b/x/committee/keeper/integration_test.go @@ -7,6 +7,12 @@ import ( "github.com/kava-labs/kava/x/committee/types" ) +// Avoid cluttering test cases with long function names +func i(in int64) sdk.Int { return sdk.NewInt(in) } +func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) } +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } +func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } + // proposalVoteMap collects up votes into a map indexed by proposalID func getProposalVoteMap(k keeper.Keeper, ctx sdk.Context) map[uint64]([]types.Vote) { diff --git a/x/committee/keeper/keeper_test.go b/x/committee/keeper/keeper_test.go index 9144f449..3001eec0 100644 --- a/x/committee/keeper/keeper_test.go +++ b/x/committee/keeper/keeper_test.go @@ -15,8 +15,6 @@ import ( "github.com/kava-labs/kava/x/committee/types" ) -func d(s string) sdk.Dec { return sdk.MustNewDecFromStr(s) } - type KeeperTestSuite struct { suite.Suite diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index 66cfddb4..bbdb666f 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -78,6 +78,7 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress } // GetProposalResult calculates if a proposal currently has enough votes to pass. +// TODO rename GetProposalTally? func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64) (bool, sdk.Error) { pr, found := k.GetProposal(ctx, proposalID) if !found { @@ -114,18 +115,19 @@ func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) sdk.Error { return types.ErrUnknownProposal(k.codespace, proposalID) } - // Run the proposal's changes through the associated handler, but using a cached version of state to ensure changes are not permanent if an error occurs. - handler := k.router.GetRoute(pr.ProposalRoute()) - cacheCtx, writeCache := ctx.CacheContext() - if err := handler(cacheCtx, pr.PubProposal); err != nil { + if err := k.ValidatePubProposal(ctx, pr.PubProposal); err != nil { return err } - // write state to the underlying multi-store - writeCache() + handler := k.router.GetRoute(pr.ProposalRoute()) + if err := handler(ctx, pr.PubProposal); err != nil { + // the handler should not error as it was checked in ValidatePubProposal + panic(fmt.Sprintf("unexpected handler error: %s", err)) + } return nil } // CloseExpiredProposals removes proposals (and associated votes) that have past their deadline. +// TODO rename to RemoveExpiredProposals? func (k Keeper) CloseExpiredProposals(ctx sdk.Context) { k.IterateProposals(ctx, func(proposal types.Proposal) bool { @@ -147,7 +149,7 @@ func (k Keeper) CloseExpiredProposals(ctx sdk.Context) { } // ValidatePubProposal checks if a pubproposal is valid. -func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error { +func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) (returnErr sdk.Error) { if pubProposal == nil { return types.ErrInvalidPubProposal(k.codespace, "pub proposal cannot be nil") } @@ -162,6 +164,17 @@ func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubPropos // Run the proposal's changes through the associated handler using a cached version of state to ensure changes are not permanent. cacheCtx, _ := ctx.CacheContext() handler := k.router.GetRoute(pubProposal.ProposalRoute()) + + // Handle an edge case where a param change proposal causes the proposal handler to panic. + // A param change proposal with a registered subspace value but unregistered key value will cause a panic in the param change proposal handler. + // This defer will catch panics and return a normal error: `recover()` gets the panic value, then the enclosing function's return value is swapped for an error. + // reference: https://stackoverflow.com/questions/33167282/how-to-return-a-value-in-a-go-function-that-panics?noredirect=1&lq=1 + defer func() { + if r := recover(); r != nil { + returnErr = types.ErrInvalidPubProposal(k.codespace, fmt.Sprintf("proposal handler panicked: %s", r)) + } + }() + if err := handler(cacheCtx, pubProposal); err != nil { return err } @@ -169,6 +182,7 @@ func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubPropos } // DeleteProposalAndVotes removes a proposal and its associated votes. +// TODO move to keeper.go func (k Keeper) DeleteProposalAndVotes(ctx sdk.Context, proposalID uint64) { var votes []types.Vote k.IterateVotes(ctx, proposalID, func(vote types.Vote) bool { diff --git a/x/committee/keeper/proposal_test.go b/x/committee/keeper/proposal_test.go index 7f77c847..cc6417e5 100644 --- a/x/committee/keeper/proposal_test.go +++ b/x/committee/keeper/proposal_test.go @@ -11,6 +11,7 @@ import ( abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" + cdptypes "github.com/kava-labs/kava/x/cdp/types" "github.com/kava-labs/kava/x/committee" "github.com/kava-labs/kava/x/committee/types" ) @@ -33,7 +34,7 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { pubProposal types.PubProposal proposer sdk.AccAddress committeeID uint64 - expectPass bool + expectErr bool }{ { name: "normal", @@ -41,7 +42,7 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), proposer: normalCom.Members[0], committeeID: normalCom.ID, - expectPass: true, + expectErr: false, }, { name: "invalid proposal", @@ -49,7 +50,7 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { pubProposal: nil, proposer: normalCom.Members[0], committeeID: normalCom.ID, - expectPass: false, + expectErr: true, }, { name: "missing committee", @@ -57,7 +58,7 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), proposer: suite.addresses[0], committeeID: 0, - expectPass: false, + expectErr: true, }, { name: "not a member", @@ -65,7 +66,7 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), proposer: suite.addresses[4], committeeID: normalCom.ID, - expectPass: false, + expectErr: true, }, { name: "not enough permissions", @@ -73,7 +74,7 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), proposer: noPermissionsCom.Members[0], committeeID: noPermissionsCom.ID, - expectPass: false, + expectErr: true, }, } @@ -91,14 +92,14 @@ func (suite *KeeperTestSuite) TestSubmitProposal() { id, err := keeper.SubmitProposal(ctx, tc.proposer, tc.committeeID, tc.pubProposal) - if tc.expectPass { + if tc.expectErr { + suite.NotNil(err) + } else { suite.NoError(err) pr, found := keeper.GetProposal(ctx, id) suite.True(found) suite.Equal(tc.committeeID, pr.CommitteeID) suite.Equal(ctx.BlockTime().Add(tc.committee.ProposalDuration), pr.Deadline) - } else { - suite.NotNil(err) } }) } @@ -117,32 +118,32 @@ func (suite *KeeperTestSuite) TestAddVote() { proposalID uint64 voter sdk.AccAddress voteTime time.Time - expectPass bool + expectErr bool }{ { name: "normal", proposalID: types.DefaultNextProposalID, voter: normalCom.Members[0], - expectPass: true, + expectErr: false, }, { name: "nonexistent proposal", proposalID: 9999999, voter: normalCom.Members[0], - expectPass: false, + expectErr: true, }, { name: "voter not committee member", proposalID: types.DefaultNextProposalID, voter: suite.addresses[4], - expectPass: false, + expectErr: true, }, { name: "proposal expired", proposalID: types.DefaultNextProposalID, voter: normalCom.Members[0], voteTime: firstBlockTime.Add(normalCom.ProposalDuration), - expectPass: false, + expectErr: true, }, } @@ -162,12 +163,12 @@ func (suite *KeeperTestSuite) TestAddVote() { ctx = ctx.WithBlockTime(tc.voteTime) err = keeper.AddVote(ctx, tc.proposalID, tc.voter) - if tc.expectPass { + if tc.expectErr { + suite.NotNil(err) + } else { suite.NoError(err) _, found := keeper.GetVote(ctx, tc.proposalID, tc.voter) suite.True(found) - } else { - suite.NotNil(err) } }) } @@ -190,7 +191,7 @@ func (suite *KeeperTestSuite) TestGetProposalResult() { committee types.Committee votes []types.Vote proposalPasses bool - expectPass bool + expectErr bool }{ { name: "enough votes", @@ -202,7 +203,7 @@ func (suite *KeeperTestSuite) TestGetProposalResult() { {ProposalID: defaultID, Voter: suite.addresses[3]}, }, proposalPasses: true, - expectPass: true, + expectErr: false, }, { name: "not enough votes", @@ -211,7 +212,7 @@ func (suite *KeeperTestSuite) TestGetProposalResult() { {ProposalID: defaultID, Voter: suite.addresses[0]}, }, proposalPasses: false, - expectPass: true, + expectErr: false, }, } @@ -238,11 +239,11 @@ func (suite *KeeperTestSuite) TestGetProposalResult() { proposalPasses, err := keeper.GetProposalResult(ctx, defaultID) - if tc.expectPass { + if tc.expectErr { + suite.NotNil(err) + } else { suite.NoError(err) suite.Equal(tc.proposalPasses, proposalPasses) - } else { - suite.NotNil(err) } }) } @@ -272,27 +273,40 @@ func (suite *KeeperTestSuite) TestValidatePubProposal() { testcases := []struct { name string pubProposal types.PubProposal - expectPass bool + expectErr bool }{ { - name: "valid", + name: "valid (text proposal)", pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), - expectPass: true, + expectErr: false, + }, + { + name: "valid (param change proposal)", + pubProposal: params.NewParameterChangeProposal( + "Change the debt limit", + "This proposal changes the debt limit of the cdp module.", + []params.ParamChange{{ + Subspace: cdptypes.ModuleName, + Key: string(cdptypes.KeyGlobalDebtLimit), + Value: string(types.ModuleCdc.MustMarshalJSON(cs(c("usdx", 100000000000)))), + }}, + ), + expectErr: false, }, { name: "invalid (missing title)", pubProposal: gov.TextProposal{Description: "A description of this proposal."}, - expectPass: false, + expectErr: true, }, { name: "invalid (unregistered)", pubProposal: UnregisteredProposal{gov.TextProposal{Title: "A Title", Description: "A description of this proposal."}}, - expectPass: false, + expectErr: true, }, { name: "invalid (nil)", pubProposal: nil, - expectPass: false, + expectErr: true, }, { name: "invalid (proposal handler fails)", @@ -300,24 +314,35 @@ func (suite *KeeperTestSuite) TestValidatePubProposal() { "A Title", "A description of this proposal.", []params.ParamChange{{ - Subspace: "non existant", - Key: "non existant", - Value: "nonsense", + Subspace: "nonsense-subspace", + Key: "nonsense-key", + Value: "nonsense-value", }}, ), - expectPass: false, + expectErr: true, + }, + { + name: "invalid (proposal handler panics)", + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{{ + Subspace: cdptypes.ModuleName, + Key: "nonsense-key", // a valid Subspace but invalid Key will trigger a panic in the paramchange propsal handler + Value: "nonsense-value", + }}, + ), + expectErr: true, }, - // Some proposals can cause the proposal handler to panic. - // However panics will be caught when the proposal is first submitted so should avoid making it onto the chain. } for _, tc := range testcases { suite.Run(tc.name, func() { err := suite.keeper.ValidatePubProposal(suite.ctx, tc.pubProposal) - if tc.expectPass { - suite.NoError(err) - } else { + if tc.expectErr { suite.NotNil(err) + } else { + suite.NoError(err) } }) } From c231912642dbb3329bc30596e255e44053108730 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sun, 26 Apr 2020 15:28:57 +0100 Subject: [PATCH 44/54] add handler tests --- x/committee/abci_test.go | 2 - x/committee/handler_test.go | 191 +++++++++++++++++++++++++ x/committee/integration_test.go | 20 +++ x/committee/keeper/integration_test.go | 8 ++ x/committee/keeper/proposal_test.go | 13 ++ x/committee/keeper/querier_test.go | 5 - 6 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 x/committee/handler_test.go create mode 100644 x/committee/integration_test.go diff --git a/x/committee/abci_test.go b/x/committee/abci_test.go index 21e2af68..8750822e 100644 --- a/x/committee/abci_test.go +++ b/x/committee/abci_test.go @@ -14,8 +14,6 @@ import ( "github.com/kava-labs/kava/x/committee" ) -func d(s string) sdk.Dec { return sdk.MustNewDecFromStr(s) } - type ModuleTestSuite struct { suite.Suite diff --git a/x/committee/handler_test.go b/x/committee/handler_test.go new file mode 100644 index 00000000..89d729fd --- /dev/null +++ b/x/committee/handler_test.go @@ -0,0 +1,191 @@ +package committee_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + cdptypes "github.com/kava-labs/kava/x/cdp/types" + "github.com/kava-labs/kava/x/committee" + "github.com/kava-labs/kava/x/committee/keeper" + "github.com/kava-labs/kava/x/committee/types" +) + +// NewDistributionGenesisWithPool creates a default distribution genesis state with some coins in the community pool. +func NewDistributionGenesisWithPool(communityPoolCoins sdk.Coins) app.GenesisState { + gs := distribution.DefaultGenesisState() + gs.FeePool = distribution.FeePool{CommunityPool: sdk.NewDecCoins(communityPoolCoins)} + return app.GenesisState{distribution.ModuleName: distribution.ModuleCdc.MustMarshalJSON(gs)} +} + +type HandlerTestSuite struct { + suite.Suite + + app app.TestApp + keeper keeper.Keeper + handler sdk.Handler + ctx sdk.Context + addresses []sdk.AccAddress + + communityPoolAmt sdk.Coins +} + +func (suite *HandlerTestSuite) SetupTest() { + _, suite.addresses = app.GeneratePrivKeyAddressPairs(5) + suite.app = app.NewTestApp() + suite.keeper = suite.app.GetCommitteeKeeper() + suite.handler = committee.NewHandler(suite.keeper) + + firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC) + testGenesis := types.NewGenesisState( + 3, + []types.Committee{ + { + ID: 1, + Description: "This committee is for testing.", + Members: suite.addresses[:3], + Permissions: []types.Permission{types.GodPermission{}}, + VoteThreshold: d("0.5"), + ProposalDuration: time.Hour * 24 * 7, + }, + }, + []types.Proposal{}, + []types.Vote{}, + ) + suite.communityPoolAmt = cs(c("ukava", 1000)) + suite.app.InitializeFromGenesisStates( + NewCommitteeGenesisState(suite.app.Codec(), testGenesis), + NewDistributionGenesisWithPool(suite.communityPoolAmt), + ) + suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime}) +} + +func (suite *HandlerTestSuite) TestSubmitProposalMsg_Valid() { + msg := committee.NewMsgSubmitProposal( + params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{{ + Subspace: cdptypes.ModuleName, + Key: string(cdptypes.KeyDebtThreshold), + Value: string(types.ModuleCdc.MustMarshalJSON(i(1000000))), + }}, + ), + suite.addresses[0], + 1, + ) + + res := suite.handler(suite.ctx, msg) + + suite.True(res.IsOK()) + _, found := suite.keeper.GetProposal(suite.ctx, types.Uint64FromBytes(res.Data)) + suite.True(found) +} + +func (suite *HandlerTestSuite) TestSubmitProposalMsg_Invalid() { + msg := types.NewMsgSubmitProposal( + params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{{ + Subspace: cdptypes.ModuleName, + Key: "nonsense-key", + Value: "nonsense-value", + }}, + ), + suite.addresses[0], + 1, + ) + + res := suite.handler(suite.ctx, msg) + + suite.False(res.IsOK()) + suite.keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { + suite.Fail("proposal found when none should exist") + return true + }) +} + +func (suite *HandlerTestSuite) TestMsgAddVote_ProposalPass() { + previousCDPDebtThreshold := suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold + newDebtThreshold := previousCDPDebtThreshold.Add(i(1000000)) + msg := types.NewMsgSubmitProposal( + params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{{ + Subspace: cdptypes.ModuleName, + Key: string(cdptypes.KeyDebtThreshold), + Value: string(types.ModuleCdc.MustMarshalJSON(newDebtThreshold)), + }}, + ), + suite.addresses[0], + 1, + ) + res := suite.handler(suite.ctx, msg) + suite.True(res.IsOK()) + proposalID := types.Uint64FromBytes(res.Data) + res = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID)) + suite.True(res.IsOK()) + + // Add a vote to make the proposal pass + res = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID)) + + suite.True(res.IsOK()) + // Check the param has been updated + suite.Equal(newDebtThreshold, suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold) + // Check proposal and votes are gone + _, found := suite.keeper.GetProposal(suite.ctx, proposalID) + suite.False(found) + suite.keeper.IterateVotes(suite.ctx, proposalID, func(v types.Vote) bool { + suite.Fail("vote found when there should be none") + return true + }) +} + +func (suite *HandlerTestSuite) TestMsgAddVote_ProposalFail() { + recipient := suite.addresses[4] + recipientCoins := suite.app.GetBankKeeper().GetCoins(suite.ctx, recipient) + msg := types.NewMsgSubmitProposal( + distribution.NewCommunityPoolSpendProposal( + "A Title", + "A description of this proposal.", + recipient, + cs(c("ukava", 500)), + ), + suite.addresses[0], + 1, + ) + res := suite.handler(suite.ctx, msg) + suite.True(res.IsOK()) + proposalID := types.Uint64FromBytes(res.Data) + res = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID)) + suite.True(res.IsOK()) + + // invalidate the proposal by emptying community pool + suite.app.GetDistrKeeper().DistributeFromFeePool(suite.ctx, suite.communityPoolAmt, suite.addresses[0]) + + // Add a vote to make the proposal pass + res = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID)) + + suite.True(res.IsOK()) + // Check the proposal was not enacted + suite.Equal(recipientCoins, suite.app.GetBankKeeper().GetCoins(suite.ctx, recipient)) + // Check proposal and votes are gone + _, found := suite.keeper.GetProposal(suite.ctx, proposalID) + suite.False(found) + suite.keeper.IterateVotes(suite.ctx, proposalID, func(v types.Vote) bool { + suite.Fail("vote found when there should be none") + return true + }) +} + +func TestHandlerTestSuite(t *testing.T) { + suite.Run(t, new(HandlerTestSuite)) +} diff --git a/x/committee/integration_test.go b/x/committee/integration_test.go new file mode 100644 index 00000000..ce5414df --- /dev/null +++ b/x/committee/integration_test.go @@ -0,0 +1,20 @@ +package committee_test + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/committee/types" +) + +// Avoid cluttering test cases with long function names +func i(in int64) sdk.Int { return sdk.NewInt(in) } +func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) } +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } +func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } + +// NewCommitteeGenesisState marshals a committee genesis state into json for use in initializing test apps. +func NewCommitteeGenesisState(cdc *codec.Codec, gs types.GenesisState) app.GenesisState { + return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(gs)} +} diff --git a/x/committee/keeper/integration_test.go b/x/committee/keeper/integration_test.go index 29a23fcc..e1cc0531 100644 --- a/x/committee/keeper/integration_test.go +++ b/x/committee/keeper/integration_test.go @@ -1,8 +1,11 @@ package keeper_test import ( + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/committee" "github.com/kava-labs/kava/x/committee/keeper" "github.com/kava-labs/kava/x/committee/types" ) @@ -27,3 +30,8 @@ func getProposalVoteMap(k keeper.Keeper, ctx sdk.Context) map[uint64]([]types.Vo }) return proposalVoteMap } + +// NewCommitteeGenesisState marshals a committee genesis state into json for use in initializing test apps. +func NewCommitteeGenesisState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState { + return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)} +} diff --git a/x/committee/keeper/proposal_test.go b/x/committee/keeper/proposal_test.go index cc6417e5..9a963fc0 100644 --- a/x/committee/keeper/proposal_test.go +++ b/x/committee/keeper/proposal_test.go @@ -334,6 +334,19 @@ func (suite *KeeperTestSuite) TestValidatePubProposal() { ), expectErr: true, }, + { + name: "invalid (proposal handler fails - invalid json)", + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{{ + Subspace: cdptypes.ModuleName, + Key: string(cdptypes.KeyGlobalDebtLimit), + Value: `{"denom": "usdx",`, + }}, + ), + expectErr: true, + }, } for _, tc := range testcases { diff --git a/x/committee/keeper/querier_test.go b/x/committee/keeper/querier_test.go index 0514151a..2278fcc6 100644 --- a/x/committee/keeper/querier_test.go +++ b/x/committee/keeper/querier_test.go @@ -13,7 +13,6 @@ import ( 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/keeper" "github.com/kava-labs/kava/x/committee/types" ) @@ -24,10 +23,6 @@ const ( var testTime time.Time = time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC) -func NewCommitteeGenesisState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState { - return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)} -} - type QuerierTestSuite struct { suite.Suite From 576dcc7dfd24003f890e3c1ae426761c1e020be7 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 27 Apr 2020 13:56:59 +0100 Subject: [PATCH 45/54] rename files --- x/committee/types/{types.go => committee.go} | 0 x/committee/types/{gov_proposal.go => proposal.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename x/committee/types/{types.go => committee.go} (100%) rename x/committee/types/{gov_proposal.go => proposal.go} (100%) diff --git a/x/committee/types/types.go b/x/committee/types/committee.go similarity index 100% rename from x/committee/types/types.go rename to x/committee/types/committee.go diff --git a/x/committee/types/gov_proposal.go b/x/committee/types/proposal.go similarity index 100% rename from x/committee/types/gov_proposal.go rename to x/committee/types/proposal.go From 20c02a6a54b8e78ac6f49b3d801d513a9b9be04f Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 27 Apr 2020 13:57:47 +0100 Subject: [PATCH 46/54] add permissions tests --- x/committee/client/cli/cli_test.go | 4 +- x/committee/handler_test.go | 16 ++ x/committee/integration_test.go | 12 ++ x/committee/keeper/proposal_test.go | 10 +- x/committee/types/committee_test.go | 195 +++++++++++++++++ x/committee/types/permissions.go | 24 ++- x/committee/types/permissions_test.go | 291 ++++++++++++++++++++++++++ 7 files changed, 540 insertions(+), 12 deletions(-) create mode 100644 x/committee/types/committee_test.go create mode 100644 x/committee/types/permissions_test.go diff --git a/x/committee/client/cli/cli_test.go b/x/committee/client/cli/cli_test.go index 95e776ae..20b6c564 100644 --- a/x/committee/client/cli/cli_test.go +++ b/x/committee/client/cli/cli_test.go @@ -16,8 +16,8 @@ type CLITestSuite struct { } func (suite *CLITestSuite) SetupTest() { - ahpp := app.NewTestApp() - suite.cdc = ahpp.Codec() + tApp := app.NewTestApp() + suite.cdc = tApp.Codec() } func (suite *CLITestSuite) TestExampleCommitteeChangeProposal() { diff --git a/x/committee/handler_test.go b/x/committee/handler_test.go index 89d729fd..111702d2 100644 --- a/x/committee/handler_test.go +++ b/x/committee/handler_test.go @@ -112,6 +112,22 @@ func (suite *HandlerTestSuite) TestSubmitProposalMsg_Invalid() { }) } +func (suite *HandlerTestSuite) TestSubmitProposalMsg_Unregistered() { + msg := types.NewMsgSubmitProposal( + UnregisteredPubProposal{}, + suite.addresses[0], + 1, + ) + + res := suite.handler(suite.ctx, msg) + + suite.False(res.IsOK()) + suite.keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { + suite.Fail("proposal found when none should exist") + return true + }) +} + func (suite *HandlerTestSuite) TestMsgAddVote_ProposalPass() { previousCDPDebtThreshold := suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold newDebtThreshold := previousCDPDebtThreshold.Add(i(1000000)) diff --git a/x/committee/integration_test.go b/x/committee/integration_test.go index ce5414df..a26d7300 100644 --- a/x/committee/integration_test.go +++ b/x/committee/integration_test.go @@ -18,3 +18,15 @@ func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } func NewCommitteeGenesisState(cdc *codec.Codec, gs types.GenesisState) app.GenesisState { return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(gs)} } + +var _ types.PubProposal = UnregisteredPubProposal{} + +// UnregisteredPubProposal is a pubproposal type that is not registered on the amino codec. +type UnregisteredPubProposal struct{} + +func (UnregisteredPubProposal) GetTitle() string { return "unregistered" } +func (UnregisteredPubProposal) GetDescription() string { return "unregistered" } +func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" } +func (UnregisteredPubProposal) ProposalType() string { return "unregistered" } +func (UnregisteredPubProposal) ValidateBasic() sdk.Error { return nil } +func (UnregisteredPubProposal) String() string { return "unregistered" } diff --git a/x/committee/keeper/proposal_test.go b/x/committee/keeper/proposal_test.go index 9a963fc0..e8fddd5e 100644 --- a/x/committee/keeper/proposal_test.go +++ b/x/committee/keeper/proposal_test.go @@ -259,14 +259,14 @@ func committeeGenState(cdc *codec.Codec, committees []types.Committee, proposals return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)} } -type UnregisteredProposal struct { +type UnregisteredPubProposal struct { gov.TextProposal } -func (UnregisteredProposal) ProposalRoute() string { return "unregistered" } -func (UnregisteredProposal) ProposalType() string { return "unregistered" } +func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" } +func (UnregisteredPubProposal) ProposalType() string { return "unregistered" } -var _ types.PubProposal = UnregisteredProposal{} +var _ types.PubProposal = UnregisteredPubProposal{} func (suite *KeeperTestSuite) TestValidatePubProposal() { @@ -300,7 +300,7 @@ func (suite *KeeperTestSuite) TestValidatePubProposal() { }, { name: "invalid (unregistered)", - pubProposal: UnregisteredProposal{gov.TextProposal{Title: "A Title", Description: "A description of this proposal."}}, + pubProposal: UnregisteredPubProposal{gov.TextProposal{Title: "A Title", Description: "A description of this proposal."}}, expectErr: true, }, { diff --git a/x/committee/types/committee_test.go b/x/committee/types/committee_test.go new file mode 100644 index 00000000..1f7ec077 --- /dev/null +++ b/x/committee/types/committee_test.go @@ -0,0 +1,195 @@ +package types + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/stretchr/testify/suite" +) + +var _ PubProposal = UnregisteredPubProposal{} + +type UnregisteredPubProposal struct { + gov.TextProposal +} + +func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" } +func (UnregisteredPubProposal) ProposalType() string { return "unregistered" } + +type TypesTestSuite struct { + suite.Suite +} + +func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { + + testcases := []struct { + name string + permissions []Permission + pubProposal PubProposal + expectHasPermissions bool + }{ + { + name: "normal (single permission)", + permissions: []Permission{ParamChangePermission{ + AllowedParams: AllowedParams{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + }, + }}}, + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000"}`, + }, + }, + ), + expectHasPermissions: true, + }, + { + name: "normal (multiple permissions)", + permissions: []Permission{ + ParamChangePermission{ + AllowedParams: AllowedParams{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + }, + }}, + TextPermission{}, + }, + pubProposal: gov.NewTextProposal("A Proposal Title", "A description of this proposal"), + expectHasPermissions: true, + }, + { + name: "overruling permission", + permissions: []Permission{ + ParamChangePermission{ + AllowedParams: AllowedParams{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + }, + }}, + GodPermission{}, + }, + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{ + { + Subspace: "cdp", + Key: "CollateralParams", + Subkey: "", + Value: `[]`, + }, + }, + ), + expectHasPermissions: true, + }, + { + name: "no permissions", + permissions: nil, + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{ + { + Subspace: "cdp", + Key: "CollateralParams", + Subkey: "", + Value: `[]`, + }, + }, + ), + expectHasPermissions: false, + }, + { + name: "split permissions", + // These permissions looks like they allow the param change proposal, however a proposal must pass a single permission independently of others. + permissions: []Permission{ + ParamChangePermission{ + AllowedParams: AllowedParams{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + }, + }}, + ParamChangePermission{ + AllowedParams: AllowedParams{ + { + Subspace: "cdp", + Key: "DebtParams", + Subkey: "", + }, + }}, + }, + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description of this proposal.", + []params.ParamChange{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000"}`, + }, + { + Subspace: "cdp", + Key: "DebtParams", + Subkey: "", + Value: `[]`, + }, + }, + ), + expectHasPermissions: false, + }, + { + name: "unregistered proposal", + permissions: []Permission{ + ParamChangePermission{ + AllowedParams: AllowedParams{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + }, + }}, + }, + pubProposal: UnregisteredPubProposal{gov.TextProposal{"A Title", "A description."}}, + expectHasPermissions: false, + }, + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + com := NewCommittee( + 12, + "a description of this committee", + nil, + tc.permissions, + d("0.5"), + 24*time.Hour, + ) + suite.Equal( + tc.expectHasPermissions, + com.HasPermissionsFor(tc.pubProposal), + ) + }) + } +} + +func TestTypesTestSuite(t *testing.T) { + suite.Run(t, new(TypesTestSuite)) +} diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index fcb1c0a4..77a66aa2 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -52,7 +52,7 @@ func (perm ParamChangePermission) Allows(p gov.Content) bool { func (perm ParamChangePermission) MarshalYAML() (interface{}, error) { valueToMarshal := struct { Type string `yaml:"type"` - AllowedParams AllowedParams `yaml:"allowed_params` + AllowedParams AllowedParams `yaml:"allowed_params"` }{ Type: "param_change_permission", AllowedParams: perm.AllowedParams, @@ -76,7 +76,21 @@ func (allowed AllowedParams) Contains(paramChange params.ParamChange) bool { return false } -// TODO add more permissions? -// - limit parameter changes to be within small ranges -// - allow community spend proposals -// - allow committee change proposals +// TextPermission allows any text governance proposal. +type TextPermission struct{} + +var _ Permission = TextPermission{} + +func (TextPermission) Allows(p gov.Content) bool { + _, ok := p.(gov.TextProposal) + return ok +} + +func (TextPermission) MarshalYAML() (interface{}, error) { + valueToMarshal := struct { + Type string `yaml:"type"` + }{ + Type: "text_permission", + } + return valueToMarshal, nil +} diff --git a/x/committee/types/permissions_test.go b/x/committee/types/permissions_test.go new file mode 100644 index 00000000..9794d10a --- /dev/null +++ b/x/committee/types/permissions_test.go @@ -0,0 +1,291 @@ +package types + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/stretchr/testify/suite" +) + +type PermissionsTestSuite struct { + suite.Suite + + exampleAllowedParams AllowedParams +} + +func (suite *PermissionsTestSuite) SetupTest() { + suite.exampleAllowedParams = AllowedParams{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + }, + { + Subspace: "cdp", + Key: "SurplusThreshold", + Subkey: "", + }, + { + Subspace: "cdp", + Key: "CollateralParams", + Subkey: "", + }, + { + Subspace: "auction", + Key: "BidDuration", + Subkey: "", + }, + } +} + +func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() { + testcases := []struct { + name string + allowedParams AllowedParams + pubProposal PubProposal + expectAllowed bool + }{ + { + name: "normal (single param)", + allowedParams: suite.exampleAllowedParams, + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description for this proposal.", + []params.ParamChange{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000"}`, + }, + }, + ), + expectAllowed: true, + }, + { + name: "normal (multiple params)", + allowedParams: suite.exampleAllowedParams, + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description for this proposal.", + []params.ParamChange{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000"}`, + }, + { + Subspace: "cdp", + Key: "CollateralParams", + Subkey: "", + Value: `[]`, + }, + }, + ), + expectAllowed: true, + }, + { + name: "not allowed (not in list)", + allowedParams: suite.exampleAllowedParams, + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description for this proposal.", + []params.ParamChange{ + { + Subspace: "cdp", + Key: "GlobalDebtLimit", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000000"}`, + }, + }, + ), + expectAllowed: false, + }, + { + name: "not allowed (nil allowed params)", + allowedParams: nil, + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description for this proposal.", + []params.ParamChange{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + Value: `[{"denom": "usdx", "amount": "1000000"}]`, + }, + }, + ), + expectAllowed: false, + }, + { + name: "not allowed (mismatched pubproposal type)", + allowedParams: suite.exampleAllowedParams, + pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + expectAllowed: false, + }, + { + name: "not allowed (nil pubproposal)", + allowedParams: suite.exampleAllowedParams, + pubProposal: nil, + expectAllowed: false, + }, + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + permission := ParamChangePermission{ + AllowedParams: tc.allowedParams, + } + suite.Equal( + tc.expectAllowed, + permission.Allows(tc.pubProposal), + ) + }) + } +} + +func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { + testcases := []struct { + name string + allowedParams AllowedParams + testParam params.ParamChange + expectContained bool + }{ + { + name: "normal", + allowedParams: suite.exampleAllowedParams, + testParam: params.ParamChange{ + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000"}`, + }, + expectContained: true, + }, + { + name: "missing subspace", + allowedParams: suite.exampleAllowedParams, + testParam: params.ParamChange{ + Subspace: "", + Key: "DebtThreshold", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000"}`, + }, + expectContained: false, + }, + { + name: "missing key", + allowedParams: suite.exampleAllowedParams, + testParam: params.ParamChange{ + Subspace: "cdp", + Key: "", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000"}`, + }, + expectContained: false, + }, + { + name: "empty list", + allowedParams: AllowedParams{}, + testParam: params.ParamChange{ + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000"}`, + }, + expectContained: false, + }, + { + name: "nil list", + allowedParams: nil, + testParam: params.ParamChange{ + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000"}`, + }, + expectContained: false, + }, + { + name: "no param change", + allowedParams: suite.exampleAllowedParams, + testParam: params.ParamChange{}, + expectContained: false, + }, + { + name: "empty list and no param change", + allowedParams: AllowedParams{}, + testParam: params.ParamChange{}, + expectContained: false, + }, + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + suite.Require().Equal( + tc.expectContained, + tc.allowedParams.Contains(tc.testParam), + ) + }) + } +} + +func (suite *PermissionsTestSuite) TestTextPermission_Allows() { + testcases := []struct { + name string + pubProposal PubProposal + expectAllowed bool + }{ + { + name: "normal", + pubProposal: gov.NewTextProposal( + "A Title", + "A description for this proposal.", + ), + expectAllowed: true, + }, + { + name: "not allowed (wrong pubproposal type)", + pubProposal: params.NewParameterChangeProposal( + "A Title", + "A description for this proposal.", + []params.ParamChange{ + { + Subspace: "cdp", + Key: "DebtThreshold", + Subkey: "", + Value: `{"denom": "usdx", "amount": "1000000"}`, + }, + { + Subspace: "cdp", + Key: "CollateralParams", + Subkey: "", + Value: `[]`, + }, + }, + ), + expectAllowed: false, + }, + { + name: "not allowed (nil pubproposal)", + pubProposal: nil, + expectAllowed: false, + }, + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + permission := TextPermission{} + suite.Equal( + tc.expectAllowed, + permission.Allows(tc.pubProposal), + ) + }) + } +} +func TestPermissionsTestSuite(t *testing.T) { + suite.Run(t, new(PermissionsTestSuite)) +} From c17de631d2d96beb6497dd8663fd4699bda6a9c8 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 27 Apr 2020 15:04:47 +0100 Subject: [PATCH 47/54] add nicer keeper get methods --- x/committee/genesis.go | 18 ++----- x/committee/handler_test.go | 39 +++++++------- x/committee/keeper/integration_test.go | 7 +-- x/committee/keeper/keeper.go | 72 ++++++++++++++++++++++++-- x/committee/keeper/proposal.go | 21 +------- x/committee/keeper/querier.go | 20 ++----- x/committee/proposal_handler.go | 12 +---- x/committee/proposal_handler_test.go | 52 ++++--------------- 8 files changed, 110 insertions(+), 131 deletions(-) diff --git a/x/committee/genesis.go b/x/committee/genesis.go index cc0b9409..af29c0e1 100644 --- a/x/committee/genesis.go +++ b/x/committee/genesis.go @@ -33,21 +33,9 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { if err != nil { panic(err) } - committees := []types.Committee{} - keeper.IterateCommittees(ctx, func(com types.Committee) bool { - committees = append(committees, com) - return false - }) - proposals := []types.Proposal{} - votes := []types.Vote{} - keeper.IterateProposals(ctx, func(p types.Proposal) bool { - proposals = append(proposals, p) - keeper.IterateVotes(ctx, p.ID, func(v types.Vote) bool { - votes = append(votes, v) - return false - }) - return false - }) + committees := keeper.GetCommittees(ctx) + proposals := keeper.GetProposals(ctx) + votes := keeper.GetVotes(ctx) return types.NewGenesisState( nextID, diff --git a/x/committee/handler_test.go b/x/committee/handler_test.go index 111702d2..69dca714 100644 --- a/x/committee/handler_test.go +++ b/x/committee/handler_test.go @@ -89,6 +89,7 @@ func (suite *HandlerTestSuite) TestSubmitProposalMsg_Valid() { } func (suite *HandlerTestSuite) TestSubmitProposalMsg_Invalid() { + var committeeID uint64 = 1 msg := types.NewMsgSubmitProposal( params.NewParameterChangeProposal( "A Title", @@ -100,32 +101,34 @@ func (suite *HandlerTestSuite) TestSubmitProposalMsg_Invalid() { }}, ), suite.addresses[0], - 1, + committeeID, ) res := suite.handler(suite.ctx, msg) suite.False(res.IsOK()) - suite.keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { - suite.Fail("proposal found when none should exist") - return true - }) + suite.Empty( + suite.keeper.GetProposalsByCommittee(suite.ctx, committeeID), + "proposal found when none should exist", + ) + } func (suite *HandlerTestSuite) TestSubmitProposalMsg_Unregistered() { + var committeeID uint64 = 1 msg := types.NewMsgSubmitProposal( UnregisteredPubProposal{}, suite.addresses[0], - 1, + committeeID, ) res := suite.handler(suite.ctx, msg) suite.False(res.IsOK()) - suite.keeper.IterateProposals(suite.ctx, func(p types.Proposal) bool { - suite.Fail("proposal found when none should exist") - return true - }) + suite.Empty( + suite.keeper.GetProposalsByCommittee(suite.ctx, committeeID), + "proposal found when none should exist", + ) } func (suite *HandlerTestSuite) TestMsgAddVote_ProposalPass() { @@ -159,10 +162,10 @@ func (suite *HandlerTestSuite) TestMsgAddVote_ProposalPass() { // Check proposal and votes are gone _, found := suite.keeper.GetProposal(suite.ctx, proposalID) suite.False(found) - suite.keeper.IterateVotes(suite.ctx, proposalID, func(v types.Vote) bool { - suite.Fail("vote found when there should be none") - return true - }) + suite.Empty( + suite.keeper.GetVotesByProposal(suite.ctx, proposalID), + "vote found when there should be none", + ) } func (suite *HandlerTestSuite) TestMsgAddVote_ProposalFail() { @@ -196,10 +199,10 @@ func (suite *HandlerTestSuite) TestMsgAddVote_ProposalFail() { // Check proposal and votes are gone _, found := suite.keeper.GetProposal(suite.ctx, proposalID) suite.False(found) - suite.keeper.IterateVotes(suite.ctx, proposalID, func(v types.Vote) bool { - suite.Fail("vote found when there should be none") - return true - }) + suite.Empty( + suite.keeper.GetVotesByProposal(suite.ctx, proposalID), + "vote found when there should be none", + ) } func TestHandlerTestSuite(t *testing.T) { diff --git a/x/committee/keeper/integration_test.go b/x/committee/keeper/integration_test.go index e1cc0531..ff0a1cef 100644 --- a/x/committee/keeper/integration_test.go +++ b/x/committee/keeper/integration_test.go @@ -16,16 +16,13 @@ func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } -// proposalVoteMap collects up votes into a map indexed by proposalID +// getProposalVoteMap collects up votes into a map indexed by proposalID func getProposalVoteMap(k keeper.Keeper, ctx sdk.Context) map[uint64]([]types.Vote) { proposalVoteMap := map[uint64]([]types.Vote){} k.IterateProposals(ctx, func(p types.Proposal) bool { - k.IterateVotes(ctx, p.ID, func(v types.Vote) bool { - proposalVoteMap[p.ID] = append(proposalVoteMap[p.ID], v) - return false - }) + proposalVoteMap[p.ID] = k.GetVotesByProposal(ctx, p.ID) return false }) return proposalVoteMap diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index dbc3a31c..1ad4815a 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -78,6 +78,16 @@ func (k Keeper) IterateCommittees(ctx sdk.Context, cb func(committee types.Commi } } +// GetCommittees returns all stored committees. +func (k Keeper) GetCommittees(ctx sdk.Context) []types.Committee { + results := []types.Committee{} + k.IterateCommittees(ctx, func(com types.Committee) bool { + results = append(results, com) + return false + }) + return results +} + // ------------------------------------------ // Proposals // ------------------------------------------ @@ -171,6 +181,39 @@ func (k Keeper) IterateProposals(ctx sdk.Context, cb func(proposal types.Proposa } } +// GetProposals returns all stored proposals. +func (k Keeper) GetProposals(ctx sdk.Context) []types.Proposal { + results := []types.Proposal{} + k.IterateProposals(ctx, func(prop types.Proposal) bool { + results = append(results, prop) + return false + }) + return results +} + +// GetProposalsByCommittee returns all proposals for one committee. +func (k Keeper) GetProposalsByCommittee(ctx sdk.Context, committeeID uint64) []types.Proposal { + results := []types.Proposal{} + k.IterateProposals(ctx, func(prop types.Proposal) bool { + if prop.CommitteeID == committeeID { + results = append(results, prop) + } + return false + }) + return results +} + +// DeleteProposalAndVotes removes a proposal and its associated votes. +func (k Keeper) DeleteProposalAndVotes(ctx sdk.Context, proposalID uint64) { + + votes := k.GetVotesByProposal(ctx, proposalID) + + k.DeleteProposal(ctx, proposalID) + for _, v := range votes { + k.DeleteVote(ctx, v.ProposalID, v.Voter) + } +} + // ------------------------------------------ // Votes // ------------------------------------------ @@ -200,11 +243,10 @@ func (k Keeper) DeleteVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddr store.Delete(types.GetVoteKey(proposalID, voter)) } -// IterateVotes provides an iterator over all stored votes for a given proposal. +// IterateVotes provides an iterator over all stored votes. // For each vote, cb will be called. If cb returns true, the iterator will close and stop. -func (k Keeper) IterateVotes(ctx sdk.Context, proposalID uint64, cb func(vote types.Vote) (stop bool)) { - // iterate over the section of the votes store that has all votes for a particular proposal - iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), append(types.VoteKeyPrefix, types.GetKeyFromID(proposalID)...)) +func (k Keeper) IterateVotes(ctx sdk.Context, cb func(vote types.Vote) (stop bool)) { + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.VoteKeyPrefix) defer iterator.Close() for ; iterator.Valid(); iterator.Next() { @@ -216,3 +258,25 @@ func (k Keeper) IterateVotes(ctx sdk.Context, proposalID uint64, cb func(vote ty } } } + +// GetVotes returns all stored votes. +func (k Keeper) GetVotes(ctx sdk.Context) []types.Vote { + results := []types.Vote{} + k.IterateVotes(ctx, func(vote types.Vote) bool { + results = append(results, vote) + return false + }) + return results +} + +// GetVotesByProposal returns all votes for one proposal. +func (k Keeper) GetVotesByProposal(ctx sdk.Context, proposalID uint64) []types.Vote { + results := []types.Vote{} + k.IterateVotes(ctx, func(vote types.Vote) bool { + if vote.ProposalID == proposalID { + results = append(results, vote) + } + return false + }) + return results +} diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index bbdb666f..b36f4470 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -99,11 +99,7 @@ func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64) (bool, sdk // TallyVotes counts all the votes on a proposal func (k Keeper) TallyVotes(ctx sdk.Context, proposalID uint64) int64 { - var votes []types.Vote - k.IterateVotes(ctx, proposalID, func(vote types.Vote) bool { - votes = append(votes, vote) - return false - }) + votes := k.GetVotesByProposal(ctx, proposalID) return int64(len(votes)) } @@ -180,18 +176,3 @@ func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubPropos } return nil } - -// DeleteProposalAndVotes removes a proposal and its associated votes. -// TODO move to keeper.go -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) - } -} diff --git a/x/committee/keeper/querier.go b/x/committee/keeper/querier.go index 5981f945..a1a1a002 100644 --- a/x/committee/keeper/querier.go +++ b/x/committee/keeper/querier.go @@ -42,11 +42,7 @@ func NewQuerier(keeper Keeper) sdk.Querier { func queryCommittees(ctx sdk.Context, path []string, _ abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { - committees := []types.Committee{} - keeper.IterateCommittees(ctx, func(com types.Committee) bool { - committees = append(committees, com) - return false - }) + committees := keeper.GetCommittees(ctx) bz, err := codec.MarshalJSONIndent(keeper.cdc, committees) if err != nil { @@ -85,13 +81,7 @@ func queryProposals(ctx sdk.Context, path []string, req abci.RequestQuery, keepe return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) } - proposals := []types.Proposal{} - keeper.IterateProposals(ctx, func(p types.Proposal) bool { - if p.CommitteeID == params.CommitteeID { - proposals = append(proposals, p) - } - return false - }) + proposals := keeper.GetProposalsByCommittee(ctx, params.CommitteeID) bz, err := codec.MarshalJSONIndent(keeper.cdc, proposals) if err != nil { @@ -131,11 +121,7 @@ func queryVotes(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Ke return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) } - votes := []types.Vote{} - keeper.IterateVotes(ctx, params.ProposalID, func(v types.Vote) bool { - votes = append(votes, v) - return false - }) + votes := keeper.GetVotesByProposal(ctx, params.ProposalID) bz, err := codec.MarshalJSONIndent(keeper.cdc, votes) if err != nil { diff --git a/x/committee/proposal_handler.go b/x/committee/proposal_handler.go index e48ada2c..749434bd 100644 --- a/x/committee/proposal_handler.go +++ b/x/committee/proposal_handler.go @@ -28,16 +28,12 @@ func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal } // 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) + k.DeleteProposalAndVotes(ctx, p.ID) } 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) @@ -50,16 +46,12 @@ func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal } // 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) + k.DeleteProposalAndVotes(ctx, p.ID) } 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 diff --git a/x/committee/proposal_handler_test.go b/x/committee/proposal_handler_test.go index 43da7eb2..0a97cfc0 100644 --- a/x/committee/proposal_handler_test.go +++ b/x/committee/proposal_handler_test.go @@ -133,19 +133,7 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { 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 - }) + oldProposals := suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.NewCommittee.ID) // Run err := handler(suite.ctx, tc.proposal) @@ -153,19 +141,15 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { // Check if tc.expectPass { suite.NoError(err) - // check proposal is accurate + // check committee 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) + suite.Empty(suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.NewCommittee.ID)) + for _, p := range oldProposals { + suite.Empty(suite.keeper.GetVotesByProposal(suite.ctx, p.ID)) } } else { suite.Error(err) @@ -211,19 +195,7 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_DeleteCommittee() { 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 - }) + oldProposals := suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.CommitteeID) // Run err := handler(suite.ctx, tc.proposal) @@ -231,18 +203,14 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_DeleteCommittee() { // Check if tc.expectPass { suite.NoError(err) - // check proposal is accurate + // check committee has been removed _, 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) + suite.Empty(suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.CommitteeID)) + for _, p := range oldProposals { + suite.Empty(suite.keeper.GetVotesByProposal(suite.ctx, p.ID)) } } else { suite.Error(err) From 631b87eaf0afed0df12abb465ae84c4d47f4ad64 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 27 Apr 2020 15:37:25 +0100 Subject: [PATCH 48/54] apply various PR comments --- x/committee/keeper/proposal.go | 27 +++++++++++++-------------- x/committee/proposal_handler.go | 10 ++++++---- x/committee/types/committee.go | 14 +++++--------- x/committee/types/permissions.go | 17 +++++++++++++++++ 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index b36f4470..f2db48c5 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -78,7 +78,6 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress } // GetProposalResult calculates if a proposal currently has enough votes to pass. -// TODO rename GetProposalTally? func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64) (bool, sdk.Error) { pr, found := k.GetProposal(ctx, proposalID) if !found { @@ -123,23 +122,23 @@ func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) sdk.Error { } // CloseExpiredProposals removes proposals (and associated votes) that have past their deadline. -// TODO rename to RemoveExpiredProposals? func (k Keeper) CloseExpiredProposals(ctx sdk.Context) { k.IterateProposals(ctx, func(proposal types.Proposal) bool { - if proposal.HasExpiredBy(ctx.BlockTime()) { - - k.DeleteProposalAndVotes(ctx, proposal.ID) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeProposalClose, - sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)), - sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)), - sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, types.AttributeValueProposalTimeout), - ), - ) + if !proposal.HasExpiredBy(ctx.BlockTime()) { + return false } + + k.DeleteProposalAndVotes(ctx, proposal.ID) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeProposalClose, + sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)), + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)), + sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, types.AttributeValueProposalTimeout), + ), + ) return false }) } diff --git a/x/committee/proposal_handler.go b/x/committee/proposal_handler.go index 749434bd..da5a049e 100644 --- a/x/committee/proposal_handler.go +++ b/x/committee/proposal_handler.go @@ -29,9 +29,10 @@ func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal // Remove all committee's ongoing proposals k.IterateProposals(ctx, func(p Proposal) bool { - if p.CommitteeID == committeeProposal.NewCommittee.ID { - k.DeleteProposalAndVotes(ctx, p.ID) + if p.CommitteeID != committeeProposal.NewCommittee.ID { + return false } + k.DeleteProposalAndVotes(ctx, p.ID) return false }) @@ -47,9 +48,10 @@ func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal // Remove all committee's ongoing proposals k.IterateProposals(ctx, func(p Proposal) bool { - if p.CommitteeID == committeeProposal.CommitteeID { - k.DeleteProposalAndVotes(ctx, p.ID) + if p.CommitteeID != committeeProposal.CommitteeID { + return false } + k.DeleteProposalAndVotes(ctx, p.ID) return false }) diff --git a/x/committee/types/committee.go b/x/committee/types/committee.go index 4f705a22..fb22a196 100644 --- a/x/committee/types/committee.go +++ b/x/committee/types/committee.go @@ -21,8 +21,8 @@ type Committee struct { Description string `json:"description" yaml:"description"` Members []sdk.AccAddress `json:"members" yaml:"members"` Permissions []Permission `json:"permissions" yaml:"permissions"` - VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` - ProposalDuration time.Duration `json:"proposal_duration" yaml:"proposal_duration"` + VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` // Smallest percentage of members that must vote for a proposal to pass. + ProposalDuration time.Duration `json:"proposal_duration" yaml:"proposal_duration"` // The length of time a proposal remains active for. Proposals will close earlier if they get enough votes. } func NewCommittee(id uint64, description string, members []sdk.AccAddress, permissions []Permission, threshold sdk.Dec, duration time.Duration) Committee { @@ -80,22 +80,18 @@ func (c Committee) Validate() error { return fmt.Errorf("invalid description") } - if c.VoteThreshold.IsNil() || c.VoteThreshold.IsNegative() || c.VoteThreshold.GT(sdk.NewDec(1)) { + // 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") } if c.ProposalDuration < 0 { - return fmt.Errorf("invalid time") + return fmt.Errorf("invalid proposal duration") } return nil } -// Permission is anything with a method that validates whether a proposal is allowed by it or not. -type Permission interface { - Allows(PubProposal) bool -} - // ------------------------------------------ // Proposals // ------------------------------------------ diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index 77a66aa2..6608d9bd 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -13,6 +13,15 @@ func init() { gov.RegisterProposalTypeCodec(ParamChangePermission{}, "kava/ParamChangePermission") } +// Permission is anything with a method that validates whether a proposal is allowed by it or not. +type Permission interface { + Allows(PubProposal) bool +} + +// ------------------------------------------ +// GodPermission +// ------------------------------------------ + // GodPermission allows any governance proposal. It is used mainly for testing. type GodPermission struct{} @@ -29,6 +38,10 @@ func (GodPermission) MarshalYAML() (interface{}, error) { return valueToMarshal, nil } +// ------------------------------------------ +// ParamChangePermission +// ------------------------------------------ + // ParamChangeProposal only allows changes to certain params type ParamChangePermission struct { AllowedParams AllowedParams `json:"allowed_params" yaml:"allowed_params"` @@ -76,6 +89,10 @@ func (allowed AllowedParams) Contains(paramChange params.ParamChange) bool { return false } +// ------------------------------------------ +// TextPermission +// ------------------------------------------ + // TextPermission allows any text governance proposal. type TextPermission struct{} From 073cd7ebda70d5051cb11e4cd299d256b2d95754 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 27 Apr 2020 19:19:05 +0100 Subject: [PATCH 49/54] update module to v0.38 --- app/app.go | 11 ++--- x/committee/alias.go | 36 +++++++--------- x/committee/client/cli/query.go | 3 +- x/committee/client/cli/tx.go | 13 ++++-- x/committee/handler.go | 24 +++++------ x/committee/handler_test.go | 38 ++++++++--------- x/committee/integration_test.go | 12 +++--- x/committee/keeper/keeper.go | 23 +++++----- x/committee/keeper/proposal.go | 38 +++++++++-------- x/committee/keeper/querier.go | 55 ++++++++++++------------ x/committee/module.go | 61 +++++++++++++++------------ x/committee/proposal_handler.go | 16 +++---- x/committee/simulation/decoder.go | 4 +- x/committee/simulation/genesis.go | 2 +- x/committee/types/codec.go | 2 +- x/committee/types/committee_test.go | 26 +++++------- x/committee/types/errors.go | 55 +++++------------------- x/committee/types/msg.go | 11 ++--- x/committee/types/permissions.go | 3 +- x/committee/types/permissions_test.go | 46 +++++++++----------- x/committee/types/proposal.go | 15 +++---- 21 files changed, 226 insertions(+), 268 deletions(-) diff --git a/app/app.go b/app/app.go index 5d628acf..ba62c3c2 100644 --- a/app/app.go +++ b/app/app.go @@ -4,7 +4,6 @@ import ( "io" "os" - abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" tmos "github.com/tendermint/tendermint/libs/os" @@ -39,7 +38,6 @@ import ( "github.com/kava-labs/kava/x/kavadist" "github.com/kava-labs/kava/x/pricefeed" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" - ) const ( @@ -261,8 +259,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.committeeKeeper = committee.NewKeeper( app.cdc, keys[committee.StoreKey], - committeeGovRouter, - committee.DefaultCodespace, // TODO blacklist module addresses?) + committeeGovRouter, // TODO blacklist module addresses? ) // create gov keeper with router @@ -356,7 +353,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper), kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper), incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper), - committee.NewAppModule(app.committeeKeeper), + committee.NewAppModule(app.committeeKeeper, app.accountKeeper), ) // During begin block slashing happens after distr.BeginBlocker so that @@ -364,7 +361,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // CanWithdrawInvariant invariant. // Auction.BeginBlocker will close out expired auctions and pay debt back to cdp. // So it should be run before cdp.BeginBlocker which cancels out debt with stable and starts more auctions. - app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName, bep3.ModuleName, incentive.ModuleName committee.ModuleName) + app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName, bep3.ModuleName, incentive.ModuleName, committee.ModuleName) app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName) @@ -374,7 +371,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, staking.ModuleName, bank.ModuleName, slashing.ModuleName, gov.ModuleName, mint.ModuleName, evidence.ModuleName, pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, - bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName + bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName, supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis crisis.ModuleName, // runs the invariants at genesis - should run after other modules genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts. diff --git a/x/committee/alias.go b/x/committee/alias.go index 996dddfc..17138e4b 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -16,12 +16,6 @@ const ( AttributeValueProposalFailed = types.AttributeValueProposalFailed AttributeValueProposalPassed = types.AttributeValueProposalPassed AttributeValueProposalTimeout = types.AttributeValueProposalTimeout - CodeInvalidCommittee = types.CodeInvalidCommittee - CodeInvalidGenesis = types.CodeInvalidGenesis - CodeInvalidProposal = types.CodeInvalidProposal - CodeProposalExpired = types.CodeProposalExpired - CodeUnknownItem = types.CodeUnknownItem - DefaultCodespace = types.DefaultCodespace DefaultNextProposalID = types.DefaultNextProposalID DefaultParamspace = types.DefaultParamspace EventTypeProposalClose = types.EventTypeProposalClose @@ -50,14 +44,6 @@ var ( NewKeeper = keeper.NewKeeper NewQuerier = keeper.NewQuerier DefaultGenesisState = types.DefaultGenesisState - ErrInvalidCommittee = types.ErrInvalidCommittee - ErrInvalidGenesis = types.ErrInvalidGenesis - ErrInvalidPubProposal = types.ErrInvalidPubProposal - ErrNoProposalHandlerExists = types.ErrNoProposalHandlerExists - ErrProposalExpired = types.ErrProposalExpired - ErrUnknownCommittee = types.ErrUnknownCommittee - ErrUnknownProposal = types.ErrUnknownProposal - ErrUnknownVote = types.ErrUnknownVote GetKeyFromID = types.GetKeyFromID GetVoteKey = types.GetVoteKey NewCommittee = types.NewCommittee @@ -66,6 +52,7 @@ var ( NewGenesisState = types.NewGenesisState NewMsgSubmitProposal = types.NewMsgSubmitProposal NewMsgVote = types.NewMsgVote + NewProposal = types.NewProposal NewQueryCommitteeParams = types.NewQueryCommitteeParams NewQueryProposalParams = types.NewQueryProposalParams NewQueryVoteParams = types.NewQueryVoteParams @@ -75,12 +62,20 @@ var ( Uint64FromBytes = types.Uint64FromBytes // variable aliases - ProposalHandler = client.ProposalHandler - CommitteeKeyPrefix = types.CommitteeKeyPrefix - ModuleCdc = types.ModuleCdc - NextProposalIDKey = types.NextProposalIDKey - ProposalKeyPrefix = types.ProposalKeyPrefix - VoteKeyPrefix = types.VoteKeyPrefix + ProposalHandler = client.ProposalHandler + CommitteeKeyPrefix = types.CommitteeKeyPrefix + ErrInvalidCommittee = types.ErrInvalidCommittee + ErrInvalidGenesis = types.ErrInvalidGenesis + ErrInvalidPubProposal = types.ErrInvalidPubProposal + ErrNoProposalHandlerExists = types.ErrNoProposalHandlerExists + ErrProposalExpired = types.ErrProposalExpired + ErrUnknownCommittee = types.ErrUnknownCommittee + ErrUnknownProposal = types.ErrUnknownProposal + ErrUnknownVote = types.ErrUnknownVote + ModuleCdc = types.ModuleCdc + NextProposalIDKey = types.NextProposalIDKey + ProposalKeyPrefix = types.ProposalKeyPrefix + VoteKeyPrefix = types.VoteKeyPrefix ) type ( @@ -101,5 +96,6 @@ type ( QueryCommitteeParams = types.QueryCommitteeParams QueryProposalParams = types.QueryProposalParams QueryVoteParams = types.QueryVoteParams + TextPermission = types.TextPermission Vote = types.Vote ) diff --git a/x/committee/client/cli/query.go b/x/committee/client/cli/query.go index e97a8499..68928e11 100644 --- a/x/committee/client/cli/query.go +++ b/x/committee/client/cli/query.go @@ -8,6 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/version" @@ -25,7 +26,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { RunE: client.ValidateCmd, } - queryCmd.AddCommand(client.GetCommands( + queryCmd.AddCommand(flags.GetCommands( // committees GetCmdQueryCommittee(queryRoute, cdc), GetCmdQueryCommittees(queryRoute, cdc), diff --git a/x/committee/client/cli/tx.go b/x/committee/client/cli/tx.go index e3fbbd94..ea8fc273 100644 --- a/x/committee/client/cli/tx.go +++ b/x/committee/client/cli/tx.go @@ -1,6 +1,7 @@ package cli import ( + "bufio" "fmt" "io/ioutil" "strconv" @@ -10,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/version" @@ -31,7 +33,7 @@ func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command { RunE: client.ValidateCmd, } - txCmd.AddCommand(client.PostCommands( + txCmd.AddCommand(flags.PostCommands( GetCmdVote(cdc), GetCmdSubmitProposal(cdc), )...) @@ -53,7 +55,8 @@ For example: Args: cobra.ExactArgs(2), Example: fmt.Sprintf("%s tx %s submit-proposal 1 your-proposal.json", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { - txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) cliCtx := context.NewCLIContext().WithCodec(cdc) // Get proposing address @@ -102,7 +105,8 @@ func GetCmdVote(cdc *codec.Codec) *cobra.Command { Long: "Submit a yes vote for the proposal with id [proposal-id].", Example: fmt.Sprintf("%s tx %s vote 2", version.ClientName, types.ModuleName), RunE: func(cmd *cobra.Command, args []string) error { - txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) cliCtx := context.NewCLIContext().WithCodec(cdc) // Get voting address @@ -142,7 +146,8 @@ and to delete a committee: `, MustGetExampleCommitteeChangeProposal(cdc), MustGetExampleCommitteeDeleteProposal(cdc)), Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) cliCtx := context.NewCLIContext().WithCodec(cdc) // Get proposing address diff --git a/x/committee/handler.go b/x/committee/handler.go index 23210d43..9129d4e1 100644 --- a/x/committee/handler.go +++ b/x/committee/handler.go @@ -4,6 +4,7 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/kava-labs/kava/x/committee/keeper" "github.com/kava-labs/kava/x/committee/types" @@ -11,7 +12,7 @@ import ( // NewHandler creates an sdk.Handler for committee messages func NewHandler(k keeper.Keeper) sdk.Handler { - return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { ctx = ctx.WithEventManager(sdk.NewEventManager()) switch msg := msg.(type) { @@ -20,16 +21,15 @@ func NewHandler(k keeper.Keeper) sdk.Handler { case types.MsgVote: return handleMsgVote(ctx, k, msg) default: - errMsg := fmt.Sprintf("unrecognized %s msg type: %T", types.ModuleName, msg) - return sdk.ErrUnknownRequest(errMsg).Result() + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg) } } } -func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubmitProposal) sdk.Result { +func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubmitProposal) (*sdk.Result, error) { proposalID, err := k.SubmitProposal(ctx, msg.Proposer, msg.CommitteeID, msg.PubProposal) if err != nil { - return err.Result() + return nil, err } ctx.EventManager().EmitEvent( @@ -40,28 +40,28 @@ func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubm ), ) - return sdk.Result{ + return &sdk.Result{ Data: GetKeyFromID(proposalID), Events: ctx.EventManager().Events(), - } + }, nil } -func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Result { +func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) (*sdk.Result, error) { // get the proposal just to add fields to the event proposal, found := k.GetProposal(ctx, msg.ProposalID) if !found { - return ErrUnknownProposal(DefaultCodespace, msg.ProposalID).Result() + return nil, sdkerrors.Wrapf(ErrUnknownProposal, "%d", msg.ProposalID) } err := k.AddVote(ctx, msg.ProposalID, msg.Voter) if err != nil { - return err.Result() + return nil, err } // Enact a proposal if it has enough votes passes, err := k.GetProposalResult(ctx, msg.ProposalID) if err != nil { - return err.Result() + return nil, err } if passes { err = k.EnactProposal(ctx, msg.ProposalID) @@ -89,5 +89,5 @@ func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) sdk.Resu ), ) - return sdk.Result{Events: ctx.EventManager().Events()} + return &sdk.Result{Events: ctx.EventManager().Events()}, nil } diff --git a/x/committee/handler_test.go b/x/committee/handler_test.go index 69dca714..7ae9d781 100644 --- a/x/committee/handler_test.go +++ b/x/committee/handler_test.go @@ -20,7 +20,7 @@ import ( // NewDistributionGenesisWithPool creates a default distribution genesis state with some coins in the community pool. func NewDistributionGenesisWithPool(communityPoolCoins sdk.Coins) app.GenesisState { gs := distribution.DefaultGenesisState() - gs.FeePool = distribution.FeePool{CommunityPool: sdk.NewDecCoins(communityPoolCoins)} + gs.FeePool = distribution.FeePool{CommunityPool: sdk.NewDecCoinsFromCoins(communityPoolCoins...)} return app.GenesisState{distribution.ModuleName: distribution.ModuleCdc.MustMarshalJSON(gs)} } @@ -81,9 +81,9 @@ func (suite *HandlerTestSuite) TestSubmitProposalMsg_Valid() { 1, ) - res := suite.handler(suite.ctx, msg) + res, err := suite.handler(suite.ctx, msg) - suite.True(res.IsOK()) + suite.NoError(err) _, found := suite.keeper.GetProposal(suite.ctx, types.Uint64FromBytes(res.Data)) suite.True(found) } @@ -104,9 +104,9 @@ func (suite *HandlerTestSuite) TestSubmitProposalMsg_Invalid() { committeeID, ) - res := suite.handler(suite.ctx, msg) + _, err := suite.handler(suite.ctx, msg) - suite.False(res.IsOK()) + suite.Error(err) suite.Empty( suite.keeper.GetProposalsByCommittee(suite.ctx, committeeID), "proposal found when none should exist", @@ -122,9 +122,9 @@ func (suite *HandlerTestSuite) TestSubmitProposalMsg_Unregistered() { committeeID, ) - res := suite.handler(suite.ctx, msg) + _, err := suite.handler(suite.ctx, msg) - suite.False(res.IsOK()) + suite.Error(err) suite.Empty( suite.keeper.GetProposalsByCommittee(suite.ctx, committeeID), "proposal found when none should exist", @@ -147,16 +147,16 @@ func (suite *HandlerTestSuite) TestMsgAddVote_ProposalPass() { suite.addresses[0], 1, ) - res := suite.handler(suite.ctx, msg) - suite.True(res.IsOK()) + res, err := suite.handler(suite.ctx, msg) + suite.NoError(err) proposalID := types.Uint64FromBytes(res.Data) - res = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID)) - suite.True(res.IsOK()) + _, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID)) + suite.NoError(err) // Add a vote to make the proposal pass - res = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID)) + _, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID)) - suite.True(res.IsOK()) + suite.NoError(err) // Check the param has been updated suite.Equal(newDebtThreshold, suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold) // Check proposal and votes are gone @@ -181,19 +181,19 @@ func (suite *HandlerTestSuite) TestMsgAddVote_ProposalFail() { suite.addresses[0], 1, ) - res := suite.handler(suite.ctx, msg) - suite.True(res.IsOK()) + res, err := suite.handler(suite.ctx, msg) + suite.NoError(err) proposalID := types.Uint64FromBytes(res.Data) - res = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID)) - suite.True(res.IsOK()) + _, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID)) + suite.NoError(err) // invalidate the proposal by emptying community pool suite.app.GetDistrKeeper().DistributeFromFeePool(suite.ctx, suite.communityPoolAmt, suite.addresses[0]) // Add a vote to make the proposal pass - res = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID)) + _, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID)) - suite.True(res.IsOK()) + suite.NoError(err) // Check the proposal was not enacted suite.Equal(recipientCoins, suite.app.GetBankKeeper().GetCoins(suite.ctx, recipient)) // Check proposal and votes are gone diff --git a/x/committee/integration_test.go b/x/committee/integration_test.go index a26d7300..daaf4d8f 100644 --- a/x/committee/integration_test.go +++ b/x/committee/integration_test.go @@ -24,9 +24,9 @@ var _ types.PubProposal = UnregisteredPubProposal{} // UnregisteredPubProposal is a pubproposal type that is not registered on the amino codec. type UnregisteredPubProposal struct{} -func (UnregisteredPubProposal) GetTitle() string { return "unregistered" } -func (UnregisteredPubProposal) GetDescription() string { return "unregistered" } -func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" } -func (UnregisteredPubProposal) ProposalType() string { return "unregistered" } -func (UnregisteredPubProposal) ValidateBasic() sdk.Error { return nil } -func (UnregisteredPubProposal) String() string { return "unregistered" } +func (UnregisteredPubProposal) GetTitle() string { return "unregistered" } +func (UnregisteredPubProposal) GetDescription() string { return "unregistered" } +func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" } +func (UnregisteredPubProposal) ProposalType() string { return "unregistered" } +func (UnregisteredPubProposal) ValidateBasic() error { return nil } +func (UnregisteredPubProposal) String() string { return "unregistered" } diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 1ad4815a..e6b3d9c8 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -6,30 +6,29 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/kava-labs/kava/x/committee/types" ) type Keeper struct { - cdc *codec.Codec - storeKey sdk.StoreKey - codespace sdk.CodespaceType + cdc *codec.Codec + storeKey sdk.StoreKey // Proposal router router govtypes.Router } -func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router, codespace sdk.CodespaceType) Keeper { +func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) Keeper { // Logic in the keeper methods assume the set of gov handlers is fixed. // So the gov router must be sealed so no handlers can be added or removed after the keeper is created. router.Seal() return Keeper{ - cdc: cdc, - storeKey: storeKey, - codespace: codespace, - router: router, + cdc: cdc, + storeKey: storeKey, + router: router, } } @@ -99,17 +98,17 @@ func (k Keeper) SetNextProposalID(ctx sdk.Context, id uint64) { } // GetNextProposalID reads the next available global ID from store -func (k Keeper) GetNextProposalID(ctx sdk.Context) (uint64, sdk.Error) { +func (k Keeper) GetNextProposalID(ctx sdk.Context) (uint64, error) { store := ctx.KVStore(k.storeKey) bz := store.Get(types.NextProposalIDKey) if bz == nil { - return 0, types.ErrInvalidGenesis(k.codespace, "next proposal ID not set at genesis") + return 0, sdkerrors.Wrap(types.ErrInvalidGenesis, "next proposal ID not set at genesis") } return types.Uint64FromBytes(bz), nil } // IncrementNextProposalID increments the next proposal ID in the store by 1. -func (k Keeper) IncrementNextProposalID(ctx sdk.Context) sdk.Error { +func (k Keeper) IncrementNextProposalID(ctx sdk.Context) error { id, err := k.GetNextProposalID(ctx) if err != nil { return err @@ -119,7 +118,7 @@ func (k Keeper) IncrementNextProposalID(ctx sdk.Context) sdk.Error { } // StoreNewProposal stores a proposal, adding a new ID -func (k Keeper) StoreNewProposal(ctx sdk.Context, pubProposal types.PubProposal, committeeID uint64, deadline time.Time) (uint64, sdk.Error) { +func (k Keeper) StoreNewProposal(ctx sdk.Context, pubProposal types.PubProposal, committeeID uint64, deadline time.Time) (uint64, error) { newProposalID, err := k.GetNextProposalID(ctx) if err != nil { return 0, err diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index f2db48c5..47fbd8b4 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -4,24 +4,25 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/kava-labs/kava/x/committee/types" ) // SubmitProposal adds a proposal to a committee so that it can be voted on. -func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committeeID uint64, pubProposal types.PubProposal) (uint64, sdk.Error) { +func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committeeID uint64, pubProposal types.PubProposal) (uint64, error) { // Limit proposals to only be submitted by committee members com, found := k.GetCommittee(ctx, committeeID) if !found { - return 0, types.ErrUnknownCommittee(k.codespace, committeeID) + return 0, sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", committeeID) } if !com.HasMember(proposer) { - return 0, sdk.ErrUnauthorized("proposer not member of committee") + return 0, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "proposer not member of committee") } // Check committee has permissions to enact proposal. if !com.HasPermissionsFor(pubProposal) { - return 0, sdk.ErrUnauthorized("committee does not have permissions to enact proposal") + return 0, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "committee does not have permissions to enact proposal") } // Check proposal is valid @@ -47,21 +48,22 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ } // AddVote submits a vote on a proposal. -func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) sdk.Error { +func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) error { // Validate pr, found := k.GetProposal(ctx, proposalID) if !found { - return types.ErrUnknownProposal(k.codespace, proposalID) + return sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID) } if pr.HasExpiredBy(ctx.BlockTime()) { - return types.ErrProposalExpired(k.codespace, ctx.BlockTime(), pr.Deadline) + return sdkerrors.Wrapf(types.ErrProposalExpired, "%s ≥ %s", ctx.BlockTime(), pr.Deadline) + } com, found := k.GetCommittee(ctx, pr.CommitteeID) if !found { - return types.ErrUnknownCommittee(k.codespace, pr.CommitteeID) + return sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", pr.CommitteeID) } if !com.HasMember(voter) { - return sdk.ErrUnauthorized("voter must be a member of committee") + return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "voter must be a member of committee") } // Store vote, overwriting any prior vote @@ -78,14 +80,14 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress } // GetProposalResult calculates if a proposal currently has enough votes to pass. -func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64) (bool, sdk.Error) { +func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64) (bool, error) { pr, found := k.GetProposal(ctx, proposalID) if !found { - return false, types.ErrUnknownProposal(k.codespace, proposalID) + return false, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID) } com, found := k.GetCommittee(ctx, pr.CommitteeID) if !found { - return false, types.ErrUnknownCommittee(k.codespace, pr.CommitteeID) + return false, sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", pr.CommitteeID) } numVotes := k.TallyVotes(ctx, proposalID) @@ -104,10 +106,10 @@ func (k Keeper) TallyVotes(ctx sdk.Context, proposalID uint64) int64 { } // EnactProposal makes the changes proposed in a proposal. -func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) sdk.Error { +func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) error { pr, found := k.GetProposal(ctx, proposalID) if !found { - return types.ErrUnknownProposal(k.codespace, proposalID) + return sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID) } if err := k.ValidatePubProposal(ctx, pr.PubProposal); err != nil { @@ -144,16 +146,16 @@ func (k Keeper) CloseExpiredProposals(ctx sdk.Context) { } // ValidatePubProposal checks if a pubproposal is valid. -func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) (returnErr sdk.Error) { +func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) (returnErr error) { if pubProposal == nil { - return types.ErrInvalidPubProposal(k.codespace, "pub proposal cannot be nil") + return sdkerrors.Wrap(types.ErrInvalidPubProposal, "pub proposal cannot be nil") } if err := pubProposal.ValidateBasic(); err != nil { return err } if !k.router.HasRoute(pubProposal.ProposalRoute()) { - return types.ErrNoProposalHandlerExists(k.codespace, pubProposal) + return sdkerrors.Wrapf(types.ErrNoProposalHandlerExists, "%T", pubProposal) } // Run the proposal's changes through the associated handler using a cached version of state to ensure changes are not permanent. @@ -166,7 +168,7 @@ func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubPropos // reference: https://stackoverflow.com/questions/33167282/how-to-return-a-value-in-a-go-function-that-panics?noredirect=1&lq=1 defer func() { if r := recover(); r != nil { - returnErr = types.ErrInvalidPubProposal(k.codespace, fmt.Sprintf("proposal handler panicked: %s", r)) + returnErr = sdkerrors.Wrapf(types.ErrInvalidPubProposal, "proposal handler panicked: %s", r) } }() diff --git a/x/committee/keeper/querier.go b/x/committee/keeper/querier.go index a1a1a002..6e33c7c1 100644 --- a/x/committee/keeper/querier.go +++ b/x/committee/keeper/querier.go @@ -1,10 +1,9 @@ package keeper import ( - "fmt" - "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/x/committee/types" @@ -12,7 +11,7 @@ import ( // NewQuerier creates a new gov Querier instance func NewQuerier(keeper Keeper) sdk.Querier { - return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, sdk.Error) { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) { switch path[0] { case types.QueryCommittees: @@ -31,7 +30,7 @@ func NewQuerier(keeper Keeper) sdk.Querier { return queryTally(ctx, path[1:], req, keeper) default: - return nil, sdk.ErrUnknownRequest(fmt.Sprintf("unknown %s query endpoint", types.ModuleName)) + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName) } } } @@ -40,32 +39,32 @@ func NewQuerier(keeper Keeper) sdk.Querier { // Committees // ------------------------------------------ -func queryCommittees(ctx sdk.Context, path []string, _ abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { +func queryCommittees(ctx sdk.Context, path []string, _ abci.RequestQuery, keeper Keeper) ([]byte, error) { committees := keeper.GetCommittees(ctx) bz, err := codec.MarshalJSONIndent(keeper.cdc, committees) if err != nil { - return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } return bz, nil } -func queryCommittee(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { +func queryCommittee(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) { var params types.QueryCommitteeParams err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) if err != nil { - return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) } committee, found := keeper.GetCommittee(ctx, params.CommitteeID) if !found { - return nil, types.ErrUnknownCommittee(types.DefaultCodespace, params.CommitteeID) + return nil, sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", params.CommitteeID) } bz, err := codec.MarshalJSONIndent(keeper.cdc, committee) if err != nil { - return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } return bz, nil } @@ -74,37 +73,37 @@ func queryCommittee(ctx sdk.Context, path []string, req abci.RequestQuery, keepe // Proposals // ------------------------------------------ -func queryProposals(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { +func queryProposals(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) { var params types.QueryCommitteeParams err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) if err != nil { - return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) } proposals := keeper.GetProposalsByCommittee(ctx, params.CommitteeID) bz, err := codec.MarshalJSONIndent(keeper.cdc, proposals) if err != nil { - return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } return bz, nil } -func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { +func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) { var params types.QueryProposalParams err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) if err != nil { - return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) } proposal, found := keeper.GetProposal(ctx, params.ProposalID) if !found { - return nil, types.ErrUnknownProposal(types.DefaultCodespace, params.ProposalID) + return nil, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", params.ProposalID) } bz, err := codec.MarshalJSONIndent(keeper.cdc, proposal) if err != nil { - return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } return bz, nil } @@ -113,38 +112,38 @@ func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper // Votes // ------------------------------------------ -func queryVotes(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { +func queryVotes(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) { var params types.QueryProposalParams err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) if err != nil { - return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) } votes := keeper.GetVotesByProposal(ctx, params.ProposalID) bz, err := codec.MarshalJSONIndent(keeper.cdc, votes) if err != nil { - return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } return bz, nil } -func queryVote(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { +func queryVote(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) { var params types.QueryVoteParams err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) if err != nil { - return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) } vote, found := keeper.GetVote(ctx, params.ProposalID, params.Voter) if !found { - return nil, types.ErrUnknownVote(types.DefaultCodespace, params.ProposalID, params.Voter) + return nil, sdkerrors.Wrapf(types.ErrUnknownVote, "proposal id: %d, voter: %s", params.ProposalID, params.Voter) } bz, err := codec.MarshalJSONIndent(keeper.cdc, vote) if err != nil { - return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } return bz, nil } @@ -153,22 +152,22 @@ func queryVote(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Kee // Tally // ------------------------------------------ -func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { +func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) { var params types.QueryProposalParams err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) if err != nil { - return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) } _, found := keeper.GetProposal(ctx, params.ProposalID) if !found { - return nil, types.ErrUnknownProposal(types.DefaultCodespace, params.ProposalID) + return nil, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", params.ProposalID) } numVotes := keeper.TallyVotes(ctx, params.ProposalID) bz, err := codec.MarshalJSONIndent(keeper.cdc, numVotes) if err != nil { - return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } return bz, nil } diff --git a/x/committee/module.go b/x/committee/module.go index f39e8c44..1423eff7 100644 --- a/x/committee/module.go +++ b/x/committee/module.go @@ -11,6 +11,7 @@ import ( "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/auth" sim "github.com/cosmos/cosmos-sdk/x/simulation" abci "github.com/tendermint/tendermint/abci/types" @@ -22,7 +23,7 @@ import ( var ( _ module.AppModule = AppModule{} _ module.AppModuleBasic = AppModuleBasic{} - _ module.AppModuleSimulation = AppModuleSimulation{} + _ module.AppModuleSimulation = AppModule{} ) // AppModuleBasic app module basics object @@ -70,39 +71,20 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { //____________________________________________________________________________ -// AppModuleSimulation defines the module simulation functions used by the module. -type AppModuleSimulation struct{} - -// RegisterStoreDecoder registers a decoder for the module's types -func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { - sdr[StoreKey] = simulation.DecodeStore -} - -// GenerateGenesisState creates a randomized GenState of the module -func (AppModuleSimulation) GenerateGenesisState(simState *module.SimulationState) { - simulation.RandomizedGenState(simState) -} - -// RandomizedParams creates randomized param changes for the simulator. -func (AppModuleSimulation) RandomizedParams(r *rand.Rand) []sim.ParamChange { - return simulation.ParamChanges(r) -} - -//____________________________________________________________________________ - // AppModule app module type type AppModule struct { AppModuleBasic - AppModuleSimulation - keeper Keeper + keeper Keeper + accountKeeper auth.AccountKeeper } // NewAppModule creates a new AppModule object -func NewAppModule(keeper Keeper) AppModule { +func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper) AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{}, keeper: keeper, + accountKeeper: accountKeeper, } } @@ -116,7 +98,7 @@ func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} // Route module message route name func (AppModule) Route() string { - return ModuleName + return RouterKey } // NewHandler module handler @@ -126,7 +108,7 @@ func (am AppModule) NewHandler() sdk.Handler { // QuerierRoute module querier route name func (AppModule) QuerierRoute() string { - return ModuleName + return QuerierRoute } // NewQuerierHandler module querier @@ -158,3 +140,30 @@ func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { return []abci.ValidatorUpdate{} } + +//____________________________________________________________________________ + +// GenerateGenesisState creates a randomized GenState for the module +func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// TODO +func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent { + return nil +} + +// RandomizedParams returns functions that generate params for the module. +func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange { + return nil +} + +// RegisterStoreDecoder registers a decoder for the module's types +func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[StoreKey] = simulation.DecodeStore +} + +// WeightedOperations returns the all the auction module operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation { + return nil // TODO simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper) +} diff --git a/x/committee/proposal_handler.go b/x/committee/proposal_handler.go index da5a049e..bae3d4ff 100644 --- a/x/committee/proposal_handler.go +++ b/x/committee/proposal_handler.go @@ -1,14 +1,13 @@ package committee import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 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 { + return func(ctx sdk.Context, content govtypes.Content) error { switch c := content.(type) { case CommitteeChangeProposal: return handleCommitteeChangeProposal(ctx, k, c) @@ -16,15 +15,14 @@ func NewProposalHandler(k Keeper) govtypes.Handler { return handleCommitteeDeleteProposal(ctx, k, c) default: - errMsg := fmt.Sprintf("unrecognized %s proposal content type: %T", ModuleName, c) - return sdk.ErrUnknownRequest(errMsg) + return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s proposal content type: %T", ModuleName, c) } } } -func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeChangeProposal) sdk.Error { +func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeChangeProposal) error { if err := committeeProposal.ValidateBasic(); err != nil { - return ErrInvalidCommittee(DefaultCodespace, err.Error()) + return sdkerrors.Wrap(ErrInvalidPubProposal, err.Error()) } // Remove all committee's ongoing proposals @@ -41,9 +39,9 @@ func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal return nil } -func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeDeleteProposal) sdk.Error { +func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeDeleteProposal) error { if err := committeeProposal.ValidateBasic(); err != nil { - return ErrInvalidPubProposal(DefaultCodespace, err.Error()) + return sdkerrors.Wrap(ErrInvalidPubProposal, err.Error()) } // Remove all committee's ongoing proposals diff --git a/x/committee/simulation/decoder.go b/x/committee/simulation/decoder.go index 2cdd439b..bcbba73f 100644 --- a/x/committee/simulation/decoder.go +++ b/x/committee/simulation/decoder.go @@ -2,11 +2,11 @@ package simulation import ( "github.com/cosmos/cosmos-sdk/codec" - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/kv" ) // DecodeStore unmarshals the KVPair's Value to the corresponding module type -func DecodeStore(cdc *codec.Codec, kvA, kvB cmn.KVPair) string { +func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string { // TODO implement this return "" } diff --git a/x/committee/simulation/genesis.go b/x/committee/simulation/genesis.go index 5f11ea53..22a25439 100644 --- a/x/committee/simulation/genesis.go +++ b/x/committee/simulation/genesis.go @@ -6,7 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/types/module" - "github.com/kava-labs/kava/x/auction/types" + "github.com/kava-labs/kava/x/committee/types" ) // RandomizedGenState generates a random GenesisState for the module diff --git a/x/committee/types/codec.go b/x/committee/types/codec.go index bdac0489..5c246fc0 100644 --- a/x/committee/types/codec.go +++ b/x/committee/types/codec.go @@ -24,7 +24,6 @@ func RegisterModuleCodec(cdc *codec.Codec) { 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) RegisterAppCodec(cdc) } @@ -43,6 +42,7 @@ func RegisterAppCodec(cdc *codec.Codec) { cdc.RegisterInterface((*Permission)(nil), nil) cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil) cdc.RegisterConcrete(ParamChangePermission{}, "kava/ParamChangePermission", nil) + cdc.RegisterConcrete(TextPermission{}, "kava/TextPermission", nil) // Msgs cdc.RegisterConcrete(MsgSubmitProposal{}, "kava/MsgSubmitProposal", nil) diff --git a/x/committee/types/committee_test.go b/x/committee/types/committee_test.go index 1f7ec077..434b2049 100644 --- a/x/committee/types/committee_test.go +++ b/x/committee/types/committee_test.go @@ -37,7 +37,6 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", }, }}}, pubProposal: params.NewParameterChangeProposal( @@ -47,8 +46,8 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", - Value: `{"denom": "usdx", "amount": "1000000"}`, + + Value: `{"denom": "usdx", "amount": "1000000"}`, }, }, ), @@ -62,7 +61,6 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", }, }}, TextPermission{}, @@ -78,7 +76,6 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", }, }}, GodPermission{}, @@ -90,8 +87,8 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { Subspace: "cdp", Key: "CollateralParams", - Subkey: "", - Value: `[]`, + + Value: `[]`, }, }, ), @@ -107,8 +104,8 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { Subspace: "cdp", Key: "CollateralParams", - Subkey: "", - Value: `[]`, + + Value: `[]`, }, }, ), @@ -123,7 +120,6 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", }, }}, ParamChangePermission{ @@ -131,7 +127,6 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { Subspace: "cdp", Key: "DebtParams", - Subkey: "", }, }}, }, @@ -142,14 +137,14 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", - Value: `{"denom": "usdx", "amount": "1000000"}`, + + Value: `{"denom": "usdx", "amount": "1000000"}`, }, { Subspace: "cdp", Key: "DebtParams", - Subkey: "", - Value: `[]`, + + Value: `[]`, }, }, ), @@ -163,7 +158,6 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", }, }}, }, diff --git a/x/committee/types/errors.go b/x/committee/types/errors.go index 0fa66126..e64766a4 100644 --- a/x/committee/types/errors.go +++ b/x/committee/types/errors.go @@ -1,50 +1,17 @@ package types import ( - "fmt" - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) -const ( - DefaultCodespace sdk.CodespaceType = ModuleName - - CodeProposalExpired sdk.CodeType = 1 - CodeUnknownItem sdk.CodeType = 2 - CodeInvalidGenesis sdk.CodeType = 3 - CodeInvalidProposal sdk.CodeType = 4 - CodeInvalidCommittee sdk.CodeType = 5 +// TODO nums ok? +var ( + ErrUnknownCommittee = sdkerrors.Register(ModuleName, 2, "committee not found") + ErrInvalidCommittee = sdkerrors.Register(ModuleName, 3, "invalid committee") + ErrUnknownProposal = sdkerrors.Register(ModuleName, 4, "proposal not found") + ErrProposalExpired = sdkerrors.Register(ModuleName, 5, "proposal expired") + ErrInvalidPubProposal = sdkerrors.Register(ModuleName, 6, "invalid pubproposal") + ErrUnknownVote = sdkerrors.Register(ModuleName, 7, "vote not found") + ErrInvalidGenesis = sdkerrors.Register(ModuleName, 8, "invalid genesis") + ErrNoProposalHandlerExists = sdkerrors.Register(ModuleName, 9, "pubproposal has no corresponding handler") ) - -func ErrUnknownCommittee(codespace sdk.CodespaceType, id uint64) sdk.Error { - return sdk.NewError(codespace, CodeUnknownItem, fmt.Sprintf("committee with id '%d' not found", id)) -} - -func ErrInvalidCommittee(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidCommittee, msg) -} - -func ErrUnknownProposal(codespace sdk.CodespaceType, id uint64) sdk.Error { - return sdk.NewError(codespace, CodeUnknownItem, fmt.Sprintf("proposal with id '%d' not found", id)) -} - -func ErrProposalExpired(codespace sdk.CodespaceType, blockTime, expiry time.Time) sdk.Error { - return sdk.NewError(codespace, CodeProposalExpired, fmt.Sprintf("proposal expired at %s, current blocktime %s", expiry, blockTime)) -} - -func ErrInvalidPubProposal(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidProposal, msg) -} - -func ErrUnknownVote(codespace sdk.CodespaceType, proposalID uint64, voter sdk.AccAddress) sdk.Error { - return sdk.NewError(codespace, CodeUnknownItem, fmt.Sprintf("vote with for proposal '%d' and voter %s not found", proposalID, voter)) -} - -func ErrInvalidGenesis(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidGenesis, msg) -} - -func ErrNoProposalHandlerExists(codespace sdk.CodespaceType, content interface{}) sdk.Error { - return sdk.NewError(codespace, CodeUnknownItem, fmt.Sprintf("'%T' does not have a corresponding handler", content)) -} diff --git a/x/committee/types/msg.go b/x/committee/types/msg.go index 5e2b138b..50ac64f6 100644 --- a/x/committee/types/msg.go +++ b/x/committee/types/msg.go @@ -2,6 +2,7 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) const ( @@ -34,12 +35,12 @@ func (msg MsgSubmitProposal) Route() string { return RouterKey } func (msg MsgSubmitProposal) Type() string { return TypeMsgSubmitProposal } // ValidateBasic does a simple validation check that doesn't require access to any other information. -func (msg MsgSubmitProposal) ValidateBasic() sdk.Error { +func (msg MsgSubmitProposal) ValidateBasic() error { if msg.PubProposal == nil { - return ErrInvalidPubProposal(DefaultCodespace, "pub proposal cannot be nil") + return sdkerrors.Wrap(ErrInvalidPubProposal, "pub proposal cannot be nil") } if msg.Proposer.Empty() { - return sdk.ErrInvalidAddress(msg.Proposer.String()) + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "proposer address cannot be empty") } return msg.PubProposal.ValidateBasic() @@ -74,9 +75,9 @@ func (msg MsgVote) Route() string { return RouterKey } func (msg MsgVote) Type() string { return TypeMsgVote } // ValidateBasic does a simple validation check that doesn't require access to any other information. -func (msg MsgVote) ValidateBasic() sdk.Error { +func (msg MsgVote) ValidateBasic() error { if msg.Voter.Empty() { - return sdk.ErrInvalidAddress(msg.Voter.String()) + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "voter address cannot be empty") } return nil } diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index 6608d9bd..1b33a46e 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -76,13 +76,12 @@ func (perm ParamChangePermission) MarshalYAML() (interface{}, error) { type AllowedParam struct { Subspace string `json:"subspace" yaml:"subspace"` Key string `json:"key" yaml:"key"` - Subkey string `json:"subkey,omitempty" yaml:"subkey,omitempty"` } type AllowedParams []AllowedParam func (allowed AllowedParams) Contains(paramChange params.ParamChange) bool { for _, p := range allowed { - if paramChange.Subspace == p.Subspace && paramChange.Key == p.Key && paramChange.Subkey == p.Subkey { + if paramChange.Subspace == p.Subspace && paramChange.Key == p.Key { return true } } diff --git a/x/committee/types/permissions_test.go b/x/committee/types/permissions_test.go index 9794d10a..96a2fa25 100644 --- a/x/committee/types/permissions_test.go +++ b/x/committee/types/permissions_test.go @@ -19,22 +19,18 @@ func (suite *PermissionsTestSuite) SetupTest() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", }, { Subspace: "cdp", Key: "SurplusThreshold", - Subkey: "", }, { Subspace: "cdp", Key: "CollateralParams", - Subkey: "", }, { Subspace: "auction", Key: "BidDuration", - Subkey: "", }, } } @@ -56,8 +52,8 @@ func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", - Value: `{"denom": "usdx", "amount": "1000000"}`, + + Value: `{"denom": "usdx", "amount": "1000000"}`, }, }, ), @@ -73,14 +69,14 @@ func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", - Value: `{"denom": "usdx", "amount": "1000000"}`, + + Value: `{"denom": "usdx", "amount": "1000000"}`, }, { Subspace: "cdp", Key: "CollateralParams", - Subkey: "", - Value: `[]`, + + Value: `[]`, }, }, ), @@ -96,8 +92,8 @@ func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() { { Subspace: "cdp", Key: "GlobalDebtLimit", - Subkey: "", - Value: `{"denom": "usdx", "amount": "1000000000"}`, + + Value: `{"denom": "usdx", "amount": "1000000000"}`, }, }, ), @@ -113,8 +109,8 @@ func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", - Value: `[{"denom": "usdx", "amount": "1000000"}]`, + + Value: `[{"denom": "usdx", "amount": "1000000"}]`, }, }, ), @@ -160,8 +156,8 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { testParam: params.ParamChange{ Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", - Value: `{"denom": "usdx", "amount": "1000000"}`, + + Value: `{"denom": "usdx", "amount": "1000000"}`, }, expectContained: true, }, @@ -171,8 +167,8 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { testParam: params.ParamChange{ Subspace: "", Key: "DebtThreshold", - Subkey: "", - Value: `{"denom": "usdx", "amount": "1000000"}`, + + Value: `{"denom": "usdx", "amount": "1000000"}`, }, expectContained: false, }, @@ -182,8 +178,8 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { testParam: params.ParamChange{ Subspace: "cdp", Key: "", - Subkey: "", - Value: `{"denom": "usdx", "amount": "1000000"}`, + + Value: `{"denom": "usdx", "amount": "1000000"}`, }, expectContained: false, }, @@ -193,8 +189,8 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { testParam: params.ParamChange{ Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", - Value: `{"denom": "usdx", "amount": "1000000"}`, + + Value: `{"denom": "usdx", "amount": "1000000"}`, }, expectContained: false, }, @@ -204,8 +200,8 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { testParam: params.ParamChange{ Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", - Value: `{"denom": "usdx", "amount": "1000000"}`, + + Value: `{"denom": "usdx", "amount": "1000000"}`, }, expectContained: false, }, @@ -256,13 +252,11 @@ func (suite *PermissionsTestSuite) TestTextPermission_Allows() { { Subspace: "cdp", Key: "DebtThreshold", - Subkey: "", Value: `{"denom": "usdx", "amount": "1000000"}`, }, { Subspace: "cdp", Key: "CollateralParams", - Subkey: "", Value: `[]`, }, }, diff --git a/x/committee/types/proposal.go b/x/committee/types/proposal.go index 45bfe0a4..95f073fa 100644 --- a/x/committee/types/proposal.go +++ b/x/committee/types/proposal.go @@ -3,7 +3,7 @@ package types import ( "gopkg.in/yaml.v2" - sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) @@ -48,12 +48,12 @@ func (ccp CommitteeChangeProposal) ProposalRoute() string { return RouterKey } 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 { +func (ccp CommitteeChangeProposal) ValidateBasic() error { + if err := govtypes.ValidateAbstract(ccp); err != nil { return err } if err := ccp.NewCommittee.Validate(); err != nil { - return ErrInvalidCommittee(DefaultCodespace, err.Error()) + return sdkerrors.Wrap(ErrInvalidCommittee, err.Error()) } return nil } @@ -100,11 +100,8 @@ func (cdp CommitteeDeleteProposal) ProposalRoute() string { return RouterKey } 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 +func (cdp CommitteeDeleteProposal) ValidateBasic() error { + return govtypes.ValidateAbstract(cdp) } // String implements the Stringer interface. From 447e7579a8c4ca869468ed2fd18f1a137d231156 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 28 Apr 2020 01:26:00 +0100 Subject: [PATCH 50/54] tidy up codec type registrations --- x/committee/alias.go | 38 +++++++++--------- x/committee/module.go | 2 +- x/committee/types/codec.go | 51 ++++++++++++------------ x/committee/types/committee.go | 9 +++-- x/committee/types/permissions.go | 25 ++++++------ x/committee/types/proposal.go | 29 ++++++-------- x/committee/types/proposal_test.go | 57 ++++++++++++++++++++++++++ x/committee/types/test_test.go | 64 ++++++++++++++++++++++++++++++ 8 files changed, 198 insertions(+), 77 deletions(-) create mode 100644 x/committee/types/proposal_test.go create mode 100644 x/committee/types/test_test.go diff --git a/x/committee/alias.go b/x/committee/alias.go index 17138e4b..5daadb23 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -41,25 +41,25 @@ const ( var ( // function aliases - NewKeeper = keeper.NewKeeper - NewQuerier = keeper.NewQuerier - DefaultGenesisState = types.DefaultGenesisState - GetKeyFromID = types.GetKeyFromID - GetVoteKey = types.GetVoteKey - NewCommittee = types.NewCommittee - NewCommitteeChangeProposal = types.NewCommitteeChangeProposal - NewCommitteeDeleteProposal = types.NewCommitteeDeleteProposal - NewGenesisState = types.NewGenesisState - NewMsgSubmitProposal = types.NewMsgSubmitProposal - NewMsgVote = types.NewMsgVote - NewProposal = types.NewProposal - NewQueryCommitteeParams = types.NewQueryCommitteeParams - NewQueryProposalParams = types.NewQueryProposalParams - NewQueryVoteParams = types.NewQueryVoteParams - RegisterAppCodec = types.RegisterAppCodec - RegisterModuleCodec = types.RegisterModuleCodec - RegisterProposalTypeCodec = types.RegisterProposalTypeCodec - Uint64FromBytes = types.Uint64FromBytes + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + DefaultGenesisState = types.DefaultGenesisState + GetKeyFromID = types.GetKeyFromID + GetVoteKey = types.GetVoteKey + NewCommittee = types.NewCommittee + NewCommitteeChangeProposal = types.NewCommitteeChangeProposal + NewCommitteeDeleteProposal = types.NewCommitteeDeleteProposal + NewGenesisState = types.NewGenesisState + NewMsgSubmitProposal = types.NewMsgSubmitProposal + NewMsgVote = types.NewMsgVote + NewProposal = types.NewProposal + NewQueryCommitteeParams = types.NewQueryCommitteeParams + NewQueryProposalParams = types.NewQueryProposalParams + NewQueryVoteParams = types.NewQueryVoteParams + RegisterCodec = types.RegisterCodec + RegisterPermissionTypeCodec = types.RegisterPermissionTypeCodec + RegisterProposalTypeCodec = types.RegisterProposalTypeCodec + Uint64FromBytes = types.Uint64FromBytes // variable aliases ProposalHandler = client.ProposalHandler diff --git a/x/committee/module.go b/x/committee/module.go index 1423eff7..84f06265 100644 --- a/x/committee/module.go +++ b/x/committee/module.go @@ -36,7 +36,7 @@ func (AppModuleBasic) Name() string { // RegisterCodec register module codec func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { - RegisterAppCodec(cdc) + RegisterCodec(cdc) } // DefaultGenesis default genesis state diff --git a/x/committee/types/codec.go b/x/committee/types/codec.go index 5c246fc0..dcbaddb6 100644 --- a/x/committee/types/codec.go +++ b/x/committee/types/codec.go @@ -2,39 +2,32 @@ package types import ( "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" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" ) -// ModuleCdc generic sealed codec to be used throughout module +// ModuleCdc is a generic codec to be used throughout module var ModuleCdc *codec.Codec func init() { cdc := codec.New() - RegisterModuleCodec(cdc) - ModuleCdc = cdc.Seal() + RegisterCodec(cdc) + ModuleCdc = cdc + // ModuleCdc is not sealed so that other modules can register their own pubproposal and/or permission types. + + // Register external module pubproposal types. Ideally these would be registered within the modules' types pkg init function. + // However registration happens here as a work-around. + RegisterProposalTypeCodec(distrtypes.CommunityPoolSpendProposal{}, "cosmos-sdk/CommunityPoolSpendProposal") + RegisterProposalTypeCodec(paramstypes.ParameterChangeProposal{}, "cosmos-sdk/ParameterChangeProposal") + RegisterProposalTypeCodec(govtypes.TextProposal{}, "cosmos-sdk/TextProposal") } -// TODO decide if not using gov's Content type would be better +// RegisterCodec registers the necessary types for the module +func RegisterCodec(cdc *codec.Codec) { -func RegisterModuleCodec(cdc *codec.Codec) { - cdc.RegisterInterface((*gov.Content)(nil), nil) // registering the Content interface on the ModuleCdc will not conflict with gov. - // Ideally dist and params would register their proposals on here at their init. However don't want to fork 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) - - RegisterAppCodec(cdc) -} - -// RegisterAppCodec registers the necessary types for the module -func RegisterAppCodec(cdc *codec.Codec) { // Proposals - // 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((*PubProposal)(nil), nil) cdc.RegisterConcrete(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal", nil) cdc.RegisterConcrete(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal", nil) @@ -49,9 +42,15 @@ func RegisterAppCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgVote{}, "kava/MsgVote", nil) } -// RegisterProposalTypeCodec registers an external proposal content type defined -// in another module for the internal ModuleCdc. This allows the MsgSubmitProposal -// to be correctly Amino encoded and decoded. +// RegisterPermissionTypeCodec allows external modules to register their own permission types on +// the internal ModuleCdc. This allows the MsgSubmitProposal to be correctly Amino encoded and +// decoded (when the msg contains a CommitteeChangeProposal). +func RegisterPermissionTypeCodec(o interface{}, name string) { + ModuleCdc.RegisterConcrete(o, name, nil) +} + +// RegisterProposalTypeCodec allows external modules to register their own pubproposal types on the +// internal ModuleCdc. This allows the MsgSubmitProposal to be correctly Amino encoded and decoded. func RegisterProposalTypeCodec(o interface{}, name string) { ModuleCdc.RegisterConcrete(o, name, nil) } diff --git a/x/committee/types/committee.go b/x/committee/types/committee.go index fb22a196..c516f2bb 100644 --- a/x/committee/types/committee.go +++ b/x/committee/types/committee.go @@ -5,7 +5,7 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/gov" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "gopkg.in/yaml.v2" ) @@ -96,9 +96,12 @@ func (c Committee) Validate() error { // Proposals // ------------------------------------------ -// PubProposal is an interface that all gov proposals defined in other modules must satisfy. -type PubProposal = gov.Content +// PubProposal is the interface that all proposals must fulfill to be submitted to a committee. +// Proposal types can be created external to this module. For example a ParamChangeProposal, or CommunityPoolSpendProposal. +// It is pinned to the equivalent type in the gov module to create compatability between proposal types. +type PubProposal govtypes.Content +// Proposal is an internal record of a governance proposal submitted to a committee. type Proposal struct { PubProposal `json:"pub_proposal" yaml:"pub_proposal"` ID uint64 `json:"id" yaml:"id"` diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index 1b33a46e..3cfbd5f9 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -1,16 +1,17 @@ package types import ( - "github.com/cosmos/cosmos-sdk/x/gov" - "github.com/cosmos/cosmos-sdk/x/params" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" ) func init() { - // CommitteeChange/Delete proposals need to be registered on gov's ModuleCdc. + // CommitteeChange/Delete proposals are registered on gov's ModuleCdc (see proposal.go). // But since these proposals contain Permissions, these types also need registering: - gov.ModuleCdc.RegisterInterface((*Permission)(nil), nil) - gov.RegisterProposalTypeCodec(GodPermission{}, "kava/GodPermission") - gov.RegisterProposalTypeCodec(ParamChangePermission{}, "kava/ParamChangePermission") + govtypes.ModuleCdc.RegisterInterface((*Permission)(nil), nil) + govtypes.RegisterProposalTypeCodec(GodPermission{}, "kava/GodPermission") + govtypes.RegisterProposalTypeCodec(ParamChangePermission{}, "kava/ParamChangePermission") + govtypes.RegisterProposalTypeCodec(TextPermission{}, "kava/TextPermission") } // Permission is anything with a method that validates whether a proposal is allowed by it or not. @@ -27,7 +28,7 @@ type GodPermission struct{} var _ Permission = GodPermission{} -func (GodPermission) Allows(gov.Content) bool { return true } +func (GodPermission) Allows(PubProposal) bool { return true } func (GodPermission) MarshalYAML() (interface{}, error) { valueToMarshal := struct { @@ -49,8 +50,8 @@ type ParamChangePermission struct { var _ Permission = ParamChangePermission{} -func (perm ParamChangePermission) Allows(p gov.Content) bool { - proposal, ok := p.(params.ParameterChangeProposal) +func (perm ParamChangePermission) Allows(p PubProposal) bool { + proposal, ok := p.(paramstypes.ParameterChangeProposal) if !ok { return false } @@ -79,7 +80,7 @@ type AllowedParam struct { } type AllowedParams []AllowedParam -func (allowed AllowedParams) Contains(paramChange params.ParamChange) bool { +func (allowed AllowedParams) Contains(paramChange paramstypes.ParamChange) bool { for _, p := range allowed { if paramChange.Subspace == p.Subspace && paramChange.Key == p.Key { return true @@ -97,8 +98,8 @@ type TextPermission struct{} var _ Permission = TextPermission{} -func (TextPermission) Allows(p gov.Content) bool { - _, ok := p.(gov.TextProposal) +func (TextPermission) Allows(p PubProposal) bool { + _, ok := p.(govtypes.TextProposal) return ok } diff --git a/x/committee/types/proposal.go b/x/committee/types/proposal.go index 95f073fa..426df42d 100644 --- a/x/committee/types/proposal.go +++ b/x/committee/types/proposal.go @@ -12,19 +12,24 @@ const ( ProposalTypeCommitteeDelete = "CommitteeDelete" ) -// CommitteeChangeProposal is a gov proposal for creating a new committee or modifying an existing one. -type CommitteeChangeProposal struct { - Title string `json:"title" yaml:"title"` - Description string `json:"description" yaml:"description"` - NewCommittee Committee `json:"new_committee" yaml:"new_committee"` -} - -var _ govtypes.Content = CommitteeChangeProposal{} +// ensure proposal types fulfill the PubProposal interface and the gov Content interface. +var _, _ govtypes.Content = CommitteeChangeProposal{}, CommitteeDeleteProposal{} +var _, _ PubProposal = CommitteeChangeProposal{}, CommitteeDeleteProposal{} func init() { // Gov proposals need to be registered on gov's ModuleCdc so MsgSubmitProposal can be encoded. govtypes.RegisterProposalType(ProposalTypeCommitteeChange) govtypes.RegisterProposalTypeCodec(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal") + + govtypes.RegisterProposalType(ProposalTypeCommitteeDelete) + govtypes.RegisterProposalTypeCodec(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal") +} + +// CommitteeChangeProposal is a gov proposal for creating a new committee or modifying an existing one. +type CommitteeChangeProposal struct { + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + NewCommittee Committee `json:"new_committee" yaml:"new_committee"` } func NewCommitteeChangeProposal(title string, description string, newCommittee Committee) CommitteeChangeProposal { @@ -71,14 +76,6 @@ type CommitteeDeleteProposal struct { CommitteeID uint64 `json:"committee_id" yaml:"committee_id"` } -var _ govtypes.Content = CommitteeDeleteProposal{} - -func init() { - // Gov proposals need to be registered on gov's ModuleCdc so MsgSubmitProposal can be encoded. - govtypes.RegisterProposalType(ProposalTypeCommitteeDelete) - govtypes.RegisterProposalTypeCodec(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal") -} - func NewCommitteeDeleteProposal(title string, description string, committeeID uint64) CommitteeDeleteProposal { return CommitteeDeleteProposal{ Title: title, diff --git a/x/committee/types/proposal_test.go b/x/committee/types/proposal_test.go new file mode 100644 index 00000000..0b6ab0e4 --- /dev/null +++ b/x/committee/types/proposal_test.go @@ -0,0 +1,57 @@ +package types + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +func (suite *TypesTestSuite) TestCommitteeChangeProposalMarshals() { + + ccp := CommitteeChangeProposal{ + Title: "A Title", + Description: "A description for this committee.", + NewCommittee: Committee{ + ID: 12, + Description: "This committee is for testing.", + Members: nil, + Permissions: []Permission{ParamChangePermission{}}, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, + }, + } + + appCdc := codec.New() + // register sdk types in case their needed + sdk.RegisterCodec(appCdc) + codec.RegisterCrypto(appCdc) + codec.RegisterEvidences(appCdc) + // register committee types + RegisterCodec(appCdc) + + var ppModuleCdc PubProposal + suite.NotPanics(func() { + ModuleCdc.MustUnmarshalBinaryBare( + ModuleCdc.MustMarshalBinaryBare(PubProposal(ccp)), + &ppModuleCdc, + ) + }) + + var ppAppCdc PubProposal + suite.NotPanics(func() { + appCdc.MustUnmarshalBinaryBare( + appCdc.MustMarshalBinaryBare(PubProposal(ccp)), + &ppAppCdc, + ) + }) + + var ppGovCdc govtypes.Content + suite.NotPanics(func() { + govtypes.ModuleCdc.MustUnmarshalBinaryBare( + govtypes.ModuleCdc.MustMarshalBinaryBare(govtypes.Content(ccp)), + &ppGovCdc, + ) + }) +} diff --git a/x/committee/types/test_test.go b/x/committee/types/test_test.go new file mode 100644 index 00000000..3234a830 --- /dev/null +++ b/x/committee/types/test_test.go @@ -0,0 +1,64 @@ +package types + +import ( + "fmt" + "testing" + + "github.com/cosmos/cosmos-sdk/codec" +) + +type InterA interface { + GetTitle() string +} + +type InterB InterA + +// interface { +// GetDescription() string +// } + +type Prop1 struct{} + +func (p Prop1) GetTitle() string { return "prop1 title" } +func (p Prop1) GetDescription() string { return "prop1 description" } + +type Prop2 struct{} + +func (p Prop2) GetTitle() string { return "prop2 title" } +func (p Prop2) GetDescription() string { return "prop2 description" } + +func TestTest(t *testing.T) { + /* + register content, register new pubproposal + register concrete types (should satisfy both of them) + + try marshalling and unmarshalling all 4 combinations + */ + cdc := codec.New() + + cdc.RegisterInterface((*InterA)(nil), nil) + cdc.RegisterConcrete(Prop1{}, "test/prop1", nil) + cdc.RegisterInterface((*InterB)(nil), nil) + cdc.RegisterConcrete(Prop2{}, "test/prop2", nil) + + p1ia := InterA(Prop1{}) + p2ia := InterA(Prop2{}) + p1ib := InterB(Prop1{}) + p2ib := InterB(Prop2{}) + + var iap1 InterA + cdc.MustUnmarshalBinaryBare(cdc.MustMarshalBinaryBare(p1ia), &iap1) + fmt.Printf("%T, %T\n", p1ia, iap1) + + var iap2 InterA + cdc.MustUnmarshalBinaryBare(cdc.MustMarshalBinaryBare(p2ia), &iap2) + fmt.Printf("%T, %T\n", p2ia, iap2) + + var ibp1 InterB + cdc.MustUnmarshalBinaryBare(cdc.MustMarshalBinaryBare(p1ib), &ibp1) + fmt.Printf("%T, %T\n", p1ib, ibp1) + + var ibp2 InterB + cdc.MustUnmarshalBinaryBare(cdc.MustMarshalBinaryBare(p2ib), &ibp2) + fmt.Printf("%T, %T\n", p2ib, ibp2) +} From 307ecd54e2f47b295e4d91dec0a56f540c99985e Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 28 Apr 2020 01:26:48 +0100 Subject: [PATCH 51/54] remove unecessary codec tests --- x/committee/types/proposal_test.go | 57 -------------------------- x/committee/types/test_test.go | 64 ------------------------------ 2 files changed, 121 deletions(-) delete mode 100644 x/committee/types/proposal_test.go delete mode 100644 x/committee/types/test_test.go diff --git a/x/committee/types/proposal_test.go b/x/committee/types/proposal_test.go deleted file mode 100644 index 0b6ab0e4..00000000 --- a/x/committee/types/proposal_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package types - -import ( - "time" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" -) - -func (suite *TypesTestSuite) TestCommitteeChangeProposalMarshals() { - - ccp := CommitteeChangeProposal{ - Title: "A Title", - Description: "A description for this committee.", - NewCommittee: Committee{ - ID: 12, - Description: "This committee is for testing.", - Members: nil, - Permissions: []Permission{ParamChangePermission{}}, - VoteThreshold: d("0.667"), - ProposalDuration: time.Hour * 24 * 7, - }, - } - - appCdc := codec.New() - // register sdk types in case their needed - sdk.RegisterCodec(appCdc) - codec.RegisterCrypto(appCdc) - codec.RegisterEvidences(appCdc) - // register committee types - RegisterCodec(appCdc) - - var ppModuleCdc PubProposal - suite.NotPanics(func() { - ModuleCdc.MustUnmarshalBinaryBare( - ModuleCdc.MustMarshalBinaryBare(PubProposal(ccp)), - &ppModuleCdc, - ) - }) - - var ppAppCdc PubProposal - suite.NotPanics(func() { - appCdc.MustUnmarshalBinaryBare( - appCdc.MustMarshalBinaryBare(PubProposal(ccp)), - &ppAppCdc, - ) - }) - - var ppGovCdc govtypes.Content - suite.NotPanics(func() { - govtypes.ModuleCdc.MustUnmarshalBinaryBare( - govtypes.ModuleCdc.MustMarshalBinaryBare(govtypes.Content(ccp)), - &ppGovCdc, - ) - }) -} diff --git a/x/committee/types/test_test.go b/x/committee/types/test_test.go deleted file mode 100644 index 3234a830..00000000 --- a/x/committee/types/test_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package types - -import ( - "fmt" - "testing" - - "github.com/cosmos/cosmos-sdk/codec" -) - -type InterA interface { - GetTitle() string -} - -type InterB InterA - -// interface { -// GetDescription() string -// } - -type Prop1 struct{} - -func (p Prop1) GetTitle() string { return "prop1 title" } -func (p Prop1) GetDescription() string { return "prop1 description" } - -type Prop2 struct{} - -func (p Prop2) GetTitle() string { return "prop2 title" } -func (p Prop2) GetDescription() string { return "prop2 description" } - -func TestTest(t *testing.T) { - /* - register content, register new pubproposal - register concrete types (should satisfy both of them) - - try marshalling and unmarshalling all 4 combinations - */ - cdc := codec.New() - - cdc.RegisterInterface((*InterA)(nil), nil) - cdc.RegisterConcrete(Prop1{}, "test/prop1", nil) - cdc.RegisterInterface((*InterB)(nil), nil) - cdc.RegisterConcrete(Prop2{}, "test/prop2", nil) - - p1ia := InterA(Prop1{}) - p2ia := InterA(Prop2{}) - p1ib := InterB(Prop1{}) - p2ib := InterB(Prop2{}) - - var iap1 InterA - cdc.MustUnmarshalBinaryBare(cdc.MustMarshalBinaryBare(p1ia), &iap1) - fmt.Printf("%T, %T\n", p1ia, iap1) - - var iap2 InterA - cdc.MustUnmarshalBinaryBare(cdc.MustMarshalBinaryBare(p2ia), &iap2) - fmt.Printf("%T, %T\n", p2ia, iap2) - - var ibp1 InterB - cdc.MustUnmarshalBinaryBare(cdc.MustMarshalBinaryBare(p1ib), &ibp1) - fmt.Printf("%T, %T\n", p1ib, ibp1) - - var ibp2 InterB - cdc.MustUnmarshalBinaryBare(cdc.MustMarshalBinaryBare(p2ib), &ibp2) - fmt.Printf("%T, %T\n", p2ib, ibp2) -} From d1c0dd18b10e2cf04fc70c00879c8f0adcccfca4 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 28 Apr 2020 01:28:00 +0100 Subject: [PATCH 52/54] only import types pkgs within types --- x/committee/types/committee_test.go | 26 ++++++++-------- x/committee/types/errors.go | 1 - x/committee/types/genesis_test.go | 6 ++-- x/committee/types/msg_test.go | 8 ++--- x/committee/types/permissions_test.go | 45 ++++++++++++++------------- 5 files changed, 43 insertions(+), 43 deletions(-) diff --git a/x/committee/types/committee_test.go b/x/committee/types/committee_test.go index 434b2049..54204292 100644 --- a/x/committee/types/committee_test.go +++ b/x/committee/types/committee_test.go @@ -4,15 +4,15 @@ import ( "testing" "time" - "github.com/cosmos/cosmos-sdk/x/gov" - "github.com/cosmos/cosmos-sdk/x/params" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/stretchr/testify/suite" ) var _ PubProposal = UnregisteredPubProposal{} type UnregisteredPubProposal struct { - gov.TextProposal + govtypes.TextProposal } func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" } @@ -39,10 +39,10 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { Key: "DebtThreshold", }, }}}, - pubProposal: params.NewParameterChangeProposal( + pubProposal: paramstypes.NewParameterChangeProposal( "A Title", "A description of this proposal.", - []params.ParamChange{ + []paramstypes.ParamChange{ { Subspace: "cdp", Key: "DebtThreshold", @@ -65,7 +65,7 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { }}, TextPermission{}, }, - pubProposal: gov.NewTextProposal("A Proposal Title", "A description of this proposal"), + pubProposal: govtypes.NewTextProposal("A Proposal Title", "A description of this proposal"), expectHasPermissions: true, }, { @@ -80,10 +80,10 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { }}, GodPermission{}, }, - pubProposal: params.NewParameterChangeProposal( + pubProposal: paramstypes.NewParameterChangeProposal( "A Title", "A description of this proposal.", - []params.ParamChange{ + []paramstypes.ParamChange{ { Subspace: "cdp", Key: "CollateralParams", @@ -97,10 +97,10 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { { name: "no permissions", permissions: nil, - pubProposal: params.NewParameterChangeProposal( + pubProposal: paramstypes.NewParameterChangeProposal( "A Title", "A description of this proposal.", - []params.ParamChange{ + []paramstypes.ParamChange{ { Subspace: "cdp", Key: "CollateralParams", @@ -130,10 +130,10 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { }, }}, }, - pubProposal: params.NewParameterChangeProposal( + pubProposal: paramstypes.NewParameterChangeProposal( "A Title", "A description of this proposal.", - []params.ParamChange{ + []paramstypes.ParamChange{ { Subspace: "cdp", Key: "DebtThreshold", @@ -161,7 +161,7 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() { }, }}, }, - pubProposal: UnregisteredPubProposal{gov.TextProposal{"A Title", "A description."}}, + pubProposal: UnregisteredPubProposal{govtypes.TextProposal{"A Title", "A description."}}, expectHasPermissions: false, }, } diff --git a/x/committee/types/errors.go b/x/committee/types/errors.go index e64766a4..f5a6fc49 100644 --- a/x/committee/types/errors.go +++ b/x/committee/types/errors.go @@ -4,7 +4,6 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) -// TODO nums ok? var ( ErrUnknownCommittee = sdkerrors.Register(ModuleName, 2, "committee not found") ErrInvalidCommittee = sdkerrors.Register(ModuleName, 3, "invalid committee") diff --git a/x/committee/types/genesis_test.go b/x/committee/types/genesis_test.go index 7fde2202..d1ba83d7 100644 --- a/x/committee/types/genesis_test.go +++ b/x/committee/types/genesis_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/gov" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/tendermint/tendermint/crypto" ) @@ -42,7 +42,7 @@ func TestGenesisState_Validate(t *testing.T) { }, }, Proposals: []Proposal{ - {ID: 1, CommitteeID: 1, PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)}, + {ID: 1, CommitteeID: 1, PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)}, }, Votes: []Vote{ {ProposalID: 1, Voter: addresses[0]}, @@ -114,7 +114,7 @@ func TestGenesisState_Validate(t *testing.T) { testGenesis.Proposals, Proposal{ ID: testGenesis.NextProposalID, - PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."), CommitteeID: 247, // doesn't exist }), Votes: testGenesis.Votes, diff --git a/x/committee/types/msg_test.go b/x/committee/types/msg_test.go index 1a7b15f7..b4c6a585 100644 --- a/x/committee/types/msg_test.go +++ b/x/committee/types/msg_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/gov" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) func TestMsgSubmitProposal_ValidateBasic(t *testing.T) { @@ -18,17 +18,17 @@ func TestMsgSubmitProposal_ValidateBasic(t *testing.T) { }{ { name: "normal", - msg: MsgSubmitProposal{gov.NewTextProposal("A Title", "A proposal description."), addr, 3}, + msg: MsgSubmitProposal{govtypes.NewTextProposal("A Title", "A proposal description."), addr, 3}, expectPass: true, }, { name: "empty address", - msg: MsgSubmitProposal{gov.NewTextProposal("A Title", "A proposal description."), nil, 3}, + msg: MsgSubmitProposal{govtypes.NewTextProposal("A Title", "A proposal description."), nil, 3}, expectPass: false, }, { name: "invalid proposal", - msg: MsgSubmitProposal{gov.TextProposal{}, addr, 3}, + msg: MsgSubmitProposal{govtypes.TextProposal{}, addr, 3}, expectPass: false, }, } diff --git a/x/committee/types/permissions_test.go b/x/committee/types/permissions_test.go index 96a2fa25..8218985f 100644 --- a/x/committee/types/permissions_test.go +++ b/x/committee/types/permissions_test.go @@ -3,8 +3,8 @@ package types import ( "testing" - "github.com/cosmos/cosmos-sdk/x/gov" - "github.com/cosmos/cosmos-sdk/x/params" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/stretchr/testify/suite" ) @@ -45,10 +45,10 @@ func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() { { name: "normal (single param)", allowedParams: suite.exampleAllowedParams, - pubProposal: params.NewParameterChangeProposal( + pubProposal: paramstypes.NewParameterChangeProposal( "A Title", "A description for this proposal.", - []params.ParamChange{ + []paramstypes.ParamChange{ { Subspace: "cdp", Key: "DebtThreshold", @@ -62,10 +62,10 @@ func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() { { name: "normal (multiple params)", allowedParams: suite.exampleAllowedParams, - pubProposal: params.NewParameterChangeProposal( + pubProposal: paramstypes.NewParameterChangeProposal( "A Title", "A description for this proposal.", - []params.ParamChange{ + []paramstypes.ParamChange{ { Subspace: "cdp", Key: "DebtThreshold", @@ -85,10 +85,10 @@ func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() { { name: "not allowed (not in list)", allowedParams: suite.exampleAllowedParams, - pubProposal: params.NewParameterChangeProposal( + pubProposal: paramstypes.NewParameterChangeProposal( "A Title", "A description for this proposal.", - []params.ParamChange{ + []paramstypes.ParamChange{ { Subspace: "cdp", Key: "GlobalDebtLimit", @@ -102,10 +102,10 @@ func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() { { name: "not allowed (nil allowed params)", allowedParams: nil, - pubProposal: params.NewParameterChangeProposal( + pubProposal: paramstypes.NewParameterChangeProposal( "A Title", "A description for this proposal.", - []params.ParamChange{ + []paramstypes.ParamChange{ { Subspace: "cdp", Key: "DebtThreshold", @@ -119,7 +119,7 @@ func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() { { name: "not allowed (mismatched pubproposal type)", allowedParams: suite.exampleAllowedParams, - pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), + pubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."), expectAllowed: false, }, { @@ -147,13 +147,13 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { testcases := []struct { name string allowedParams AllowedParams - testParam params.ParamChange + testParam paramstypes.ParamChange expectContained bool }{ { name: "normal", allowedParams: suite.exampleAllowedParams, - testParam: params.ParamChange{ + testParam: paramstypes.ParamChange{ Subspace: "cdp", Key: "DebtThreshold", @@ -164,7 +164,7 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { { name: "missing subspace", allowedParams: suite.exampleAllowedParams, - testParam: params.ParamChange{ + testParam: paramstypes.ParamChange{ Subspace: "", Key: "DebtThreshold", @@ -175,7 +175,7 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { { name: "missing key", allowedParams: suite.exampleAllowedParams, - testParam: params.ParamChange{ + testParam: paramstypes.ParamChange{ Subspace: "cdp", Key: "", @@ -186,7 +186,7 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { { name: "empty list", allowedParams: AllowedParams{}, - testParam: params.ParamChange{ + testParam: paramstypes.ParamChange{ Subspace: "cdp", Key: "DebtThreshold", @@ -197,7 +197,7 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { { name: "nil list", allowedParams: nil, - testParam: params.ParamChange{ + testParam: paramstypes.ParamChange{ Subspace: "cdp", Key: "DebtThreshold", @@ -208,13 +208,13 @@ func (suite *PermissionsTestSuite) TestAllowedParams_Contains() { { name: "no param change", allowedParams: suite.exampleAllowedParams, - testParam: params.ParamChange{}, + testParam: paramstypes.ParamChange{}, expectContained: false, }, { name: "empty list and no param change", allowedParams: AllowedParams{}, - testParam: params.ParamChange{}, + testParam: paramstypes.ParamChange{}, expectContained: false, }, } @@ -237,7 +237,7 @@ func (suite *PermissionsTestSuite) TestTextPermission_Allows() { }{ { name: "normal", - pubProposal: gov.NewTextProposal( + pubProposal: govtypes.NewTextProposal( "A Title", "A description for this proposal.", ), @@ -245,10 +245,10 @@ func (suite *PermissionsTestSuite) TestTextPermission_Allows() { }, { name: "not allowed (wrong pubproposal type)", - pubProposal: params.NewParameterChangeProposal( + pubProposal: paramstypes.NewParameterChangeProposal( "A Title", "A description for this proposal.", - []params.ParamChange{ + []paramstypes.ParamChange{ { Subspace: "cdp", Key: "DebtThreshold", @@ -280,6 +280,7 @@ func (suite *PermissionsTestSuite) TestTextPermission_Allows() { }) } } + func TestPermissionsTestSuite(t *testing.T) { suite.Run(t, new(PermissionsTestSuite)) } From ffbeced199cfc2adfa486b9bf1ec3df932dc546c Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 28 Apr 2020 01:51:53 +0100 Subject: [PATCH 53/54] apply various pr suggestions --- x/committee/handler.go | 50 ++++++++++++++++++---------------- x/committee/types/committee.go | 2 +- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/x/committee/handler.go b/x/committee/handler.go index 9129d4e1..e46c0b6a 100644 --- a/x/committee/handler.go +++ b/x/committee/handler.go @@ -57,30 +57,6 @@ func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) (*sdk.Re if err != nil { return nil, err } - - // Enact a proposal if it has enough votes - passes, err := k.GetProposalResult(ctx, msg.ProposalID) - if err != nil { - return nil, err - } - if passes { - err = k.EnactProposal(ctx, msg.ProposalID) - outcome := types.AttributeValueProposalPassed - if err != nil { - outcome = types.AttributeValueProposalFailed - } - k.DeleteProposalAndVotes(ctx, msg.ProposalID) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeProposalClose, - sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)), - sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)), - sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, outcome), - ), - ) - } - ctx.EventManager().EmitEvent( sdk.NewEvent( sdk.EventTypeMessage, @@ -89,5 +65,31 @@ func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) (*sdk.Re ), ) + // Enact a proposal if it has enough votes + passes, err := k.GetProposalResult(ctx, msg.ProposalID) + if err != nil { + return nil, err + } + if !passes { + return &sdk.Result{Events: ctx.EventManager().Events()}, nil + } + + err = k.EnactProposal(ctx, msg.ProposalID) + outcome := types.AttributeValueProposalPassed + if err != nil { + outcome = types.AttributeValueProposalFailed + } + + k.DeleteProposalAndVotes(ctx, msg.ProposalID) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeProposalClose, + sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)), + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)), + sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, outcome), + ), + ) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil } diff --git a/x/committee/types/committee.go b/x/committee/types/committee.go index c516f2bb..c240ac52 100644 --- a/x/committee/types/committee.go +++ b/x/committee/types/committee.go @@ -9,7 +9,7 @@ import ( "gopkg.in/yaml.v2" ) -const MaxCommitteeDescriptionLength int = 5000 +const MaxCommitteeDescriptionLength int = 512 // ------------------------------------------ // Committees From e1cb079589491966b09233674604a6c0feb29275 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 28 Apr 2020 16:36:06 +0100 Subject: [PATCH 54/54] remove alias comment from godoc --- x/committee/alias.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/committee/alias.go b/x/committee/alias.go index 5daadb23..5bc4c1ea 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -1,7 +1,7 @@ -// nolint -// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen) package committee +// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen) + import ( "github.com/kava-labs/kava/x/committee/client" "github.com/kava-labs/kava/x/committee/keeper"