add proposal voting deadlines

This commit is contained in:
rhuairahrighairigh 2020-03-12 17:05:40 +00:00
parent 20bcfec407
commit f773f7f278
9 changed files with 173 additions and 35 deletions

View File

@ -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 // there is nothing left over in the validator fee pool, so as to keep the
// CanWithdrawInvariant invariant. // 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. // 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) app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)

View File

@ -1,12 +1,23 @@
package committee package committee
// func BeginBlocker() { import (
// // TODO much the same as the current gov endblocker does sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
// // Get all active proposals "github.com/kava-labs/kava/x/committee/types"
// // 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 // BeginBlocker runs at the start of every block.
// handler := keeper.Router().GetRoute(proposal.ProposalRoute()) func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k Keeper) {
// err := handler(ctx, proposal.Content)
// } // 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
})
}

68
x/committee/abci_test.go Normal file
View File

@ -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))
}

View File

@ -9,27 +9,35 @@ import (
const ( const (
DefaultNextProposalID = types.DefaultNextProposalID DefaultNextProposalID = types.DefaultNextProposalID
DefaultParamspace = types.DefaultParamspace
ModuleName = types.ModuleName ModuleName = types.ModuleName
QuerierRoute = types.QuerierRoute
RouterKey = types.RouterKey
StoreKey = types.StoreKey StoreKey = types.StoreKey
TypeMsgSubmitProposal = types.TypeMsgSubmitProposal
TypeMsgVote = types.TypeMsgVote
) )
var ( var (
// function aliases // function aliases
NewKeeper = keeper.NewKeeper NewKeeper = keeper.NewKeeper
DefaultGenesisState = types.DefaultGenesisState DefaultGenesisState = types.DefaultGenesisState
GetKeyFromID = types.GetKeyFromID GetKeyFromID = types.GetKeyFromID
GetVoteKey = types.GetVoteKey GetVoteKey = types.GetVoteKey
NewGenesisState = types.NewGenesisState NewGenesisState = types.NewGenesisState
RegisterCodec = types.RegisterCodec NewMsgSubmitProposal = types.NewMsgSubmitProposal
Uint64FromBytes = types.Uint64FromBytes NewMsgVote = types.NewMsgVote
RegisterCodec = types.RegisterCodec
Uint64FromBytes = types.Uint64FromBytes
// variable aliases // variable aliases
CommitteeKeyPrefix = types.CommitteeKeyPrefix CommitteeKeyPrefix = types.CommitteeKeyPrefix
ModuleCdc = types.ModuleCdc MaxProposalDuration = types.MaxProposalDuration
NextProposalIDKey = types.NextProposalIDKey ModuleCdc = types.ModuleCdc
ProposalKeyPrefix = types.ProposalKeyPrefix NextProposalIDKey = types.NextProposalIDKey
VoteKeyPrefix = types.VoteKeyPrefix ProposalKeyPrefix = types.ProposalKeyPrefix
VoteThreshold = types.VoteThreshold VoteKeyPrefix = types.VoteKeyPrefix
VoteThreshold = types.VoteThreshold
) )
type ( type (

View File

@ -1,10 +1,11 @@
package keeper package keeper
import ( import (
"time"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix" "github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types" 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" "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 // 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) newProposalID, err := k.GetNextProposalID(ctx)
if err != nil { if err != nil {
return 0, err return 0, err
@ -103,6 +104,7 @@ func (k Keeper) StoreNewProposal(ctx sdk.Context, committeeID uint64, pubProposa
PubProposal: pubProposal, PubProposal: pubProposal,
ID: newProposalID, ID: newProposalID,
CommitteeID: committeeID, CommitteeID: committeeID,
Deadline: deadline,
} }
k.SetProposal(ctx, proposal) k.SetProposal(ctx, proposal)
@ -139,6 +141,22 @@ func (k Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) {
store.Delete(types.GetKeyFromID(proposalID)) 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 ---------- // ---------- Votes ----------
// GetVote gets a vote from the store. // GetVote gets a vote from the store.

View File

@ -27,7 +27,8 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ
} }
// Get a new ID and store the proposal // 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 { 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 { if !found {
return sdk.ErrInternal("proposal not found") return sdk.ErrInternal("proposal not found")
} }
if pr.HasExpiredBy(ctx.BlockTime()) {
return sdk.ErrInternal("proposal expired")
}
com, found := k.GetCommittee(ctx, pr.CommitteeID) com, found := k.GetCommittee(ctx, pr.CommitteeID)
if !found { if !found {
return sdk.ErrInternal("committee disbanded") return sdk.ErrInternal("committee disbanded")
@ -65,7 +69,9 @@ func (k Keeper) CloseOutProposal(ctx sdk.Context, proposalID uint64) sdk.Error {
votes = append(votes, vote) votes = append(votes, vote)
return false 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 // eneact vote
// The proposal handler may execute state mutating logic depending // The proposal handler may execute state mutating logic depending
// on the proposal content. If the handler fails, no state mutation // 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 // write state to the underlying multi-store
writeCache() writeCache()
} // if handler returns error, then still delete the proposal - it's still over, but send an event } // 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 // delete proposal and votes
k.DeleteProposal(ctx, proposalID) k.DeleteProposal(ctx, proposalID)
for _, v := range votes { for _, v := range votes {
k.DeleteVote(ctx, v.ProposalID, v.Voter) k.DeleteVote(ctx, v.ProposalID, v.Voter)
} }
} else { return nil
return sdk.ErrInternal("note enough votes to close proposal")
} }
return nil return sdk.ErrInternal("note enough votes to close proposal")
} }
func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error { func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) sdk.Error {

View File

@ -2,6 +2,7 @@ package keeper_test
import ( import (
"reflect" "reflect"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/gov"
@ -89,8 +90,10 @@ func (suite *KeeperTestSuite) TestSubmitProposal() {
if tc.expectPass { if tc.expectPass {
suite.NoError(err) suite.NoError(err)
_, found := keeper.GetProposal(ctx, id) pr, found := keeper.GetProposal(ctx, id)
suite.True(found) suite.True(found)
suite.Equal(tc.committeeID, pr.CommitteeID)
suite.Equal(ctx.BlockTime().Add(types.MaxProposalDuration), pr.Deadline)
} else { } else {
suite.NotNil(err) suite.NotNil(err)
} }
@ -104,11 +107,13 @@ func (suite *KeeperTestSuite) TestAddVote() {
Members: suite.addresses[:2], Members: suite.addresses[:2],
Permissions: []types.Permission{types.GodPermission{}}, Permissions: []types.Permission{types.GodPermission{}},
} }
firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
testcases := []struct { testcases := []struct {
name string name string
proposalID uint64 proposalID uint64
voter sdk.AccAddress voter sdk.AccAddress
voteTime time.Time
expectPass bool expectPass bool
}{ }{
{ {
@ -129,6 +134,13 @@ func (suite *KeeperTestSuite) TestAddVote() {
voter: suite.addresses[4], voter: suite.addresses[4],
expectPass: false, expectPass: false,
}, },
{
name: "proposal expired",
proposalID: types.DefaultNextProposalID,
voter: normalCom.Members[0],
voteTime: firstBlockTime.Add(types.MaxProposalDuration),
expectPass: false,
},
} }
for _, tc := range testcases { 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. // 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() tApp := app.NewTestApp()
keeper := tApp.GetCommitteeKeeper() keeper := tApp.GetCommitteeKeeper()
ctx := tApp.NewContext(true, abci.Header{}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime})
tApp.InitializeFromGenesisStates() tApp.InitializeFromGenesisStates()
// setup the committee and proposal // 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.")) _, err := keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, gov.NewTextProposal("A Title", "A description of this proposal."))
suite.NoError(err) suite.NoError(err)
ctx = ctx.WithBlockTime(tc.voteTime)
err = keeper.AddVote(ctx, tc.proposalID, tc.voter) err = keeper.AddVote(ctx, tc.proposalID, tc.voter)
if tc.expectPass { if tc.expectPass {

View File

@ -150,7 +150,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
// BeginBlock module begin-block // BeginBlock module begin-block
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { 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 // EndBlock module end-block

View File

@ -1,13 +1,19 @@
package types package types
import ( import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov" "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. // A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions.
type Committee struct { type Committee struct {
@ -50,6 +56,13 @@ type Proposal struct {
PubProposal PubProposal
ID uint64 ID uint64
CommitteeID 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 { type Vote struct {