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() {