Skip to content
This repository has been archived by the owner on Aug 3, 2023. It is now read-only.

Group spec #14

Merged
merged 8 commits into from
Feb 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions incubator/group/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
vendor/
7 changes: 7 additions & 0 deletions incubator/group/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.PHONY: vendor proto-gen

vendor:
go mod vendor

proto-gen: vendor
./protocgen.sh
67 changes: 67 additions & 0 deletions incubator/group/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Group Module

## Group

A group is simply an aggregation of accounts with associated weights. It is not
an account and doesn't have a balance. It doesn't in and of itself have any
sort of voting or decision power. It does have an "administrator" which has
the power to add, remove and update members in the group. Note that a
group account could be an administrator of a group.

## Group Account

A group account is an account associated with a group and a decision policy.
Group accounts are abstracted from groups because a single group may have
multiple decision policies for different types of actions. Managing group
membership separately from decision policies results in the least overhead
and keeps membership consistent across different policies. The pattern that
is recommended is to have a single master group account for a given group,
and then to create separate group accounts with different decision policies
and delegate the desired permissions to from the master account to
those "sub-accounts" using the `msg_authorization` module.


## Decision Policy

A decision policy is the mechanism by which members of a group can vote on
proposals.

All decision policies generally would have a minimum and maximum voting window.
The minimum voting window is the minimum amount of time that must pass in order
for a proposal to potentially pass, and it may be set to 0. The maximum voting
window is the maximum time that a proposal may be voted on before it is closed.
Both of these values must be less than a chain-wide max voting window parameter.

aaronc marked this conversation as resolved.
Show resolved Hide resolved
### Threshold decision policy
aaronc marked this conversation as resolved.
Show resolved Hide resolved

A threshold decision policy defines a threshold of yes votes (based on a tally
of voter weights) that must be achieved in order for a proposal to pass. For
this decision policy, abstain and veto are simply treated as no's.

## Proposal

Any member of a group can submit a proposal for a group account to decide upon.
A proposal consists of a set of `sdk.Msg`s that will be executed if the proposal
passes as well as any comment associated with the proposal.

## Voting

There are four choices to choose while voting - yes, no, abstain and veto. Not
all decision policies will support them. Votes can contain an optional comment.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just stating the obvious (?)

  • only group members can vote
  • voting is only allowed within the voting window
  • only 1 "valid" vote per group member (if multiple votes are allowed then last one is "valid")

Can members change their vote within the voting window?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that members should be able to change their vote within the voting window. Does that seem okay?

During the voting window, accounts that have already voted may change their vote.
In the current implementation, the voting window begins as soon as a proposal
is submitted.

## Executing Proposals

Proposals will not be automatically executed by the chain in this current design,
but rather a user must submit a `MsgExec` transaction to attempt to execute the
proposal based on the current votes and decision policy. A future upgrade could
automate this propose and have the group account (or a fee granter) pay.

## Changing Group Membership

In the current implementation, changing a group's membership (adding or removing members or changing their power)
will cause all existing proposals for group accounts linked to this group
to be invalidated. They will simply fail if someone calls `MsgExec` and will
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this strict rule to start with although I want to highlight a communication issue for the clients. They may see a list of open proposals while some or all of them may be invalidated already.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to do cleanup somewhere.
I would propose to mark all open proposals as closed when membership changes. We could do it lazy with a special (close proposal) transaction that if it is invalid (too old, changed group) and then trigger a close like that.

Not sure which is preferable, but definitely some way to fix up the ux

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So closing triggered by a change in group membership could potentially have high gas costs.

There could be an EndBlocker that takes care of this.

For now, I think I will add a version field to proposal which specifies the version of the group this is valid for. A GetOpenProposals query endpoint could then do the filtering for clients based on the current version.

eventually be garbage collected.
14 changes: 14 additions & 0 deletions incubator/group/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module github.com/cosmos/modules/incubator/group

go 1.13

require (
github.com/cosmos/cosmos-sdk v0.34.4-0.20200211145837-56c586897525
github.com/cosmos/modules/incubator/orm v0.0.0-20200117100147-88228b5fa693
github.com/gogo/protobuf v1.3.1
)

replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.2-alpha.regen.1

//replace github.com/cosmos/modules/incubator/orm => github.com/regen-network/cosmos-modules/incubator/orm v0.0.0-20200206151518-3155fe39bfb9
replace github.com/cosmos/modules/incubator/orm => ../orm
355 changes: 355 additions & 0 deletions incubator/group/go.sum

Large diffs are not rendered by default.

181 changes: 181 additions & 0 deletions incubator/group/keeper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package group

