feat(group): Prune expired proposals and votes (#11315)

## Description

Closes: #11245

### 1. Pruning proposal.

Proposals are pruned:
- after a successful MsgExec (or try_exec)
- on voting_period_end + max_execution_period

whichever happens first.

### 2. Pruning votes.

Votes are pruned:
  - on MsgExec (or try_exec) if tally is final
  - on voting_period_end

whichever happens first. 

### 3. Various group fixes

See #11404 for details

TODO:
- [x] Add vote pruning after Tally, once #11310  is merged
- [x] Add tests



---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
This commit is contained in:
Amaury
2022-03-28 04:44:01 +00:00
committed by GitHub
parent 73c9f42fc0
commit 5491be27d0
17 changed files with 956 additions and 693 deletions
+8
View File
@@ -9,4 +9,12 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) {
if err := k.UpdateTallyOfVPEndProposals(ctx); err != nil {
panic(err)
}
pruneProposals(ctx, k)
}
func pruneProposals(ctx sdk.Context, k keeper.Keeper) {
err := k.PruneProposals(ctx)
if err != nil {
panic(err)
}
}
+202 -27
View File
@@ -6,18 +6,196 @@ import (
"time"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank/testutil"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/cosmos-sdk/x/group"
"github.com/cosmos/cosmos-sdk/x/group/module"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
func TestEndBlockerPruning(t *testing.T) {
app := simapp.Setup(t, false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{})
addrs := simapp.AddTestAddrsIncremental(app, ctx, 3, sdk.NewInt(30000000))
addr1 := addrs[0]
addr2 := addrs[1]
addr3 := addrs[2]
// Initial group, group policy and balance setup
members := []group.Member{
{Address: addr1.String(), Weight: "1"}, {Address: addr2.String(), Weight: "2"},
}
groupRes, err := app.GroupKeeper.CreateGroup(ctx, &group.MsgCreateGroup{
Admin: addr1.String(),
Members: members,
})
require.NoError(t, err)
groupID := groupRes.GroupId
policy := group.NewThresholdDecisionPolicy(
"2",
time.Second,
0,
)
policyReq := &group.MsgCreateGroupPolicy{
Admin: addr1.String(),
GroupId: groupID,
}
err = policyReq.SetDecisionPolicy(policy)
require.NoError(t, err)
policyRes, err := app.GroupKeeper.CreateGroupPolicy(ctx, policyReq)
require.NoError(t, err)
groupPolicyAddr, err := sdk.AccAddressFromBech32(policyRes.Address)
require.NoError(t, err)
require.NoError(t, testutil.FundAccount(app.BankKeeper, ctx, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10000)}))
msgSend1 := &banktypes.MsgSend{
FromAddress: groupPolicyAddr.String(),
ToAddress: addr2.String(),
Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)},
}
proposers := []string{addr2.String()}
specs := map[string]struct {
srcBlockTime time.Time
setupProposal func(ctx context.Context) uint64
expErr bool
expErrMsg string
expExecutorResult group.ProposalExecutorResult
}{
"proposal pruned after executor result success": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend1}
pID, err := submitProposalAndVote(app, ctx, msgs, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
require.NoError(t, err)
_, err = app.GroupKeeper.Exec(ctx, &group.MsgExec{Signer: addr3.String(), ProposalId: pID})
require.NoError(t, err)
sdkCtx := sdk.UnwrapSDKContext(ctx)
require.NoError(t, testutil.FundAccount(app.BankKeeper, sdkCtx, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
return pID
},
expErrMsg: "load proposal: not found",
expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
},
"proposal with multiple messages pruned when executed with result success": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend1, msgSend1}
pID, err := submitProposalAndVote(app, ctx, msgs, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
require.NoError(t, err)
_, err = app.GroupKeeper.Exec(ctx, &group.MsgExec{Signer: addr3.String(), ProposalId: pID})
require.NoError(t, err)
sdkCtx := sdk.UnwrapSDKContext(ctx)
require.NoError(t, testutil.FundAccount(app.BankKeeper, sdkCtx, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
return pID
},
expErrMsg: "load proposal: not found",
expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
},
"proposal not pruned when not executed and rejected": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend1}
pID, err := submitProposalAndVote(app, ctx, msgs, proposers, groupPolicyAddr, group.VOTE_OPTION_NO)
require.NoError(t, err)
_, err = app.GroupKeeper.Exec(ctx, &group.MsgExec{Signer: addr3.String(), ProposalId: pID})
require.NoError(t, err)
sdkCtx := sdk.UnwrapSDKContext(ctx)
require.NoError(t, testutil.FundAccount(app.BankKeeper, sdkCtx, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
return pID
},
expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
},
"open proposal is not pruned which must not fail ": {
setupProposal: func(ctx context.Context) uint64 {
pID, err := submitProposal(app, ctx, []sdk.Msg{msgSend1}, proposers, groupPolicyAddr)
require.NoError(t, err)
_, err = app.GroupKeeper.Exec(ctx, &group.MsgExec{Signer: addr3.String(), ProposalId: pID})
require.NoError(t, err)
sdkCtx := sdk.UnwrapSDKContext(ctx)
require.NoError(t, testutil.FundAccount(app.BankKeeper, sdkCtx, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
return pID
},
expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
},
"proposal not pruned with group policy modified before tally": {
setupProposal: func(ctx context.Context) uint64 {
pID, err := submitProposal(app, ctx, []sdk.Msg{msgSend1}, proposers, groupPolicyAddr)
require.NoError(t, err)
_, err = app.GroupKeeper.UpdateGroupPolicyMetadata(ctx, &group.MsgUpdateGroupPolicyMetadata{
Admin: addr1.String(),
Address: groupPolicyAddr.String(),
})
require.NoError(t, err)
_, err = app.GroupKeeper.Exec(ctx, &group.MsgExec{Signer: addr3.String(), ProposalId: pID})
require.NoError(t, err)
sdkCtx := sdk.UnwrapSDKContext(ctx)
require.NoError(t, testutil.FundAccount(app.BankKeeper, sdkCtx, groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
return pID
},
expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
},
"pruned when proposal is executable when failed before": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend1}
pID, err := submitProposalAndVote(app, ctx, msgs, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
require.NoError(t, err)
_, err = app.GroupKeeper.Exec(ctx, &group.MsgExec{Signer: addrs[2].String(), ProposalId: pID})
require.NoError(t, err)
return pID
},
expErrMsg: "load proposal: not found",
expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
},
}
for msg, spec := range specs {
spec := spec
t.Run(msg, func(t *testing.T) {
proposalID := spec.setupProposal(ctx)
module.EndBlocker(ctx, app.GroupKeeper)
if spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS {
// Make sure proposal is deleted from state
_, err = app.GroupKeeper.Proposal(ctx, &group.QueryProposalRequest{ProposalId: proposalID})
require.Contains(t, err.Error(), spec.expErrMsg)
res, err := app.GroupKeeper.VotesByProposal(ctx, &group.QueryVotesByProposalRequest{ProposalId: proposalID})
require.NoError(t, err)
require.Empty(t, res.GetVotes())
} else {
// Check that proposal and votes exists
res, err := app.GroupKeeper.Proposal(ctx, &group.QueryProposalRequest{ProposalId: proposalID})
require.NoError(t, err)
_, err = app.GroupKeeper.VotesByProposal(ctx, &group.QueryVotesByProposalRequest{ProposalId: res.Proposal.Id})
require.NoError(t, err)
require.Equal(t, "", spec.expErrMsg)
exp := group.ProposalExecutorResult_name[int32(spec.expExecutorResult)]
got := group.ProposalExecutorResult_name[int32(res.Proposal.ExecutorResult)]
assert.Equal(t, exp, got)
}
})
}
}
func TestEndBlocker(t *testing.T) {
app := simapp.Setup(t, false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{})
addrs := simapp.AddTestAddrsIncremental(app, ctx, 4, types.NewInt(30000000))
addrs := simapp.AddTestAddrsIncremental(app, ctx, 4, sdk.NewInt(30000000))
// Initial group, group policy and balance setup
members := []group.Member{
@@ -48,45 +226,44 @@ func TestEndBlocker(t *testing.T) {
policyRes, err := app.GroupKeeper.CreateGroupPolicy(ctx, policyReq)
require.NoError(t, err)
groupPolicyAddr, err := types.AccAddressFromBech32(policyRes.Address)
groupPolicyAddr, err := sdk.AccAddressFromBech32(policyRes.Address)
require.NoError(t, err)
votingPeriod := policy.GetVotingPeriod()
now := time.Now()
msgSend := &banktypes.MsgSend{
FromAddress: groupPolicyAddr.String(),
ToAddress: addrs[3].String(),
Amount: types.Coins{types.NewInt64Coin("test", 100)},
Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)},
}
proposers := []string{addrs[2].String()}
specs := map[string]struct {
preRun func(sdkCtx types.Context) uint64
preRun func(sdkCtx sdk.Context) uint64
proposalId uint64
admin string
expErrMsg string
newCtx types.Context
newCtx sdk.Context
tallyRes group.TallyResult
expStatus group.ProposalStatus
expExecutorResult group.ProposalResult
}{
"tally updated after voting power end": {
preRun: func(sdkCtx types.Context) uint64 {
pId, err := submitProposal(app, sdkCtx, []types.Msg{msgSend}, proposers, groupPolicyAddr)
preRun: func(sdkCtx sdk.Context) uint64 {
pId, err := submitProposal(app, sdkCtx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr)
require.NoError(t, err)
return pId
},
admin: proposers[0],
newCtx: ctx.WithBlockTime(now.Add(votingPeriod).Add(time.Hour)),
newCtx: ctx.WithBlockTime(ctx.BlockTime().Add(votingPeriod).Add(time.Hour)),
tallyRes: group.DefaultTallyResult(),
expStatus: group.PROPOSAL_STATUS_SUBMITTED,
expExecutorResult: group.PROPOSAL_RESULT_UNFINALIZED,
},
"tally within voting period": {
preRun: func(sdkCtx types.Context) uint64 {
pId, err := submitProposal(app, sdkCtx, []types.Msg{msgSend}, proposers, groupPolicyAddr)
preRun: func(sdkCtx sdk.Context) uint64 {
pId, err := submitProposal(app, sdkCtx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr)
require.NoError(t, err)
return pId
@@ -98,8 +275,8 @@ func TestEndBlocker(t *testing.T) {
expExecutorResult: group.PROPOSAL_RESULT_UNFINALIZED,
},
"tally within voting period(with votes)": {
preRun: func(sdkCtx types.Context) uint64 {
pId, err := submitProposalAndVote(app, ctx, []types.Msg{msgSend}, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
preRun: func(sdkCtx sdk.Context) uint64 {
pId, err := submitProposalAndVote(app, ctx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
require.NoError(t, err)
return pId
@@ -111,14 +288,14 @@ func TestEndBlocker(t *testing.T) {
expExecutorResult: group.PROPOSAL_RESULT_UNFINALIZED,
},
"tally after voting period(with votes)": {
preRun: func(sdkCtx types.Context) uint64 {
pId, err := submitProposalAndVote(app, ctx, []types.Msg{msgSend}, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
preRun: func(sdkCtx sdk.Context) uint64 {
pId, err := submitProposalAndVote(app, ctx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
require.NoError(t, err)
return pId
},
admin: proposers[0],
newCtx: ctx.WithBlockTime(now.Add(votingPeriod).Add(time.Hour)),
newCtx: ctx.WithBlockTime(ctx.BlockTime().Add(votingPeriod).Add(time.Hour)),
tallyRes: group.TallyResult{
YesCount: "2",
NoCount: "0",
@@ -129,8 +306,8 @@ func TestEndBlocker(t *testing.T) {
expExecutorResult: group.PROPOSAL_RESULT_ACCEPTED,
},
"tally of closed proposal": {
preRun: func(sdkCtx types.Context) uint64 {
pId, err := submitProposal(app, sdkCtx, []types.Msg{msgSend}, proposers, groupPolicyAddr)
preRun: func(sdkCtx sdk.Context) uint64 {
pId, err := submitProposal(app, sdkCtx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr)
require.NoError(t, err)
_, err = app.GroupKeeper.WithdrawProposal(ctx, &group.MsgWithdrawProposal{
@@ -148,8 +325,8 @@ func TestEndBlocker(t *testing.T) {
expExecutorResult: group.PROPOSAL_RESULT_UNFINALIZED,
},
"tally of closed proposal (with votes)": {
preRun: func(sdkCtx types.Context) uint64 {
pId, err := submitProposalAndVote(app, ctx, []types.Msg{msgSend}, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
preRun: func(sdkCtx sdk.Context) uint64 {
pId, err := submitProposalAndVote(app, ctx, []sdk.Msg{msgSend}, proposers, groupPolicyAddr, group.VOTE_OPTION_YES)
require.NoError(t, err)
_, err = app.GroupKeeper.WithdrawProposal(ctx, &group.MsgWithdrawProposal{
@@ -193,8 +370,8 @@ func TestEndBlocker(t *testing.T) {
}
func submitProposal(
app *simapp.SimApp, ctx context.Context, msgs []types.Msg,
proposers []string, groupPolicyAddr types.AccAddress) (uint64, error) {
app *simapp.SimApp, ctx context.Context, msgs []sdk.Msg,
proposers []string, groupPolicyAddr sdk.AccAddress) (uint64, error) {
proposalReq := &group.MsgSubmitProposal{
Address: groupPolicyAddr.String(),
Proposers: proposers,
@@ -213,13 +390,12 @@ func submitProposal(
}
func submitProposalAndVote(
app *simapp.SimApp, ctx context.Context, msgs []types.Msg,
proposers []string, groupPolicyAddr types.AccAddress, voteOption group.VoteOption) (uint64, error) {
app *simapp.SimApp, ctx context.Context, msgs []sdk.Msg,
proposers []string, groupPolicyAddr sdk.AccAddress, voteOption group.VoteOption) (uint64, error) {
myProposalID, err := submitProposal(app, ctx, msgs, proposers, groupPolicyAddr)
if err != nil {
return 0, err
}
_, err = app.GroupKeeper.Vote(ctx, &group.MsgVote{
ProposalId: myProposalID,
Voter: proposers[0],
@@ -228,6 +404,5 @@ func submitProposalAndVote(
if err != nil {
return 0, err
}
return myProposalID, nil
}