import (
"encoding/binary"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/modules/incubator/orm"
)

type keeper struct {
key sdk.StoreKey

// Group Table
groupTable orm.AutoUInt64Table
groupByAdminIndex orm.Index

// Group Member Table
groupMemberTable orm.NaturalKeyTable
groupMemberByGroupIndex *orm.UInt64Index
groupMemberByMemberIndex orm.Index

// Group Account Table
groupAccountTable orm.NaturalKeyTable
groupAccountByGroupIndex *orm.UInt64Index
groupAccountByAdminIndex orm.Index

// Proposal Table
proposalTable orm.AutoUInt64Table
proposalByGroupAccountIndex orm.Index
proposalByProposerIndex orm.Index

// Vote Table
voteTable orm.NaturalKeyTable
voteByProposalIndex *orm.UInt64Index
voteByVoterIndex orm.Index
}

func (g GroupMember) NaturalKey() []byte {
result := make([]byte, 0, 8 + len(g.Member))
// TODO: append uint64 to result as BigEndian
result = append(result, g.Member...)
return result
}

func (g GroupAccountMetadata) NaturalKey() []byte {
return g.GroupAccount
}

func (v Vote) NaturalKey() []byte {
result := make([]byte, 0, 8 + len(v.Voter))
// TODO: append uint64 to result as BigEndian
result = append(result, v.Voter...)
return result
}

var (
// Group Table
GroupTablePrefix byte = 0x0
GroupTableSeqPrefix byte = 0x1
GroupByAdminIndexPrefix byte = 0x2

// Group Member Table
GroupMemberTablePrefix byte = 0x3
GroupMemberTableSeqPrefix byte = 0x4
GroupMemberTableIndexPrefix byte = 0x5
GroupMemberByGroupIndexPrefix byte = 0x6
GroupMemberByMemberIndexPrefix byte = 0x7

// Group Account Table
GroupAccountTablePrefix byte = 0x8
GroupAccountTableSeqPrefix byte = 0x9
GroupAccountTableIndexPrefix byte = 0x10
GroupAccountByGroupIndexPrefix byte = 0x11
GroupAccountByAdminIndexPrefix byte = 0x12

// Proposal Table
ProposalTablePrefix byte = 0x13
ProposalTableSeqPrefix byte = 0x14
ProposalByGroupAccountIndexPrefix byte = 0x15
ProposalByProposerIndexPrefix byte = 0x16

// Vote Table
VoteTablePrefix byte = 0x17
VoteTableSeqPrefix byte = 0x18
VoteTableIndexPrefix byte = 0x19
VoteByProposalIndexPrefix byte = 0x20
VoteByVoterIndexPrefix byte = 0x21
)

func NewGroupKeeper(storeKey sdk.StoreKey) keeper {
k := keeper{key: storeKey}

//
// Group Table
//
groupTableBuilder := orm.NewAutoUInt64TableBuilder(GroupTablePrefix, GroupTableSeqPrefix, storeKey, &GroupMetadata{})
k.groupByAdminIndex = orm.NewIndex(groupTableBuilder, GroupByAdminIndexPrefix, func(val interface{}) ([][]byte, error) {
return [][]byte{val.(*GroupMetadata).Admin}, nil
})
k.groupTable = groupTableBuilder.Build()

//
// Group Member Table
//
groupMemberTableBuilder := orm.NewNaturalKeyTableBuilder(GroupMemberTablePrefix, GroupMemberTableSeqPrefix, GroupMemberTableIndexPrefix, storeKey, &GroupMember{})
k.groupMemberByGroupIndex = orm.NewUInt64Index(groupMemberTableBuilder, GroupMemberByGroupIndexPrefix, func(val interface{}) ([]uint64, error) {
group := val.(*GroupMember).Group
return []uint64{uint64(group)}, nil
aaronc marked this conversation as resolved.
Show resolved Hide resolved
})
k.groupMemberByMemberIndex = orm.NewIndex(groupMemberTableBuilder, GroupMemberByMemberIndexPrefix, func(val interface{}) ([][]byte, error) {
return [][]byte{val.(*GroupMember).Member}, nil
})
k.groupMemberTable = groupMemberTableBuilder.Build()

//
// Group Account Table
//
groupAccountTableBuilder := orm.NewNaturalKeyTableBuilder(GroupAccountTablePrefix, GroupAccountTableSeqPrefix, GroupAccountTableIndexPrefix, storeKey, &GroupAccountMetadata{})
k.groupAccountByGroupIndex = orm.NewUInt64Index(groupAccountTableBuilder, GroupAccountByGroupIndexPrefix, func(value interface{}) ([]uint64, error) {
group := value.(*GroupAccountMetadata).Group
return []uint64{uint64(group)}, nil
})
k.groupAccountByAdminIndex = orm.NewIndex(groupAccountTableBuilder, GroupAccountByAdminIndexPrefix, func(value interface{}) ([][]byte, error) {
admin := value.(*GroupAccountMetadata).Admin
return [][]byte{admin}, nil
})
k.groupAccountTable = groupAccountTableBuilder.Build()

//
// Proposal Table
//
proposalTableBuilder := orm.NewAutoUInt64TableBuilder(ProposalTablePrefix, ProposalTableSeqPrefix, storeKey, &Proposal{})
k.proposalByGroupAccountIndex = orm.NewIndex(proposalTableBuilder, ProposalByGroupAccountIndexPrefix, func(value interface{}) ([][]byte, error) {
return [][]byte{value.(*Proposal).GroupAccount}, nil

})
k.proposalByProposerIndex = orm.NewIndex(proposalTableBuilder, ProposalByProposerIndexPrefix, func(value interface{}) ([][]byte, error) {
return value.(*Proposal).Proposers, nil
})
k.proposalTable = proposalTableBuilder.Build()

//
// Vote Table
//
voteTableBuilder := orm.NewNaturalKeyTableBuilder(VoteTablePrefix, VoteTableSeqPrefix, VoteTableIndexPrefix, storeKey, &Vote{})
k.voteByProposalIndex = orm.NewUInt64Index(voteTableBuilder, VoteByProposalIndexPrefix, func(value interface{}) ([]uint64, error) {
return []uint64{uint64(value.(*Vote).Proposal)}, nil
})
k.voteByVoterIndex = orm.NewIndex(voteTableBuilder, VoteByVoterIndexPrefix, func(value interface{}) ([][]byte, error) {
return [][]byte{value.(*Vote).Voter}, nil
})
k.voteTable = voteTableBuilder.Build()

return k
}

type Keeper interface {
// Groups
CreateGroup(ctx sdk.Context, admin sdk.AccAddress, members []Member, comment string) (GroupID, error)
UpdateGroupMembers(ctx sdk.Context, group GroupID, membersUpdates []Member) error
UpdateGroupAdmin(ctx sdk.Context, group GroupID, newAdmin sdk.AccAddress) error
UpdateGroupComment(ctx sdk.Context, group GroupID, newComment string) error

// Group Accounts
CreateGroupAccount(ctx sdk.Context, admin sdk.AccAddress, group GroupID, policy DecisionPolicy, comment string) (sdk.AccAddress, error)
UpdateGroupAccountAdmin(ctx sdk.Context, groupAcc sdk.AccAddress, newAdmin sdk.AccAddress) error
UpdateGroupAccountDecisionPolicy(ctx sdk.Context, groupAcc sdk.AccAddress, newPolicy DecisionPolicy) error
UpdateGroupAccountComment(ctx sdk.Context, groupAcc sdk.AccAddress, newComment string) error

// Proposals

// Propose returns a new proposal ID and a populated sdk.Result which could return an error
// or the result of execution if execNow was set to true
Propose(ctx sdk.Context, groupAcc sdk.AccAddress, approvers []sdk.AccAddress, msgs []sdk.Msg, comment string, execNow bool) (id ProposalID, execResult sdk.Result)

Vote(ctx sdk.Context, id ProposalID, voters []sdk.AccAddress, choice Choice) error

// Exec attempts to execute the specified proposal. If the proposal is in a valid
// state and has enough approvals, then it will be executed and its result will be
// returned, otherwise the result will contain an error
Exec(ctx sdk.Context, id ProposalID) sdk.Result
}
15 changes: 15 additions & 0 deletions incubator/group/protocgen.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash

set -eo pipefail

protoc \
-I. \
--gocosmos_out=\
Mgoogle/protobuf/any.proto=github.com/gogo/protobuf/types,\
Mgoogle/protobuf/duration.proto=github.com/gogo/protobuf/types,\
Mgoogle/protobuf/empty.proto=github.com/gogo/protobuf/types,\
Mgoogle/protobuf/struct.proto=github.com/gogo/protobuf/types,\
Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types,\
Mgoogle/protobuf/wrappers.proto=github.com/gogo/protobuf/types,\
plugins=interfacetype+grpc,paths=source_relative:. \
types.proto
15 changes: 15 additions & 0 deletions incubator/group/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package group

import (
sdk "github.com/cosmos/cosmos-sdk/types"
"time"
)

type GroupID uint64

type ProposalID uint64

type DecisionPolicy interface {
Allow(tally Tally, totalPower sdk.Dec, votingDuration time.Duration) bool
}