diff --git a/proto/atomone/gov/v1/gov.proto b/proto/atomone/gov/v1/gov.proto index c217422f1..e40d259c8 100644 --- a/proto/atomone/gov/v1/gov.proto +++ b/proto/atomone/gov/v1/gov.proto @@ -149,6 +149,21 @@ message Vote { string metadata = 5; } +// QuorumCheckQueueEntry defines a quorum check queue entry. +message QuorumCheckQueueEntry { + // quorum_timeout_time is the time after which quorum checks start happening + // and voting period is extended if proposal reaches quorum. + google.protobuf.Timestamp quorum_timeout_time = 1 [(gogoproto.stdtime) = true]; + + // quorum_check_count is the number of times quorum will be checked. + // This is a snapshot of the parameter value with the same name when the + // proposal is initially added to the queue. + uint64 quorum_check_count = 2; + + // quorum_checks_done is the number of quorum checks that have been done. + uint64 quorum_checks_done = 3; +} + // DepositParams defines the params for deposits on governance proposals. message DepositParams { // Minimum deposit for a proposal to enter voting period. @@ -245,4 +260,16 @@ message Params { // Minimum proportion of Yes votes for a Law proposal to pass. Default value: 0.9. string law_threshold = 19 [(cosmos_proto.scalar) = "cosmos.Dec"]; + + // Duration of time after a proposal enters the voting period, during which quorum + // must be achieved to not incur in a voting period extension. + google.protobuf.Duration quorum_timeout = 20 [(gogoproto.stdduration) = true]; + + // Duration that expresses the maximum amount of time by which a proposal voting period + // can be extended. + google.protobuf.Duration max_voting_period_extension = 21 [(gogoproto.stdduration) = true]; + + // Number of times a proposal should be checked for quorum after the quorum timeout + // has elapsed. Used to compute the amount of time in between quorum checks. + uint64 quorum_check_count = 22; } diff --git a/tests/e2e/genesis.go b/tests/e2e/genesis.go index 2107c40e1..50fe13d77 100644 --- a/tests/e2e/genesis.go +++ b/tests/e2e/genesis.go @@ -140,6 +140,7 @@ func modifyGenesis(path, moniker, amountStr string, addrAll []sdk.AccAddress, de amendmentsQuorum.String(), amendmentsThreshold.String(), lawQuorum.String(), lawThreshold.String(), sdk.ZeroDec().String(), false, false, govv1.DefaultMinDepositRatio.String(), + govv1.DefaultQuorumTimeout, govv1.DefaultMaxVotingPeriodExtension, govv1.DefaultQuorumCheckCount, ), ) govGenStateBz, err := cdc.MarshalJSON(govGenState) diff --git a/x/gov/abci.go b/x/gov/abci.go index 6a648620c..29fc35cd6 100644 --- a/x/gov/abci.go +++ b/x/gov/abci.go @@ -52,6 +52,79 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) { return false }) + // fetch proposals that are due to be checked for quorum + keeper.IterateQuorumCheckQueue(ctx, ctx.BlockTime(), + func(proposal v1.Proposal, endTime time.Time, quorumCheckEntry v1.QuorumCheckQueueEntry) bool { + params := keeper.GetParams(ctx) + // remove from queue + keeper.RemoveFromQuorumCheckQueue(ctx, proposal.Id, endTime) + // check if proposal passed quorum + quorum, err := keeper.HasReachedQuorum(ctx, proposal) + if err != nil { + return false + } + logMsg := "proposal did not pass quorum after timeout, but was removed from quorum check queue" + tagValue := types.AttributeValueProposalQuorumNotMet + + if quorum { + logMsg = "proposal passed quorum before timeout, vote period was not extended" + tagValue = types.AttributeValueProposalQuorumMet + if quorumCheckEntry.QuorumChecksDone > 0 { + // proposal passed quorum after timeout, extend voting period. + // canonically, we consider the first quorum check to be "right after" the quorum timeout has elapsed, + // so if quorum is reached at the first check, we don't extend the voting period. + endTime := ctx.BlockTime().Add(*params.MaxVotingPeriodExtension) + logMsg = fmt.Sprintf("proposal passed quorum after timeout, but vote end %s is already after %s", proposal.VotingEndTime, endTime) + if endTime.After(*proposal.VotingEndTime) { + logMsg = fmt.Sprintf("proposal passed quorum after timeout, vote end was extended from %s to %s", proposal.VotingEndTime, endTime) + // Update ActiveProposalsQueue with new VotingEndTime + keeper.RemoveFromActiveProposalQueue(ctx, proposal.Id, *proposal.VotingEndTime) + proposal.VotingEndTime = &endTime + keeper.InsertActiveProposalQueue(ctx, proposal.Id, *proposal.VotingEndTime) + keeper.SetProposal(ctx, proposal) + } + } + } else if quorumCheckEntry.QuorumChecksDone < quorumCheckEntry.QuorumCheckCount && proposal.VotingEndTime.After(ctx.BlockTime()) { + // proposal did not pass quorum and is still active, add back to queue with updated time key and counter. + // compute time interval between quorum checks + quorumCheckPeriod := proposal.VotingEndTime.Sub(*quorumCheckEntry.QuorumTimeoutTime) + t := quorumCheckPeriod / time.Duration(quorumCheckEntry.QuorumCheckCount) + // find time for next quorum check + nextQuorumCheckTime := endTime.Add(t) + if !nextQuorumCheckTime.After(ctx.BlockTime()) { + // next quorum check time is in the past, so add enough time intervals to get to the next quorum check time in the future. + d := ctx.BlockTime().Sub(nextQuorumCheckTime) + n := d / t + nextQuorumCheckTime = nextQuorumCheckTime.Add(t * (n + 1)) + } + if nextQuorumCheckTime.After(*proposal.VotingEndTime) { + // next quorum check time is after the voting period ends, so adjust it to be equal to the voting period end time + nextQuorumCheckTime = *proposal.VotingEndTime + } + quorumCheckEntry.QuorumChecksDone++ + keeper.InsertQuorumCheckQueue(ctx, proposal.Id, nextQuorumCheckTime, quorumCheckEntry) + + logMsg = fmt.Sprintf("proposal did not pass quorum after timeout, next check happening at %s", nextQuorumCheckTime) + } + + logger.Info( + "proposal quorum check", + "proposal", proposal.Id, + "title", proposal.Title, + "results", logMsg, + ) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeQuorumCheck, + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)), + sdk.NewAttribute(types.AttributeKeyProposalResult, tagValue), + ), + ) + + return false + }) + // fetch active proposals whose voting periods have ended (are passed the block time) keeper.IterateActiveProposalsQueue(ctx, ctx.BlockHeader().Time, func(proposal v1.Proposal) bool { var tagValue, logMsg string diff --git a/x/gov/abci_test.go b/x/gov/abci_test.go index 4a54262c1..6f7da033a 100644 --- a/x/gov/abci_test.go +++ b/x/gov/abci_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" abci "github.com/cometbft/cometbft/abci/types" @@ -387,6 +388,141 @@ func TestEndBlockerProposalHandlerFailed(t *testing.T) { require.Equal(t, v1.StatusFailed, proposal.Status) } +func TestEndBlockerQuorumCheck(t *testing.T) { + params := v1.DefaultParams() + params.QuorumCheckCount = 10 // enable quorum check + quorumTimeout := *params.VotingPeriod - time.Hour*8 + params.QuorumTimeout = &quorumTimeout + oneHour := time.Hour + testcases := []struct { + name string + // the value of the MaxVotingPeriodExtension param + maxVotingPeriodExtension *time.Duration + // the duration after which the proposal reaches quorum + reachQuorumAfter time.Duration + // the expected status of the proposal after the original voting period has elapsed + expectedStatusAfterVotingPeriod v1.ProposalStatus + // the expected final voting period after the original period has elapsed + // the value would be modified if voting period is extended due to quorum being reached + // after the quorum timeout + expectedVotingPeriod time.Duration + }{ + { + name: "reach quorum before timeout: voting period not extended", + maxVotingPeriodExtension: params.MaxVotingPeriodExtension, + reachQuorumAfter: quorumTimeout - time.Hour, + expectedStatusAfterVotingPeriod: v1.StatusPassed, + expectedVotingPeriod: *params.VotingPeriod, + }, + { + name: "reach quorum exactly at timeout: voting period not extended", + maxVotingPeriodExtension: params.MaxVotingPeriodExtension, + reachQuorumAfter: quorumTimeout, + expectedStatusAfterVotingPeriod: v1.StatusPassed, + expectedVotingPeriod: *params.VotingPeriod, + }, + { + name: "quorum never reached: voting period not extended", + maxVotingPeriodExtension: params.MaxVotingPeriodExtension, + reachQuorumAfter: 0, + expectedStatusAfterVotingPeriod: v1.StatusRejected, + expectedVotingPeriod: *params.VotingPeriod, + }, + { + name: "reach quorum after timeout, voting period extended", + maxVotingPeriodExtension: params.MaxVotingPeriodExtension, + reachQuorumAfter: quorumTimeout + time.Hour, + expectedStatusAfterVotingPeriod: v1.StatusVotingPeriod, + expectedVotingPeriod: *params.VotingPeriod + *params.MaxVotingPeriodExtension - + (*params.VotingPeriod - quorumTimeout - time.Hour), + }, + { + name: "reach quorum exactly at voting period, voting period extended", + maxVotingPeriodExtension: params.MaxVotingPeriodExtension, + reachQuorumAfter: *params.VotingPeriod, + expectedStatusAfterVotingPeriod: v1.StatusVotingPeriod, + expectedVotingPeriod: *params.VotingPeriod + *params.MaxVotingPeriodExtension, + }, + { + name: "reach quorum after timeout but voting period extension too short, voting period not extended", + maxVotingPeriodExtension: &oneHour, + reachQuorumAfter: quorumTimeout + time.Hour, + expectedStatusAfterVotingPeriod: v1.StatusPassed, + expectedVotingPeriod: *params.VotingPeriod, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + suite := createTestSuite(t) + app := suite.App + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + params.MaxVotingPeriodExtension = tc.maxVotingPeriodExtension + err := suite.GovKeeper.SetParams(ctx, params) + require.NoError(t, err) + addrs := simtestutil.AddTestAddrs(suite.BankKeeper, suite.StakingKeeper, ctx, 10, valTokens) + // _, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{ + // Height: app.LastBlockHeight() + 1, + // Hash: app.LastCommitID().Hash, + // }) + // require.NoError(t, err) + // Create a validator + valAddr := sdk.ValAddress(addrs[0]) + stakingMsgSvr := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper) + createValidators(t, stakingMsgSvr, ctx, []sdk.ValAddress{valAddr}, []int64{10}) + staking.EndBlocker(ctx, suite.StakingKeeper) + // Create a proposal + govMsgSvr := keeper.NewMsgServerImpl(suite.GovKeeper) + deposit := v1.DefaultMinDepositTokens.ToLegacyDec().Mul(v1.DefaultMinDepositRatio) + newProposalMsg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{mkTestLegacyContent(t)}, + sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, deposit.RoundInt())}, + addrs[0].String(), "", "Proposal", "description of proposal", + ) + require.NoError(t, err) + res, err := govMsgSvr.SubmitProposal(ctx, newProposalMsg) + require.NoError(t, err) + require.NotNil(t, res) + // Activate proposal + newDepositMsg := v1.NewMsgDeposit(addrs[1], res.ProposalId, params.MinDeposit) + res1, err := govMsgSvr.Deposit(ctx, newDepositMsg) + require.NoError(t, err) + require.NotNil(t, res1) + prop, ok := suite.GovKeeper.GetProposal(ctx, res.ProposalId) + require.True(t, ok, "prop not found") + + // Call EndBlock until the initial voting period has elapsed + // Tick is one hour + var ( + startTime = ctx.BlockTime() + tick = time.Hour + ) + for ctx.BlockTime().Sub(startTime) < *params.VotingPeriod { + // Forward in time + newTime := ctx.BlockTime().Add(tick) + ctx = ctx.WithBlockTime(newTime) + if tc.reachQuorumAfter != 0 && newTime.Sub(startTime) >= tc.reachQuorumAfter { + // Set quorum as reached + err := suite.GovKeeper.AddVote(ctx, prop.Id, addrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), "") + require.NoError(t, err) + // Don't go there again + tc.reachQuorumAfter = 0 + } + + gov.EndBlocker(ctx, suite.GovKeeper) + + } + + // Assertions + prop, ok = suite.GovKeeper.GetProposal(ctx, prop.Id) // Get fresh prop + if assert.True(t, ok, "prop not found") { + assert.Equal(t, tc.expectedStatusAfterVotingPeriod.String(), prop.Status.String()) + assert.Equal(t, tc.expectedVotingPeriod, prop.VotingEndTime.Sub(*prop.VotingStartTime)) + assert.False(t, suite.GovKeeper.QuorumCheckQueueIterator(ctx, *prop.VotingStartTime).Valid(), "quorum check queue invalid") + } + }) + } +} + func createValidators(t *testing.T, stakingMsgSvr stakingtypes.MsgServer, ctx sdk.Context, addrs []sdk.ValAddress, powerAmt []int64) { require.True(t, len(addrs) <= len(pubkeys), "Not enough pubkeys specified at top of file.") diff --git a/x/gov/client/testutil/helpers.go b/x/gov/client/testutil/helpers.go index 20b9b18ce..a34a51268 100644 --- a/x/gov/client/testutil/helpers.go +++ b/x/gov/client/testutil/helpers.go @@ -2,6 +2,7 @@ package testutil import ( "fmt" + "time" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -10,6 +11,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" govcli "github.com/atomone-hub/atomone/x/gov/client/cli" + "github.com/atomone-hub/atomone/x/gov/keeper" + "github.com/atomone-hub/atomone/x/gov/types" + v1 "github.com/atomone-hub/atomone/x/gov/types/v1" ) var commonArgs = []string{ @@ -59,3 +63,47 @@ func MsgDeposit(clientCtx client.Context, from, id, deposit string, extraArgs .. return clitestutil.ExecTestCLICmd(clientCtx, govcli.NewCmdDeposit(), args) } + +func HasActiveProposal(ctx sdk.Context, k *keeper.Keeper, id uint64, t time.Time) bool { + it := k.ActiveProposalQueueIterator(ctx, t) + defer it.Close() + for ; it.Valid(); it.Next() { + proposalID, _ := types.SplitActiveProposalQueueKey(it.Key()) + if proposalID == id { + return true + } + } + return false +} + +func HasInactiveProposal(ctx sdk.Context, k *keeper.Keeper, id uint64, t time.Time) bool { + it := k.InactiveProposalQueueIterator(ctx, t) + defer it.Close() + for ; it.Valid(); it.Next() { + proposalID, _ := types.SplitInactiveProposalQueueKey(it.Key()) + if proposalID == id { + return true + } + } + return false +} + +func HasQuorumCheck(ctx sdk.Context, k *keeper.Keeper, id uint64, t time.Time) bool { + _, ok := GetQuorumCheckQueueEntry(ctx, k, id, t) + return ok +} + +func GetQuorumCheckQueueEntry(ctx sdk.Context, k *keeper.Keeper, id uint64, t time.Time) (v1.QuorumCheckQueueEntry, bool) { + it := k.QuorumCheckQueueIterator(ctx, t) + defer it.Close() + for ; it.Valid(); it.Next() { + proposalID, _ := types.SplitQuorumQueueKey(it.Key()) + if proposalID == id { + bz := it.Value() + var q v1.QuorumCheckQueueEntry + err := q.Unmarshal(bz) + return q, err == nil + } + } + return v1.QuorumCheckQueueEntry{}, false +} diff --git a/x/gov/genesis.go b/x/gov/genesis.go index 47723db2c..a1f221e0c 100644 --- a/x/gov/genesis.go +++ b/x/gov/genesis.go @@ -42,6 +42,29 @@ func InitGenesis(ctx sdk.Context, ak types.AccountKeeper, bk types.BankKeeper, k k.InsertActiveProposalQueue(ctx, proposal.Id, *proposal.VotingEndTime) } k.SetProposal(ctx, *proposal) + + if data.Params.QuorumCheckCount > 0 && proposal.Status == v1.StatusVotingPeriod { + quorumTimeoutTime := proposal.VotingStartTime.Add(*data.Params.QuorumTimeout) + quorumCheckEntry := v1.NewQuorumCheckQueueEntry(quorumTimeoutTime, data.Params.QuorumCheckCount) + quorum := false + if ctx.BlockTime().After(quorumTimeoutTime) { + var err error + quorum, err = k.HasReachedQuorum(ctx, *proposal) + if err != nil { + panic(err) + } + if !quorum { + // since we don't export the state of the quorum check queue, we can't know how many checks were actually + // done. However, in order to trigger a vote time extension, it is enough to have QuorumChecksDone > 0 to + // trigger a vote time extension, so we set it to 1 + quorumCheckEntry.QuorumChecksDone = 1 + } + } + if !quorum { + k.InsertQuorumCheckQueue(ctx, proposal.Id, quorumTimeoutTime, quorumCheckEntry) + } + } + } // if account has zero balance it probably means it's not set, so we set it diff --git a/x/gov/genesis_test.go b/x/gov/genesis_test.go index 64d5eec9e..d45b848c7 100644 --- a/x/gov/genesis_test.go +++ b/x/gov/genesis_test.go @@ -2,14 +2,23 @@ package gov_test import ( "testing" + "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/codec" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" "github.com/atomone-hub/atomone/x/gov" + "github.com/atomone-hub/atomone/x/gov/client/testutil" + govtypes "github.com/atomone-hub/atomone/x/gov/types" v1 "github.com/atomone-hub/atomone/x/gov/types/v1" ) @@ -37,3 +46,213 @@ func TestImportExportQueues_ErrorUnconsistentState(t *testing.T) { genState := gov.ExportGenesis(ctx, suite.GovKeeper) require.Equal(t, genState, v1.DefaultGenesisState()) } + +func TestInitGenesis(t *testing.T) { + var ( + testAddrs = simtestutil.CreateRandomAccounts(2) + params = &v1.Params{ + MinDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(42))), + } + quorumTimeout = time.Hour * 20 + paramsWithQuorumCheckEnabled = &v1.Params{ + MinDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(42))), + QuorumCheckCount: 10, + QuorumTimeout: &quorumTimeout, + } + + depositAmount = sdk.Coins{ + sdk.NewCoin( + "stake", + sdkmath.NewInt(1234), + ), + } + deposits = v1.Deposits{ + { + ProposalId: 1234, + Depositor: testAddrs[0].String(), + Amount: depositAmount, + }, + { + ProposalId: 1234, + Depositor: testAddrs[1].String(), + Amount: depositAmount, + }, + } + votes = []*v1.Vote{ + { + ProposalId: 1234, + Voter: testAddrs[0].String(), + Options: v1.NewNonSplitVoteOption(v1.OptionYes), + }, + { + ProposalId: 1234, + Voter: testAddrs[1].String(), + Options: v1.NewNonSplitVoteOption(v1.OptionNo), + }, + } + depositEndTime = time.Now().Add(time.Hour * 8) + votingStartTime = time.Now() + votingEndTime = time.Now().Add(time.Hour * 24) + proposals = []*v1.Proposal{ + { + Id: 1234, + Status: v1.StatusVotingPeriod, + DepositEndTime: &depositEndTime, + VotingStartTime: &votingStartTime, + VotingEndTime: &votingEndTime, + }, + { + Id: 12345, + Status: v1.StatusDepositPeriod, + DepositEndTime: &depositEndTime, + VotingStartTime: &votingStartTime, + VotingEndTime: &votingEndTime, + }, + { + Id: 123456, + Status: v1.StatusVotingPeriod, + DepositEndTime: &depositEndTime, + VotingStartTime: &votingStartTime, + VotingEndTime: &votingEndTime, + }, + } + assertProposals = func(t *testing.T, ctx sdk.Context, s suite, expectedProposals []*v1.Proposal) { + t.Helper() + assert := assert.New(t) + params := s.GovKeeper.GetParams(ctx) + proposals := s.GovKeeper.GetProposals(ctx) + cdc := codec.NewLegacyAmino() + expPropJSON := cdc.MustMarshalJSON(expectedProposals) + propJSON := cdc.MustMarshalJSON(proposals) + assert.JSONEq(string(expPropJSON), string(propJSON)) + // Check gov queues + for _, p := range proposals { + switch p.Status { + case v1.StatusVotingPeriod: + assert.True(testutil.HasActiveProposal(ctx, s.GovKeeper, p.Id, *p.VotingEndTime)) + assert.False(testutil.HasInactiveProposal(ctx, s.GovKeeper, p.Id, *p.DepositEndTime)) + if params.QuorumCheckCount > 0 { + assert.True(testutil.HasQuorumCheck(ctx, s.GovKeeper, p.Id, p.VotingStartTime.Add(*params.QuorumTimeout))) + } + case v1.StatusDepositPeriod: + assert.False(testutil.HasActiveProposal(ctx, s.GovKeeper, p.Id, *p.VotingEndTime)) + assert.True(testutil.HasInactiveProposal(ctx, s.GovKeeper, p.Id, *p.DepositEndTime)) + } + } + } + ) + + tests := []struct { + name string + genesis v1.GenesisState + moduleBalance sdk.Coins + requirePanic bool + assert func(*testing.T, sdk.Context, suite) + }{ + { + name: "fail: genesis without params", + requirePanic: true, + }, + { + name: "ok: genesis with only params", + genesis: v1.GenesisState{ + Params: params, + }, + assert: func(t *testing.T, ctx sdk.Context, s suite) { + t.Helper() + p := s.GovKeeper.GetParams(ctx) + assert.Equal(t, *params, p) + }, + }, + { + name: "fail: genesis with deposits but module balance is not equal to total deposits", + moduleBalance: depositAmount, + genesis: v1.GenesisState{ + Params: params, + Deposits: deposits, + }, + requirePanic: true, + }, + { + name: "ok: genesis with deposits and module balance is equal to total deposits", + moduleBalance: depositAmount.MulInt(sdkmath.NewInt(2)), // *2 because there's 2 deposits + genesis: v1.GenesisState{ + Params: params, + Deposits: deposits, + }, + assert: func(t *testing.T, ctx sdk.Context, s suite) { + t.Helper() + p := s.GovKeeper.GetParams(ctx) + assert.Equal(t, *params, p) + ds := s.GovKeeper.GetDeposits(ctx, deposits[0].ProposalId) + assert.ElementsMatch(t, deposits, ds) + }, + }, + { + name: "ok: genesis with votes", + genesis: v1.GenesisState{ + Params: params, + Votes: votes, + }, + assert: func(t *testing.T, ctx sdk.Context, s suite) { + t.Helper() + p := s.GovKeeper.GetParams(ctx) + assert.Equal(t, *params, p) + vs := s.GovKeeper.GetVotes(ctx, 1234) + assert.ElementsMatch(t, v1.Votes(votes), vs) + }, + }, + { + name: "ok: genesis with proposals", + genesis: v1.GenesisState{ + Params: params, + Proposals: proposals, + }, + assert: func(t *testing.T, ctx sdk.Context, s suite) { + t.Helper() + p := s.GovKeeper.GetParams(ctx) + assert.Equal(t, *params, p) + assertProposals(t, ctx, s, proposals) + }, + }, + { + name: "ok: genesis with proposals and quorum check enabled", + genesis: v1.GenesisState{ + Params: paramsWithQuorumCheckEnabled, + Proposals: proposals, + }, + assert: func(t *testing.T, ctx sdk.Context, s suite) { + t.Helper() + p := s.GovKeeper.GetParams(ctx) + assert.Equal(t, *paramsWithQuorumCheckEnabled, p) + assertProposals(t, ctx, s, proposals) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + suite := createTestSuite(t) + app := suite.App + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + if tt.moduleBalance.IsAllPositive() { + err := suite.BankKeeper.MintCoins(ctx, minttypes.ModuleName, tt.moduleBalance) + require.NoError(t, err) + err = suite.BankKeeper.SendCoinsFromModuleToModule(ctx, minttypes.ModuleName, govtypes.ModuleName, tt.moduleBalance) + require.NoError(t, err) + } + if tt.requirePanic { + defer func() { + require.NotNil(t, recover()) + }() + } + + gov.InitGenesis(ctx, suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper, &tt.genesis) + + if tt.requirePanic { + require.Fail(t, "should have panic") + return + } + tt.assert(t, ctx, suite) + }) + } +} diff --git a/x/gov/keeper/keeper.go b/x/gov/keeper/keeper.go index 523eb863a..7d8ebf0af 100644 --- a/x/gov/keeper/keeper.go +++ b/x/gov/keeper/keeper.go @@ -168,6 +168,19 @@ func (keeper Keeper) RemoveFromInactiveProposalQueue(ctx sdk.Context, proposalID store.Delete(types.InactiveProposalQueueKey(proposalID, endTime)) } +// InsertQuorumCheckQueue inserts a QuorumCheckEntry into the quorum check queue. +func (keeper Keeper) InsertQuorumCheckQueue(ctx sdk.Context, proposalID uint64, endTime time.Time, q v1.QuorumCheckQueueEntry) { + store := ctx.KVStore(keeper.storeKey) + bz := keeper.cdc.MustMarshal(&q) + store.Set(types.QuorumCheckQueueKey(proposalID, endTime), bz) +} + +// RemoveFromQuorumCheckQueue removes from the quorum check queue. +func (keeper Keeper) RemoveFromQuorumCheckQueue(ctx sdk.Context, proposalID uint64, endTime time.Time) { + store := ctx.KVStore(keeper.storeKey) + store.Delete(types.QuorumCheckQueueKey(proposalID, endTime)) +} + // Iterators // IterateActiveProposalsQueue iterates over the proposals in the active proposal queue @@ -208,6 +221,26 @@ func (keeper Keeper) IterateInactiveProposalsQueue(ctx sdk.Context, endTime time } } +// IterateQuorumCheckQueue iterates over the proposals in the quorum check queue +// and performs a callback function +func (keeper Keeper) IterateQuorumCheckQueue(ctx sdk.Context, endTime time.Time, cb func(v1.Proposal, time.Time, v1.QuorumCheckQueueEntry) (stop bool)) { + iterator := keeper.QuorumCheckQueueIterator(ctx, endTime) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + proposalID, endTime := types.SplitQuorumQueueKey(iterator.Key()) + proposal, found := keeper.GetProposal(ctx, proposalID) + if !found { + panic(fmt.Sprintf("proposal %d does not exist", proposalID)) + } + var q v1.QuorumCheckQueueEntry + keeper.cdc.MustUnmarshal(iterator.Value(), &q) + if cb(proposal, endTime, q) { + break + } + } +} + // ActiveProposalQueueIterator returns an sdk.Iterator for all the proposals in the Active Queue that expire by endTime func (keeper Keeper) ActiveProposalQueueIterator(ctx sdk.Context, endTime time.Time) sdk.Iterator { store := ctx.KVStore(keeper.storeKey) @@ -220,6 +253,12 @@ func (keeper Keeper) InactiveProposalQueueIterator(ctx sdk.Context, endTime time return store.Iterator(types.InactiveProposalQueuePrefix, sdk.PrefixEndBytes(types.InactiveProposalByTimeKey(endTime))) } +// QuorumCheckQueueIterator returns an sdk.Iterator for all the proposals in the QuorumCheck Queue that expire by endTime +func (keeper Keeper) QuorumCheckQueueIterator(ctx sdk.Context, endTime time.Time) sdk.Iterator { + store := ctx.KVStore(keeper.storeKey) + return store.Iterator(types.QuorumCheckQueuePrefix, sdk.PrefixEndBytes(types.QuorumCheckByTimeKey(endTime))) +} + // assertMetadataLength returns an error if given metadata length // is greater than a pre-defined MaxMetadataLen. func (keeper Keeper) assertMetadataLength(metadata string) error { diff --git a/x/gov/keeper/msg_server_test.go b/x/gov/keeper/msg_server_test.go index 2489db4f3..41d610654 100644 --- a/x/gov/keeper/msg_server_test.go +++ b/x/gov/keeper/msg_server_test.go @@ -1104,6 +1104,104 @@ func (suite *KeeperTestSuite) TestMsgUpdateParams() { expErr: true, expErrMsg: fmt.Sprintf("voting period must be at least %s: 0s", v1.MinVotingPeriod.String()), }, + { + name: "invalid quorum timeout", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.QuorumCheckCount = 1 // enable quorum check + params1.QuorumTimeout = nil + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "quorum timeout must not be nil: 0", + }, + { + name: "negative quorum timeout", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.QuorumCheckCount = 1 // enable quorum check + d := time.Duration(-1) + params1.QuorumTimeout = &d + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "quorum timeout must be 0 or greater: -1ns", + }, + { + name: "quorum timeout exceeds voting period", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.QuorumCheckCount = 1 // enable quorum check + d := *params.VotingPeriod + 1 + params1.QuorumTimeout = &d + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "quorum timeout 504h0m0.000000001s must be strictly less than the voting period 504h0m0s", + }, + { + name: "invalid max voting period extension", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.QuorumCheckCount = 1 // enable quorum check + d := *params.VotingPeriod - time.Hour*2 + params1.QuorumTimeout = &d + params1.MaxVotingPeriodExtension = nil + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "max voting period extension must not be nil: 0", + }, + { + name: "voting period extension below voting period - quorum timeout", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.QuorumCheckCount = 1 // enable quorum check + d := *params.VotingPeriod - time.Hour*2 + params1.QuorumTimeout = &d + d2 := *params1.VotingPeriod - *params1.QuorumTimeout - 1 + params1.MaxVotingPeriodExtension = &d2 + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "max voting period extension 1h59m59.999999999s must be greater than or equal to the difference between the voting period 504h0m0s and the quorum timeout 502h0m0s", + }, + { + name: "valid with quorum check enabled", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.QuorumCheckCount = 1 // enable quorum check + d := *params.VotingPeriod - time.Hour*2 + params1.QuorumTimeout = &d + d2 := *params1.VotingPeriod - *params1.QuorumTimeout + time.Hour*24 + params1.MaxVotingPeriodExtension = &d2 + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + }, } for _, tc := range testCases { diff --git a/x/gov/keeper/proposal.go b/x/gov/keeper/proposal.go index aabc53871..1aee0e6fd 100644 --- a/x/gov/keeper/proposal.go +++ b/x/gov/keeper/proposal.go @@ -3,6 +3,7 @@ package keeper import ( "errors" "fmt" + "time" sdkerrors "cosmossdk.io/errors" @@ -161,6 +162,19 @@ func (keeper Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) { if proposal.VotingEndTime != nil { keeper.RemoveFromActiveProposalQueue(ctx, proposalID, *proposal.VotingEndTime) store.Delete(types.VotingPeriodProposalKey(proposalID)) + // Delete from QuorumCheckQueue: as we do not know with certainty the value + // of the first part of the key (the time part), we need to iterate over it, + // starting by proposal.VotingStartTime, because we know for sure that the + // time part is greater than that. + keeper.IterateQuorumCheckQueue(ctx, *proposal.VotingStartTime, + func(p v1.Proposal, t time.Time, _ v1.QuorumCheckQueueEntry) bool { + if p.Id == proposalID { + // found the proposal, delete from queue and stop + keeper.RemoveFromQuorumCheckQueue(ctx, p.Id, t) + return true + } + return false + }) } store.Delete(types.ProposalKey(proposalID)) @@ -264,14 +278,21 @@ func (keeper Keeper) SetProposalID(ctx sdk.Context, proposalID uint64) { func (keeper Keeper) ActivateVotingPeriod(ctx sdk.Context, proposal v1.Proposal) { startTime := ctx.BlockHeader().Time proposal.VotingStartTime = &startTime - votingPeriod := keeper.GetParams(ctx).VotingPeriod - endTime := proposal.VotingStartTime.Add(*votingPeriod) + params := keeper.GetParams(ctx) + endTime := proposal.VotingStartTime.Add(*params.VotingPeriod) proposal.VotingEndTime = &endTime proposal.Status = v1.StatusVotingPeriod keeper.SetProposal(ctx, proposal) keeper.RemoveFromInactiveProposalQueue(ctx, proposal.Id, *proposal.DepositEndTime) keeper.InsertActiveProposalQueue(ctx, proposal.Id, *proposal.VotingEndTime) + if params.QuorumCheckCount > 0 { + // add proposal to quorum check queue + quorumTimeoutTime := proposal.VotingStartTime.Add(*params.QuorumTimeout) + keeper.InsertQuorumCheckQueue(ctx, proposal.Id, quorumTimeoutTime, + v1.NewQuorumCheckQueueEntry(quorumTimeoutTime, params.QuorumCheckCount), + ) + } } // MarshalProposal marshals the proposal and returns binary encoded bytes. diff --git a/x/gov/keeper/proposal_test.go b/x/gov/keeper/proposal_test.go index 8c823c248..ccca4f59d 100644 --- a/x/gov/keeper/proposal_test.go +++ b/x/gov/keeper/proposal_test.go @@ -12,6 +12,7 @@ import ( "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/atomone-hub/atomone/x/gov/client/testutil" "github.com/atomone-hub/atomone/x/gov/types" v1 "github.com/atomone-hub/atomone/x/gov/types/v1" "github.com/atomone-hub/atomone/x/gov/types/v1beta1" @@ -47,6 +48,15 @@ func (suite *KeeperTestSuite) TestDeleteProposal() { } func (suite *KeeperTestSuite) TestActivateVotingPeriod() { + params := v1.DefaultParams() + params.QuorumCheckCount = 10 // enable quorum check + quorumTimeout := *params.VotingPeriod - time.Hour*16 + params.QuorumTimeout = &quorumTimeout + maxVotingPeriodExtension := time.Hour * 16 + params.MaxVotingPeriodExtension = &maxVotingPeriodExtension + err := suite.govKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + tp := TestProposal proposal, err := suite.govKeeper.SubmitProposal(suite.ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) suite.Require().NoError(err) @@ -66,6 +76,13 @@ func (suite *KeeperTestSuite) TestActivateVotingPeriod() { suite.Require().Equal(proposalID, proposal.Id) activeIterator.Close() + quorumTimeoutTime := proposal.VotingStartTime.Add(*params.QuorumTimeout) + quorumCheckEntry, ok := testutil.GetQuorumCheckQueueEntry(suite.ctx, suite.govKeeper, proposal.Id, quorumTimeoutTime) + suite.Require().True(ok) + suite.Require().EqualValues(quorumTimeoutTime, *quorumCheckEntry.QuorumTimeoutTime) + suite.Require().EqualValues(params.QuorumCheckCount, quorumCheckEntry.QuorumCheckCount) + suite.Require().Zero(quorumCheckEntry.QuorumChecksDone) + // delete the proposal to avoid issues with other tests suite.Require().NotPanics(func() { suite.govKeeper.DeleteProposal(suite.ctx, proposalID) diff --git a/x/gov/keeper/tally.go b/x/gov/keeper/tally.go index b84760335..c7ec81b17 100644 --- a/x/gov/keeper/tally.go +++ b/x/gov/keeper/tally.go @@ -1,6 +1,8 @@ package keeper import ( + "fmt" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -10,28 +12,118 @@ import ( "github.com/atomone-hub/atomone/x/gov/types/v1beta1" ) -// TODO: Break into several smaller functions for clarity - // Tally iterates over the votes and updates the tally of a proposal based on the voting power of the // voters func (keeper Keeper) Tally(ctx sdk.Context, proposal v1.Proposal) (passes bool, burnDeposits bool, tallyResults v1.TallyResult) { - results := make(map[v1.VoteOption]sdk.Dec) - results[v1.OptionYes] = math.LegacyZeroDec() - results[v1.OptionAbstain] = math.LegacyZeroDec() - results[v1.OptionNo] = math.LegacyZeroDec() + // fetch all the bonded validators + currValidators := keeper.getBondedValidatorsByAddress(ctx) + totalVotingPower, results := keeper.tallyVotes(ctx, proposal, currValidators, true) + + params := keeper.GetParams(ctx) + tallyResults = v1.NewTallyResultFromMap(results) + + // If there is no staked coins, the proposal fails + totalBonded := keeper.sk.TotalBondedTokens(ctx) + if totalBonded.IsZero() { + return false, false, tallyResults + } + + // If there is not enough quorum of votes, the proposal fails + percentVoting := totalVotingPower.Quo(math.LegacyNewDecFromInt(totalBonded)) + quorum, threshold, err := keeper.getQuorumAndThreshold(ctx, proposal) + if err != nil { + return false, false, tallyResults + } + if percentVoting.LT(quorum) { + return false, params.BurnVoteQuorum, tallyResults + } + + // If no one votes (everyone abstains), proposal fails + if totalVotingPower.Sub(results[v1.OptionAbstain]).Equal(math.LegacyZeroDec()) { + return false, false, tallyResults + } + + if results[v1.OptionYes].Quo(totalVotingPower.Sub(results[v1.OptionAbstain])).GT(threshold) { + return true, false, tallyResults + } + + // If more than 1/2 of non-abstaining voters vote No, proposal fails + return false, false, tallyResults +} + +// HasReachedQuorum returns whether or not a proposal has reached quorum +// this is just a stripped down version of the Tally function above +func (keeper Keeper) HasReachedQuorum(ctx sdk.Context, proposal v1.Proposal) (quorumPassed bool, err error) { + // If there is no staked coins, the proposal has not reached quorum + totalBonded := keeper.sk.TotalBondedTokens(ctx) + if totalBonded.IsZero() { + return false, nil + } + + /* DISABLED on AtomOne - no possible increase of computation speed by + iterating over validators since vote inheritance is disabled. + Keeping as comment because this should be adapted with governors loop + + // we check first if voting power of validators alone is enough to pass quorum + // and if so, we return true skipping the iteration over all votes + // can speed up computation in case quorum is already reached by validator votes alone + approxTotalVotingPower := math.LegacyZeroDec() + for _, val := range currValidators { + _, ok := keeper.GetVote(ctx, proposal.Id, sdk.AccAddress(val.GetOperator())) + if ok { + approxTotalVotingPower = approxTotalVotingPower.Add(math.LegacyNewDecFromInt(val.GetBondedTokens())) + } + } + // check and return whether or not the proposal has reached quorum + approxPercentVoting := approxTotalVotingPower.Quo(math.LegacyNewDecFromInt(totalBonded)) + if approxPercentVoting.GTE(quorum) { + return true, nil + } + */ - totalVotingPower := math.LegacyZeroDec() - currValidators := make(map[string]stakingtypes.ValidatorI) + // voting power of validators does not reach quorum, let's tally all votes + currValidators := keeper.getBondedValidatorsByAddress(ctx) + totalVotingPower, _ := keeper.tallyVotes(ctx, proposal, currValidators, false) - // fetch all the bonded validators, insert them into currValidators + // check and return whether or not the proposal has reached quorum + percentVoting := totalVotingPower.Quo(math.LegacyNewDecFromInt(totalBonded)) + quorum, _, err := keeper.getQuorumAndThreshold(ctx, proposal) + if err != nil { + return false, err + } + return percentVoting.GTE(quorum), nil +} + +// getBondedValidatorsByAddress fetches all the bonded validators and return +// them in map using their operator address as the key. +func (keeper Keeper) getBondedValidatorsByAddress(ctx sdk.Context) map[string]stakingtypes.ValidatorI { + vals := make(map[string]stakingtypes.ValidatorI) keeper.sk.IterateBondedValidatorsByPower(ctx, func(index int64, validator stakingtypes.ValidatorI) (stop bool) { - currValidators[validator.GetOperator().String()] = validator + vals[validator.GetOperator().String()] = validator return false }) + return vals +} + +// tallyVotes returns the total voting power and tally results of the votes +// on a proposal. If `isFinal` is true, results will be stored in `results` +// map and votes will be deleted. Otherwise, only the total voting power +// will be returned and `results` will be nil. +func (keeper Keeper) tallyVotes( + ctx sdk.Context, proposal v1.Proposal, + currValidators map[string]stakingtypes.ValidatorI, isFinal bool, +) (totalVotingPower math.LegacyDec, results map[v1.VoteOption]math.LegacyDec) { + totalVotingPower = math.LegacyZeroDec() + if isFinal { + results = make(map[v1.VoteOption]math.LegacyDec) + results[v1.OptionYes] = math.LegacyZeroDec() + results[v1.OptionAbstain] = math.LegacyZeroDec() + results[v1.OptionNo] = math.LegacyZeroDec() + } keeper.IterateVotes(ctx, proposal.Id, func(vote v1.Vote) bool { voter := sdk.MustAccAddressFromBech32(vote.Voter) - // iterate over all delegations from voter + // iterate over all delegations from voter, deduct from any delegated-to validators keeper.sk.IterateDelegations(ctx, voter, func(index int64, delegation stakingtypes.DelegationI) (stop bool) { valAddrStr := delegation.GetValidatorAddr().String() @@ -39,10 +131,12 @@ func (keeper Keeper) Tally(ctx sdk.Context, proposal v1.Proposal) (passes bool, // delegation shares * bonded / total shares votingPower := delegation.GetShares().MulInt(val.GetBondedTokens()).Quo(val.GetDelegatorShares()) - for _, option := range vote.Options { - weight, _ := sdk.NewDecFromStr(option.Weight) - subPower := votingPower.Mul(weight) - results[option.Option] = results[option.Option].Add(subPower) + if isFinal { + for _, option := range vote.Options { + weight, _ := math.LegacyNewDecFromStr(option.Weight) + subPower := votingPower.Mul(weight) + results[option.Option] = results[option.Option].Add(subPower) + } } totalVotingPower = totalVotingPower.Add(votingPower) } @@ -50,7 +144,9 @@ func (keeper Keeper) Tally(ctx sdk.Context, proposal v1.Proposal) (passes bool, return false }) - keeper.deleteVote(ctx, vote.ProposalId, voter) + if isFinal { + keeper.deleteVote(ctx, vote.ProposalId, voter) + } return false }) @@ -73,20 +169,21 @@ func (keeper Keeper) Tally(ctx sdk.Context, proposal v1.Proposal) (passes bool, } */ - params := keeper.GetParams(ctx) - tallyResults = v1.NewTallyResultFromMap(results) + return totalVotingPower, results +} - // TODO: Upgrade the spec to cover all of these cases & remove pseudocode. - // If there is no staked coins, the proposal fails - totalBondedTokens := keeper.sk.TotalBondedTokens(ctx) - if totalBondedTokens.IsZero() { - return false, false, tallyResults +// getQuorumAndThreshold iterates over the proposal's messages to returns the +// appropriate quorum and threshold. +func (keeper Keeper) getQuorumAndThreshold(ctx sdk.Context, proposal v1.Proposal) (sdk.Dec, sdk.Dec, error) { + params := keeper.GetParams(ctx) + quorum, err := sdk.NewDecFromStr(params.Quorum) + if err != nil { + return sdk.Dec{}, sdk.Dec{}, fmt.Errorf("parsing params.Quorum: %w", err) + } + threshold, err := sdk.NewDecFromStr(params.Threshold) + if err != nil { + return sdk.Dec{}, sdk.Dec{}, fmt.Errorf("parsing params.Threshold: %w", err) } - - // If there is not enough quorum of votes, the proposal fails - percentVoting := totalVotingPower.Quo(sdk.NewDecFromInt(totalBondedTokens)) - quorum, _ := sdk.NewDecFromStr(params.Quorum) - threshold, _ := sdk.NewDecFromStr(params.Threshold) // Check if a proposal message is an ExecLegacyContent message if len(proposal.Messages) > 0 { @@ -99,27 +196,39 @@ func (keeper Keeper) Tally(ctx sdk.Context, proposal v1.Proposal) (passes bool, } var content v1beta1.Content if err := keeper.cdc.UnpackAny(execMsg.Content, &content); err != nil { - return false, false, tallyResults + return sdk.Dec{}, sdk.Dec{}, err } // Check if proposal is a law or constitution amendment and adjust the // quorum and threshold accordingly switch content.(type) { case *v1beta1.ConstitutionAmendmentProposal: - q, _ := sdk.NewDecFromStr(params.ConstitutionAmendmentQuorum) + q, err := sdk.NewDecFromStr(params.ConstitutionAmendmentQuorum) + if err != nil { + return sdk.Dec{}, sdk.Dec{}, fmt.Errorf("parsing params.ConstitutionAmendmentQuorum: %w", err) + } if quorum.LT(q) { quorum = q } - t, _ := sdk.NewDecFromStr(params.ConstitutionAmendmentThreshold) + t, err := sdk.NewDecFromStr(params.ConstitutionAmendmentThreshold) + if err != nil { + return sdk.Dec{}, sdk.Dec{}, fmt.Errorf("parsing params.ConstitutionAmendmentThreshold: %w", err) + } if threshold.LT(t) { threshold = t } case *v1beta1.LawProposal: - q, _ := sdk.NewDecFromStr(params.LawQuorum) + q, err := sdk.NewDecFromStr(params.LawQuorum) + if err != nil { + return sdk.Dec{}, sdk.Dec{}, fmt.Errorf("parsing params.LawQuorum: %w", err) + } if quorum.LT(q) { quorum = q } - t, _ := sdk.NewDecFromStr(params.LawThreshold) + t, err := sdk.NewDecFromStr(params.LawThreshold) + if err != nil { + return sdk.Dec{}, sdk.Dec{}, fmt.Errorf("parsing params.LawThreshold: %w", err) + } if threshold.LT(t) { threshold = t } @@ -127,21 +236,5 @@ func (keeper Keeper) Tally(ctx sdk.Context, proposal v1.Proposal) (passes bool, } } } - - if percentVoting.LT(quorum) { - return false, params.BurnVoteQuorum, tallyResults - } - - // If no one votes (everyone abstains), proposal fails - if totalVotingPower.Sub(results[v1.OptionAbstain]).Equal(math.LegacyZeroDec()) { - return false, false, tallyResults - } - - // If more than 2/3 of non-abstaining voters vote Yes, proposal passes - if results[v1.OptionYes].Quo(totalVotingPower.Sub(results[v1.OptionAbstain])).GT(threshold) { - return true, false, tallyResults - } - - // If more than 1/3 of non-abstaining voters vote No, proposal fails - return false, false, tallyResults + return quorum, threshold, nil } diff --git a/x/gov/keeper/tally_test.go b/x/gov/keeper/tally_test.go index 74e5438ad..52dffc3e0 100644 --- a/x/gov/keeper/tally_test.go +++ b/x/gov/keeper/tally_test.go @@ -471,3 +471,120 @@ func TestTally(t *testing.T) { }) } } + +func TestHasReachedQuorum(t *testing.T) { + tests := []struct { + name string + proposalMsgs []sdk.Msg + setup func(*tallyFixture) + expectedQuorum bool + }{ + { + name: "no votes: no quorum", + proposalMsgs: TestProposal, + setup: func(s *tallyFixture) { + }, + expectedQuorum: false, + }, + { + name: "not enough votes: no quorum", + proposalMsgs: TestProposal, + setup: func(s *tallyFixture) { + s.validatorVote(s.valAddrs[0], v1.VoteOption_VOTE_OPTION_NO) + s.validatorVote(s.valAddrs[1], v1.VoteOption_VOTE_OPTION_YES) + }, + expectedQuorum: false, + }, + { + name: "enough votes: quorum", + proposalMsgs: TestProposal, + setup: func(s *tallyFixture) { + s.validatorVote(s.valAddrs[0], v1.VoteOption_VOTE_OPTION_NO) + s.validatorVote(s.valAddrs[1], v1.VoteOption_VOTE_OPTION_YES) + s.delegate(s.delAddrs[0], s.valAddrs[2], 500000) + s.delegate(s.delAddrs[0], s.valAddrs[3], 500000) + s.delegate(s.delAddrs[0], s.valAddrs[4], 500000) + s.delegate(s.delAddrs[0], s.valAddrs[5], 500000) + s.vote(s.delAddrs[0], v1.VoteOption_VOTE_OPTION_ABSTAIN) + }, + expectedQuorum: true, + }, + { + name: "amendment quorum not reached", + setup: func(s *tallyFixture) { + s.validatorVote(s.valAddrs[0], v1.VoteOption_VOTE_OPTION_YES) + s.validatorVote(s.valAddrs[1], v1.VoteOption_VOTE_OPTION_YES) + s.validatorVote(s.valAddrs[2], v1.VoteOption_VOTE_OPTION_NO) + }, + proposalMsgs: TestAmendmentProposal, + expectedQuorum: false, + }, + { + name: "amendment quorum reached", + setup: func(s *tallyFixture) { + s.validatorVote(s.valAddrs[0], v1.VoteOption_VOTE_OPTION_YES) + s.validatorVote(s.valAddrs[1], v1.VoteOption_VOTE_OPTION_YES) + s.validatorVote(s.valAddrs[2], v1.VoteOption_VOTE_OPTION_NO) + s.validatorVote(s.valAddrs[3], v1.VoteOption_VOTE_OPTION_YES) + s.validatorVote(s.valAddrs[4], v1.VoteOption_VOTE_OPTION_YES) + s.delegate(s.delAddrs[0], s.valAddrs[5], 2) + s.delegate(s.delAddrs[0], s.valAddrs[6], 2) + s.vote(s.delAddrs[0], v1.VoteOption_VOTE_OPTION_YES) + s.delegate(s.delAddrs[1], s.valAddrs[5], 1) + s.delegate(s.delAddrs[1], s.valAddrs[6], 1) + s.vote(s.delAddrs[1], v1.VoteOption_VOTE_OPTION_YES) + }, + proposalMsgs: TestAmendmentProposal, + expectedQuorum: true, + }, + { + name: "law quorum not reached", + setup: func(s *tallyFixture) { + s.validatorVote(s.valAddrs[0], v1.VoteOption_VOTE_OPTION_YES) + s.validatorVote(s.valAddrs[1], v1.VoteOption_VOTE_OPTION_YES) + s.validatorVote(s.valAddrs[2], v1.VoteOption_VOTE_OPTION_NO) + }, + proposalMsgs: TestLawProposal, + expectedQuorum: false, + }, + { + name: "law quorum reached", + setup: func(s *tallyFixture) { + s.validatorVote(s.valAddrs[0], v1.VoteOption_VOTE_OPTION_YES) + s.validatorVote(s.valAddrs[1], v1.VoteOption_VOTE_OPTION_YES) + s.validatorVote(s.valAddrs[3], v1.VoteOption_VOTE_OPTION_YES) + s.validatorVote(s.valAddrs[5], v1.VoteOption_VOTE_OPTION_ABSTAIN) + }, + proposalMsgs: TestLawProposal, + expectedQuorum: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + govKeeper, mocks, _, ctx := setupGovKeeper(t, mockAccountKeeperExpectations) + var ( + numVals = 10 + numDelegators = 5 + addrs = simtestutil.CreateRandomAccounts(numVals + numDelegators) + valAddrs = simtestutil.ConvertAddrsToValAddrs(addrs[:numVals]) + delAddrs = addrs[numVals:] + ) + // Submit and activate a proposal + proposal, err := govKeeper.SubmitProposal(ctx, tt.proposalMsgs, "", "title", "summary", delAddrs[0]) + require.NoError(t, err) + govKeeper.ActivateVotingPeriod(ctx, proposal) + suite := newTallyFixture(t, ctx, proposal, valAddrs, delAddrs, govKeeper, mocks) + tt.setup(suite) + + quorum, err := govKeeper.HasReachedQuorum(ctx, proposal) + + require.NoError(t, err) + assert.Equal(t, tt.expectedQuorum, quorum) + if tt.expectedQuorum { + // Assert votes are still here after HasReachedQuorum + votes := suite.keeper.GetVotes(suite.ctx, proposal.Id) + assert.NotEmpty(t, votes, "votes must be kept after HasReachedQuorum") + } + }) + } +} diff --git a/x/gov/simulation/genesis.go b/x/gov/simulation/genesis.go index 810131040..2dc910b70 100644 --- a/x/gov/simulation/genesis.go +++ b/x/gov/simulation/genesis.go @@ -32,7 +32,10 @@ const ( TallyParamsLawThreshold = "tally_params_law_threshold" // NOTE: backport from v50 - MinDepositRatio = "min_deposit_ratio" + MinDepositRatio = "min_deposit_ratio" + QuorumTimeout = "quorum_timeout" + MaxVotingPeriodExtension = "max_voting_period_extension" + QuorumCheckCount = "quorum_check_count" ) // GenDepositParamsDepositPeriod returns randomized DepositParamsDepositPeriod @@ -82,6 +85,22 @@ func GenTallyParamsConstitutionalThreshold(r *rand.Rand, minDec sdk.Dec) math.Le return sdk.NewDecWithPrec(int64(simulation.RandIntBetween(r, min, 950)), 3) } +// GenQuorumTimeout returns a randomized QuorumTimeout between 0 and votingPeriod +func GenQuorumTimeout(r *rand.Rand, votingPeriod time.Duration) time.Duration { + return time.Duration(simulation.RandIntBetween(r, 1, int(votingPeriod.Seconds()))) * time.Second +} + +// GenMaxVotingPeriodExtension returns a randomized MaxVotingPeriodExtension +// greater than votingPeriod-quorumTimout. +func GenMaxVotingPeriodExtension(r *rand.Rand, votingPeriod, quorumTimout time.Duration) time.Duration { + return time.Duration(simulation.RandIntBetween(r, 1, int(votingPeriod.Seconds())))*time.Second + (votingPeriod - quorumTimout) +} + +// GenQuorumCheckCount returns a randomized QuorumCheckCount between 0 and 30 +func GenQuorumCheckCount(r *rand.Rand) uint64 { + return uint64(simulation.RandIntBetween(r, 0, 30)) +} + // RandomizedGenState generates a random GenesisState for gov func RandomizedGenState(simState *module.SimulationState) { startingProposalID := uint64(simState.Rand.Intn(100)) @@ -149,9 +168,20 @@ func RandomizedGenState(simState *module.SimulationState) { func(r *rand.Rand) { amendmentsThreshold = GenTallyParamsConstitutionalThreshold(r, lawThreshold) }, ) + var quorumTimout time.Duration + simState.AppParams.GetOrGenerate(simState.Cdc, QuorumTimeout, &quorumTimout, simState.Rand, func(r *rand.Rand) { quorumTimout = GenQuorumTimeout(r, votingPeriod) }) + + var maxVotingPeriodExtension time.Duration + simState.AppParams.GetOrGenerate(simState.Cdc, MaxVotingPeriodExtension, &maxVotingPeriodExtension, simState.Rand, func(r *rand.Rand) { + maxVotingPeriodExtension = GenMaxVotingPeriodExtension(r, votingPeriod, quorumTimout) + }) + + var quorumCheckCount uint64 + simState.AppParams.GetOrGenerate(simState.Cdc, QuorumCheckCount, &quorumCheckCount, simState.Rand, func(r *rand.Rand) { quorumCheckCount = GenQuorumCheckCount(r) }) + govGenesis := v1.NewGenesisState( startingProposalID, - v1.NewParams(minDeposit, depositPeriod, votingPeriod, quorum.String(), threshold.String(), amendmentsQuorum.String(), amendmentsThreshold.String(), lawQuorum.String(), lawThreshold.String(), minInitialDepositRatio.String(), simState.Rand.Intn(2) == 0, simState.Rand.Intn(2) == 0, minDepositRatio.String()), + v1.NewParams(minDeposit, depositPeriod, votingPeriod, quorum.String(), threshold.String(), amendmentsQuorum.String(), amendmentsThreshold.String(), lawQuorum.String(), lawThreshold.String(), minInitialDepositRatio.String(), simState.Rand.Intn(2) == 0, simState.Rand.Intn(2) == 0, minDepositRatio.String(), quorumTimout, maxVotingPeriodExtension, quorumCheckCount), ) bz, err := json.MarshalIndent(&govGenesis, "", " ") diff --git a/x/gov/simulation/genesis_test.go b/x/gov/simulation/genesis_test.go index 23a8de2c0..0ffeda598 100644 --- a/x/gov/simulation/genesis_test.go +++ b/x/gov/simulation/genesis_test.go @@ -63,6 +63,9 @@ func TestRandomizedGenState(t *testing.T) { require.Equal(t, lawQuorum, govGenesis.Params.LawQuorum) require.Equal(t, lawThreshold, govGenesis.Params.LawThreshold) require.Equal(t, minInitialDepositDec, govGenesis.Params.MinInitialDepositRatio) + require.Equal(t, "7h46m6s", govGenesis.Params.QuorumTimeout.String()) + require.Equal(t, "82h43m30s", govGenesis.Params.MaxVotingPeriodExtension.String()) + require.Equal(t, uint64(5), govGenesis.Params.QuorumCheckCount) require.Equal(t, uint64(0x28), govGenesis.StartingProposalId) require.Equal(t, []*v1.Deposit{}, govGenesis.Deposits) require.Equal(t, []*v1.Vote{}, govGenesis.Votes) diff --git a/x/gov/types/events.go b/x/gov/types/events.go index 709d5b02c..6a3457f46 100644 --- a/x/gov/types/events.go +++ b/x/gov/types/events.go @@ -8,18 +8,22 @@ const ( EventTypeInactiveProposal = "inactive_proposal" EventTypeActiveProposal = "active_proposal" EventTypeSignalProposal = "signal_proposal" + EventTypeQuorumCheck = "quorum_check" - AttributeKeyVoter = "voter" - AttributeKeyProposalResult = "proposal_result" - AttributeKeyOption = "option" - AttributeKeyProposalID = "proposal_id" - AttributeKeyProposalMessages = "proposal_messages" // Msg type_urls in the proposal - AttributeKeyVotingPeriodStart = "voting_period_start" - 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 - AttributeKeyProposalType = "proposal_type" - AttributeSignalTitle = "signal_title" - AttributeSignalDescription = "signal_description" + AttributeKeyVoter = "voter" + AttributeKeyProposalResult = "proposal_result" + AttributeKeyOption = "option" + AttributeKeyProposalID = "proposal_id" + AttributeKeyProposalMessages = "proposal_messages" // Msg type_urls in the proposal + AttributeKeyVotingPeriodStart = "voting_period_start" + 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 + AttributeKeyProposalType = "proposal_type" + AttributeSignalTitle = "signal_title" + AttributeSignalDescription = "signal_description" + AttributeValueProposalQuorumMet = "proposal_quorum_met" // met quorum + AttributeValueProposalQuorumNotMet = "proposal_quorum_not_met" // didn't meet quorum + AttributeValueProposalQuorumCheckSkipped = "proposal_quorum_check_skipped" // skipped quorum check ) diff --git a/x/gov/types/keys.go b/x/gov/types/keys.go index fbb11f5d3..8d12a08bd 100644 --- a/x/gov/types/keys.go +++ b/x/gov/types/keys.go @@ -44,6 +44,7 @@ var ( InactiveProposalQueuePrefix = []byte{0x02} ProposalIDKey = []byte{0x03} VotingPeriodProposalKeyPrefix = []byte{0x04} + QuorumCheckQueuePrefix = []byte{0x05} DepositsKeyPrefix = []byte{0x10} @@ -100,6 +101,16 @@ func InactiveProposalQueueKey(proposalID uint64, endTime time.Time) []byte { return append(InactiveProposalByTimeKey(endTime), GetProposalIDBytes(proposalID)...) } +// QuorumCheckByTimeKey gets the quorum check queue key by endTime +func QuorumCheckByTimeKey(endTime time.Time) []byte { + return append(QuorumCheckQueuePrefix, sdk.FormatTimeBytes(endTime)...) +} + +// QuorumCheckQueueKey returns the key for a proposalID in the quorumCheckQueue +func QuorumCheckQueueKey(proposalID uint64, endTime time.Time) []byte { + return append(QuorumCheckByTimeKey(endTime), GetProposalIDBytes(proposalID)...) +} + // DepositsKey gets the first part of the deposits key based on the proposalID func DepositsKey(proposalID uint64) []byte { return append(DepositsKeyPrefix, GetProposalIDBytes(proposalID)...) @@ -139,6 +150,11 @@ func SplitInactiveProposalQueueKey(key []byte) (proposalID uint64, endTime time. return splitKeyWithTime(key) } +// SplitQuorumQueueKey split the quorum queue key and returns the proposal id and endTime +func SplitQuorumQueueKey(key []byte) (proposalID uint64, endTime time.Time) { + return splitKeyWithTime(key) +} + // SplitKeyDeposit split the deposits key and returns the proposal id and depositor address func SplitKeyDeposit(key []byte) (proposalID uint64, depositorAddr sdk.AccAddress) { return splitKeyWithAddress(key) diff --git a/x/gov/types/v1/gov.pb.go b/x/gov/types/v1/gov.pb.go index fbe9bcaf9..4c2182c6b 100644 --- a/x/gov/types/v1/gov.pb.go +++ b/x/gov/types/v1/gov.pb.go @@ -538,6 +538,73 @@ func (m *Vote) GetMetadata() string { return "" } +// QuorumCheckQueueEntry defines a quorum check queue entry. +type QuorumCheckQueueEntry struct { + // quorum_timeout_time is the time after which quorum checks start happening + // and voting period is extended if proposal reaches quorum. + QuorumTimeoutTime *time.Time `protobuf:"bytes,1,opt,name=quorum_timeout_time,json=quorumTimeoutTime,proto3,stdtime" json:"quorum_timeout_time,omitempty"` + // quorum_check_count is the number of times quorum will be checked. + // This is a snapshot of the parameter value with the same name when the + // proposal is initially added to the queue. + QuorumCheckCount uint64 `protobuf:"varint,2,opt,name=quorum_check_count,json=quorumCheckCount,proto3" json:"quorum_check_count,omitempty"` + // quorum_checks_done is the number of quorum checks that have been done. + QuorumChecksDone uint64 `protobuf:"varint,3,opt,name=quorum_checks_done,json=quorumChecksDone,proto3" json:"quorum_checks_done,omitempty"` +} + +func (m *QuorumCheckQueueEntry) Reset() { *m = QuorumCheckQueueEntry{} } +func (m *QuorumCheckQueueEntry) String() string { return proto.CompactTextString(m) } +func (*QuorumCheckQueueEntry) ProtoMessage() {} +func (*QuorumCheckQueueEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_ecf0f9950ff6986c, []int{5} +} +func (m *QuorumCheckQueueEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QuorumCheckQueueEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QuorumCheckQueueEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QuorumCheckQueueEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_QuorumCheckQueueEntry.Merge(m, src) +} +func (m *QuorumCheckQueueEntry) XXX_Size() int { + return m.Size() +} +func (m *QuorumCheckQueueEntry) XXX_DiscardUnknown() { + xxx_messageInfo_QuorumCheckQueueEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_QuorumCheckQueueEntry proto.InternalMessageInfo + +func (m *QuorumCheckQueueEntry) GetQuorumTimeoutTime() *time.Time { + if m != nil { + return m.QuorumTimeoutTime + } + return nil +} + +func (m *QuorumCheckQueueEntry) GetQuorumCheckCount() uint64 { + if m != nil { + return m.QuorumCheckCount + } + return 0 +} + +func (m *QuorumCheckQueueEntry) GetQuorumChecksDone() uint64 { + if m != nil { + return m.QuorumChecksDone + } + return 0 +} + // DepositParams defines the params for deposits on governance proposals. type DepositParams struct { // Minimum deposit for a proposal to enter voting period. @@ -551,7 +618,7 @@ func (m *DepositParams) Reset() { *m = DepositParams{} } func (m *DepositParams) String() string { return proto.CompactTextString(m) } func (*DepositParams) ProtoMessage() {} func (*DepositParams) Descriptor() ([]byte, []int) { - return fileDescriptor_ecf0f9950ff6986c, []int{5} + return fileDescriptor_ecf0f9950ff6986c, []int{6} } func (m *DepositParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -604,7 +671,7 @@ func (m *VotingParams) Reset() { *m = VotingParams{} } func (m *VotingParams) String() string { return proto.CompactTextString(m) } func (*VotingParams) ProtoMessage() {} func (*VotingParams) Descriptor() ([]byte, []int) { - return fileDescriptor_ecf0f9950ff6986c, []int{6} + return fileDescriptor_ecf0f9950ff6986c, []int{7} } func (m *VotingParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -661,7 +728,7 @@ func (m *TallyParams) Reset() { *m = TallyParams{} } func (m *TallyParams) String() string { return proto.CompactTextString(m) } func (*TallyParams) ProtoMessage() {} func (*TallyParams) Descriptor() ([]byte, []int) { - return fileDescriptor_ecf0f9950ff6986c, []int{7} + return fileDescriptor_ecf0f9950ff6986c, []int{8} } func (m *TallyParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -770,13 +837,22 @@ type Params struct { LawQuorum string `protobuf:"bytes,18,opt,name=law_quorum,json=lawQuorum,proto3" json:"law_quorum,omitempty"` // Minimum proportion of Yes votes for a Law proposal to pass. Default value: 0.9. LawThreshold string `protobuf:"bytes,19,opt,name=law_threshold,json=lawThreshold,proto3" json:"law_threshold,omitempty"` + // Duration of time after a proposal enters the voting period, during which quorum + // must be achieved to not incur in a voting period extension. + QuorumTimeout *time.Duration `protobuf:"bytes,20,opt,name=quorum_timeout,json=quorumTimeout,proto3,stdduration" json:"quorum_timeout,omitempty"` + // Duration that expresses the maximum amount of time by which a proposal voting period + // can be extended. + MaxVotingPeriodExtension *time.Duration `protobuf:"bytes,21,opt,name=max_voting_period_extension,json=maxVotingPeriodExtension,proto3,stdduration" json:"max_voting_period_extension,omitempty"` + // Number of times a proposal should be checked for quorum after the quorum timeout + // has elapsed. Used to compute the amount of time in between quorum checks. + QuorumCheckCount uint64 `protobuf:"varint,22,opt,name=quorum_check_count,json=quorumCheckCount,proto3" json:"quorum_check_count,omitempty"` } func (m *Params) Reset() { *m = Params{} } func (m *Params) String() string { return proto.CompactTextString(m) } func (*Params) ProtoMessage() {} func (*Params) Descriptor() ([]byte, []int) { - return fileDescriptor_ecf0f9950ff6986c, []int{8} + return fileDescriptor_ecf0f9950ff6986c, []int{9} } func (m *Params) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -896,6 +972,27 @@ func (m *Params) GetLawThreshold() string { return "" } +func (m *Params) GetQuorumTimeout() *time.Duration { + if m != nil { + return m.QuorumTimeout + } + return nil +} + +func (m *Params) GetMaxVotingPeriodExtension() *time.Duration { + if m != nil { + return m.MaxVotingPeriodExtension + } + return nil +} + +func (m *Params) GetQuorumCheckCount() uint64 { + if m != nil { + return m.QuorumCheckCount + } + return 0 +} + func init() { proto.RegisterEnum("atomone.gov.v1.VoteOption", VoteOption_name, VoteOption_value) proto.RegisterEnum("atomone.gov.v1.ProposalStatus", ProposalStatus_name, ProposalStatus_value) @@ -904,6 +1001,7 @@ func init() { proto.RegisterType((*Proposal)(nil), "atomone.gov.v1.Proposal") proto.RegisterType((*TallyResult)(nil), "atomone.gov.v1.TallyResult") proto.RegisterType((*Vote)(nil), "atomone.gov.v1.Vote") + proto.RegisterType((*QuorumCheckQueueEntry)(nil), "atomone.gov.v1.QuorumCheckQueueEntry") proto.RegisterType((*DepositParams)(nil), "atomone.gov.v1.DepositParams") proto.RegisterType((*VotingParams)(nil), "atomone.gov.v1.VotingParams") proto.RegisterType((*TallyParams)(nil), "atomone.gov.v1.TallyParams") @@ -913,90 +1011,98 @@ func init() { func init() { proto.RegisterFile("atomone/gov/v1/gov.proto", fileDescriptor_ecf0f9950ff6986c) } var fileDescriptor_ecf0f9950ff6986c = []byte{ - // 1325 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x57, 0xcf, 0x6f, 0xdb, 0xc6, - 0x12, 0x36, 0xf5, 0xcb, 0xf2, 0xc8, 0x96, 0xe9, 0x8d, 0x5f, 0x42, 0xcb, 0xb1, 0xe4, 0x27, 0x3c, - 0x04, 0x7e, 0x69, 0x2c, 0xd5, 0x4e, 0x91, 0x43, 0x91, 0x8b, 0x6c, 0x31, 0x29, 0x83, 0xd4, 0x52, - 0x29, 0xc5, 0x6d, 0x7a, 0x21, 0x56, 0xe6, 0x46, 0x26, 0x2a, 0x72, 0x55, 0x72, 0x69, 0x47, 0xff, - 0x45, 0x8e, 0x45, 0x4f, 0x3d, 0xf6, 0xd8, 0x43, 0x80, 0x5e, 0x0b, 0xf4, 0x92, 0x53, 0x11, 0xe4, - 0xd4, 0x5e, 0xd2, 0x22, 0x39, 0x14, 0xc8, 0xbd, 0xf7, 0x62, 0x97, 0x4b, 0x49, 0x96, 0x15, 0xc8, - 0x09, 0xda, 0x8b, 0x4d, 0xce, 0x7c, 0xdf, 0xcc, 0xec, 0xce, 0x37, 0xbb, 0x14, 0x68, 0x98, 0x51, - 0x97, 0x7a, 0xa4, 0xda, 0xa5, 0x27, 0xd5, 0x93, 0x1d, 0xfe, 0xaf, 0xd2, 0xf7, 0x29, 0xa3, 0x28, - 0x2f, 0x3d, 0x15, 0x6e, 0x3a, 0xd9, 0x29, 0x14, 0x8f, 0x68, 0xe0, 0xd2, 0xa0, 0xda, 0xc1, 0x01, - 0xa9, 0x9e, 0xec, 0x74, 0x08, 0xc3, 0x3b, 0xd5, 0x23, 0xea, 0x78, 0x11, 0xbe, 0xb0, 0xda, 0xa5, - 0x5d, 0x2a, 0x1e, 0xab, 0xfc, 0x49, 0x5a, 0x4b, 0x5d, 0x4a, 0xbb, 0x3d, 0x52, 0x15, 0x6f, 0x9d, - 0xf0, 0x51, 0x95, 0x39, 0x2e, 0x09, 0x18, 0x76, 0xfb, 0x12, 0xb0, 0x36, 0x09, 0xc0, 0xde, 0x40, - 0xba, 0x8a, 0x93, 0x2e, 0x3b, 0xf4, 0x31, 0x73, 0x68, 0x9c, 0x71, 0x2d, 0xaa, 0xc8, 0x8a, 0x92, - 0x46, 0x2f, 0xd2, 0xb5, 0x82, 0x5d, 0xc7, 0xa3, 0x55, 0xf1, 0x37, 0x32, 0x95, 0xfb, 0x80, 0x3e, - 0x27, 0x4e, 0xf7, 0x98, 0x11, 0xfb, 0x90, 0x32, 0xd2, 0xe8, 0xf3, 0x48, 0x68, 0x17, 0x32, 0x54, - 0x3c, 0x69, 0xca, 0xa6, 0xb2, 0x95, 0xdf, 0x2d, 0x54, 0xce, 0x2e, 0xbb, 0x32, 0xc2, 0x9a, 0x12, - 0x89, 0xae, 0x41, 0xe6, 0x54, 0x44, 0xd2, 0x12, 0x9b, 0xca, 0xd6, 0xc2, 0x5e, 0xfe, 0xc5, 0xd3, - 0x6d, 0x90, 0xe9, 0xeb, 0xe4, 0xc8, 0x94, 0xde, 0xf2, 0x77, 0x0a, 0xcc, 0xd7, 0x49, 0x9f, 0x06, - 0x0e, 0x43, 0x25, 0xc8, 0xf5, 0x7d, 0xda, 0xa7, 0x01, 0xee, 0x59, 0x8e, 0x2d, 0x92, 0xa5, 0x4c, - 0x88, 0x4d, 0x86, 0x8d, 0x6e, 0xc1, 0x82, 0x1d, 0x61, 0xa9, 0x2f, 0xe3, 0x6a, 0x2f, 0x9e, 0x6e, - 0xaf, 0xca, 0xb8, 0x35, 0xdb, 0xf6, 0x49, 0x10, 0xb4, 0x98, 0xef, 0x78, 0x5d, 0x73, 0x04, 0x45, - 0xb7, 0x21, 0x83, 0x5d, 0x1a, 0x7a, 0x4c, 0x4b, 0x6e, 0x26, 0xb7, 0x72, 0xbb, 0x6b, 0x15, 0xc9, - 0xe0, 0x7d, 0xaa, 0xc8, 0x3e, 0x55, 0xf6, 0xa9, 0xe3, 0xed, 0x2d, 0x3c, 0x7b, 0x59, 0x9a, 0xfb, - 0xfe, 0xcf, 0x1f, 0xae, 0x2b, 0xa6, 0xe4, 0x94, 0x7f, 0x4e, 0x43, 0xb6, 0x29, 0x8b, 0x40, 0x79, - 0x48, 0x0c, 0x4b, 0x4b, 0x38, 0x36, 0xfa, 0x10, 0xb2, 0x2e, 0x09, 0x02, 0xdc, 0x25, 0x81, 0x96, - 0x10, 0xc1, 0x57, 0x2b, 0x51, 0x4b, 0x2a, 0x71, 0x4b, 0x2a, 0x35, 0x6f, 0x60, 0x0e, 0x51, 0xe8, - 0x16, 0x64, 0x02, 0x86, 0x59, 0x18, 0x68, 0x49, 0xb1, 0x9b, 0xc5, 0xc9, 0xdd, 0x8c, 0x73, 0xb5, - 0x04, 0xca, 0x94, 0x68, 0x64, 0x00, 0x7a, 0xe4, 0x78, 0xb8, 0x67, 0x31, 0xdc, 0xeb, 0x0d, 0x2c, - 0x9f, 0x04, 0x61, 0x8f, 0x69, 0xa9, 0x4d, 0x65, 0x2b, 0xb7, 0xbb, 0x3e, 0x19, 0xa3, 0xcd, 0x31, - 0xa6, 0x80, 0x98, 0xaa, 0xa0, 0x8d, 0x59, 0x50, 0x0d, 0x72, 0x41, 0xd8, 0x71, 0x1d, 0x66, 0x71, - 0xa5, 0x69, 0x69, 0x11, 0xa3, 0x70, 0xae, 0xee, 0x76, 0x2c, 0xc3, 0xbd, 0xd4, 0x93, 0xdf, 0x4b, - 0x8a, 0x09, 0x11, 0x89, 0x9b, 0xd1, 0x3d, 0x50, 0xe5, 0xfe, 0x5a, 0xc4, 0xb3, 0xa3, 0x38, 0x99, - 0x0b, 0xc6, 0xc9, 0x4b, 0xa6, 0xee, 0xd9, 0x22, 0x96, 0x01, 0x4b, 0x8c, 0x32, 0xdc, 0xb3, 0xa4, - 0x5d, 0x9b, 0x7f, 0x87, 0x2e, 0x2d, 0x0a, 0x6a, 0x2c, 0xa1, 0xfb, 0xb0, 0x72, 0x42, 0x99, 0xe3, - 0x75, 0xad, 0x80, 0x61, 0x5f, 0xae, 0x2f, 0x7b, 0xc1, 0xba, 0x96, 0x23, 0x6a, 0x8b, 0x33, 0x45, - 0x61, 0x9f, 0x80, 0x34, 0x8d, 0xd6, 0xb8, 0x70, 0xc1, 0x58, 0x4b, 0x11, 0x31, 0x5e, 0x62, 0x81, - 0xcb, 0x84, 0x61, 0x1b, 0x33, 0xac, 0x01, 0x17, 0xae, 0x39, 0x7c, 0x47, 0xab, 0x90, 0x66, 0x0e, - 0xeb, 0x11, 0x2d, 0x27, 0x1c, 0xd1, 0x0b, 0xd2, 0x60, 0x3e, 0x08, 0x5d, 0x17, 0xfb, 0x03, 0x6d, - 0x51, 0xd8, 0xe3, 0x57, 0xf4, 0x11, 0x64, 0xa3, 0x99, 0x20, 0xbe, 0xb6, 0x34, 0x63, 0x08, 0x86, - 0xc8, 0xf2, 0xb7, 0x0a, 0xe4, 0xc6, 0x35, 0xf0, 0x01, 0x2c, 0x0c, 0x48, 0x60, 0x1d, 0x89, 0xb1, - 0x50, 0xce, 0xcd, 0xa8, 0xe1, 0x31, 0x33, 0x3b, 0x20, 0xc1, 0x3e, 0xf7, 0xa3, 0x9b, 0xb0, 0x84, - 0x3b, 0x01, 0xc3, 0x8e, 0x27, 0x09, 0x89, 0xa9, 0x84, 0x45, 0x09, 0x8a, 0x48, 0xff, 0x87, 0xac, - 0x47, 0x25, 0x3e, 0x39, 0x15, 0x3f, 0xef, 0x51, 0x01, 0x2d, 0xff, 0xa8, 0x40, 0x8a, 0x1f, 0x22, - 0xb3, 0x8f, 0x80, 0x0a, 0xa4, 0x4f, 0x28, 0x23, 0xb3, 0xc7, 0x3f, 0x82, 0xa1, 0xdb, 0x30, 0x1f, - 0x9d, 0x48, 0x81, 0x96, 0x12, 0xaa, 0x2a, 0x4f, 0x8e, 0xca, 0xf9, 0x03, 0xcf, 0x8c, 0x29, 0x67, - 0xda, 0x96, 0x3e, 0xdb, 0xb6, 0x7b, 0xa9, 0x6c, 0x52, 0x4d, 0x95, 0x7f, 0x53, 0x60, 0x49, 0x8a, - 0xaf, 0x89, 0x7d, 0xec, 0x06, 0xe8, 0x21, 0xe4, 0x5c, 0xc7, 0x1b, 0x6a, 0x59, 0x99, 0xa5, 0xe5, - 0x0d, 0xae, 0xe5, 0x37, 0x2f, 0x4b, 0xff, 0x19, 0x63, 0xdd, 0xa0, 0xae, 0xc3, 0x88, 0xdb, 0x67, - 0x03, 0x13, 0x5c, 0xc7, 0x8b, 0xd5, 0xed, 0x02, 0x72, 0xf1, 0xe3, 0x18, 0x64, 0xf5, 0x89, 0xef, - 0x50, 0x5b, 0xec, 0x04, 0xcf, 0x30, 0x29, 0xc9, 0xba, 0xbc, 0x09, 0xf6, 0xfe, 0xf7, 0xe6, 0x65, - 0xe9, 0xea, 0x79, 0xe2, 0x28, 0xc9, 0x37, 0x5c, 0xb1, 0xaa, 0x8b, 0x1f, 0xc7, 0x2b, 0x11, 0xfe, - 0x72, 0x1b, 0x16, 0x0f, 0x85, 0x8a, 0xe5, 0xca, 0xea, 0x20, 0x55, 0x1d, 0x67, 0x56, 0x66, 0x65, - 0x4e, 0x89, 0xc8, 0x8b, 0x11, 0x4b, 0x46, 0xfd, 0x2b, 0x21, 0x85, 0x28, 0xa3, 0x5e, 0x83, 0xcc, - 0xd7, 0x21, 0xf5, 0x43, 0x77, 0x8a, 0x0a, 0xc5, 0x4d, 0x11, 0x79, 0xd1, 0x0d, 0x58, 0x60, 0xc7, - 0x3e, 0x09, 0x8e, 0x69, 0xcf, 0x7e, 0xcb, 0xa5, 0x32, 0x02, 0x20, 0x13, 0x36, 0x8e, 0xa8, 0x17, - 0x30, 0x87, 0x85, 0xbc, 0x12, 0x0b, 0xbb, 0xc4, 0xb3, 0x5d, 0xe2, 0x31, 0x4b, 0x26, 0x4b, 0x4e, - 0x8d, 0xb0, 0x3e, 0x4e, 0xaa, 0xc5, 0x9c, 0xcf, 0xa2, 0x0a, 0xbe, 0x80, 0xcd, 0xb7, 0xc4, 0x1c, - 0x15, 0x96, 0x9a, 0x1a, 0xb6, 0x38, 0x35, 0x6c, 0x7b, 0x58, 0xed, 0x36, 0x40, 0x0f, 0x9f, 0xc6, - 0xa5, 0xa5, 0xa7, 0x2f, 0xae, 0x87, 0x4f, 0x65, 0x21, 0x37, 0x61, 0x89, 0xc3, 0x47, 0x59, 0x33, - 0x53, 0x19, 0x8b, 0x3d, 0x7c, 0x3a, 0xcc, 0x51, 0xfe, 0x29, 0x03, 0x19, 0xb9, 0xe5, 0xfa, 0x3b, - 0x4a, 0x74, 0xec, 0xb8, 0x1d, 0x97, 0xe3, 0xa7, 0xef, 0x27, 0xc7, 0xd4, 0x74, 0xb9, 0x9d, 0x97, - 0x57, 0xf2, 0x3d, 0xe4, 0x35, 0x26, 0xa7, 0xd4, 0xc5, 0xe5, 0x94, 0x9e, 0x25, 0x27, 0x03, 0xd6, - 0xf8, 0x8e, 0x39, 0x9e, 0xc3, 0x9c, 0xd1, 0x45, 0x65, 0x89, 0x3a, 0xb4, 0xf9, 0xa9, 0xec, 0xcb, - 0xae, 0xe3, 0x19, 0x11, 0x5e, 0xae, 0xd3, 0xe4, 0x68, 0xb4, 0x05, 0x6a, 0x27, 0xf4, 0x3d, 0x8b, - 0x9f, 0x4f, 0x71, 0xc7, 0xf9, 0x31, 0x9e, 0x35, 0xf3, 0xdc, 0xce, 0x8f, 0x21, 0xd9, 0xe6, 0x1a, - 0x6c, 0x08, 0xe4, 0xf0, 0x44, 0x1c, 0xee, 0xb4, 0x4f, 0x38, 0x5b, 0xcb, 0x0b, 0x5a, 0x81, 0x83, - 0xe2, 0x8f, 0x86, 0x78, 0x4b, 0x23, 0x04, 0xfa, 0x18, 0x56, 0xc6, 0x3a, 0x2d, 0xeb, 0x5d, 0x9e, - 0x5a, 0xef, 0xf2, 0xa8, 0xb3, 0x51, 0xa1, 0x33, 0x47, 0x48, 0xfd, 0x77, 0x46, 0x68, 0xe5, 0x1f, - 0x18, 0x21, 0xf4, 0xce, 0x23, 0x74, 0x69, 0xf6, 0x08, 0x5d, 0xff, 0x0a, 0x60, 0xec, 0xb3, 0x78, - 0x1d, 0xae, 0x1c, 0x36, 0xda, 0xba, 0xd5, 0x68, 0xb6, 0x8d, 0xc6, 0x81, 0xf5, 0xe0, 0xa0, 0xd5, - 0xd4, 0xf7, 0x8d, 0x3b, 0x86, 0x5e, 0x57, 0xe7, 0xd0, 0x25, 0x58, 0x1e, 0x77, 0x3e, 0xd4, 0x5b, - 0xaa, 0x82, 0xae, 0xc0, 0xa5, 0x71, 0x63, 0x6d, 0xaf, 0xd5, 0xae, 0x19, 0x07, 0x6a, 0x02, 0x21, - 0xc8, 0x8f, 0x3b, 0x0e, 0x1a, 0x6a, 0xf2, 0xfa, 0x2f, 0x0a, 0xe4, 0xcf, 0x7e, 0x0a, 0xa2, 0x12, - 0xac, 0x37, 0xcd, 0x46, 0xb3, 0xd1, 0xaa, 0xdd, 0xb7, 0x5a, 0xed, 0x5a, 0xfb, 0x41, 0x6b, 0x22, - 0x6b, 0x19, 0x8a, 0x93, 0x80, 0xba, 0xde, 0x6c, 0xb4, 0x8c, 0xb6, 0xd5, 0xd4, 0x4d, 0xa3, 0x51, - 0x57, 0x15, 0xf4, 0x5f, 0xd8, 0x98, 0xc4, 0x1c, 0x36, 0xda, 0xc6, 0xc1, 0xdd, 0x18, 0x92, 0x40, - 0x05, 0xb8, 0x3c, 0x09, 0x69, 0xd6, 0x5a, 0x2d, 0xbd, 0xae, 0x26, 0xd1, 0x55, 0xd0, 0x26, 0x7d, - 0xa6, 0x7e, 0x4f, 0xdf, 0x6f, 0xeb, 0x75, 0x35, 0x35, 0x8d, 0x79, 0xa7, 0x66, 0xdc, 0xd7, 0xeb, - 0x6a, 0x7a, 0xef, 0xee, 0xb3, 0x57, 0x45, 0xe5, 0xf9, 0xab, 0xa2, 0xf2, 0xc7, 0xab, 0xa2, 0xf2, - 0xe4, 0x75, 0x71, 0xee, 0xf9, 0xeb, 0xe2, 0xdc, 0xaf, 0xaf, 0x8b, 0x73, 0x5f, 0x6e, 0x77, 0x1d, - 0x76, 0x1c, 0x76, 0x2a, 0x47, 0xd4, 0xad, 0xca, 0xdb, 0x79, 0xfb, 0x38, 0xec, 0xc4, 0xcf, 0xd5, - 0xc7, 0xe2, 0x97, 0x17, 0x1b, 0xf4, 0x49, 0xc0, 0x7f, 0x55, 0x65, 0xc4, 0x49, 0x70, 0xf3, 0xef, - 0x00, 0x00, 0x00, 0xff, 0xff, 0xbc, 0x09, 0x4c, 0x70, 0x98, 0x0d, 0x00, 0x00, + // 1447 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x57, 0xbf, 0x6f, 0xdb, 0x46, + 0x14, 0x36, 0xf5, 0xcb, 0xf2, 0xb3, 0x2d, 0xd3, 0x67, 0x27, 0xa1, 0xed, 0x58, 0x76, 0x85, 0x22, + 0x70, 0xd3, 0x58, 0xaa, 0x9d, 0x22, 0x43, 0x91, 0x45, 0xb6, 0x98, 0x54, 0x41, 0x6a, 0x29, 0x94, + 0xe2, 0x36, 0x1d, 0x4a, 0x9c, 0xc4, 0x8b, 0x4c, 0x44, 0xe4, 0x29, 0xe4, 0xd1, 0xb6, 0xfe, 0x8b, + 0x4c, 0x45, 0xd1, 0xa9, 0x63, 0xc7, 0x0e, 0x01, 0xfa, 0x07, 0x14, 0x05, 0x32, 0x15, 0x41, 0xa6, + 0x76, 0x49, 0x8b, 0x64, 0x28, 0x90, 0xbd, 0x7b, 0x71, 0xc7, 0xa3, 0x7e, 0x59, 0x81, 0x9c, 0xa0, + 0x5d, 0x6c, 0xf2, 0xde, 0xf7, 0xbd, 0xf7, 0xee, 0xbd, 0xef, 0xdd, 0x89, 0xa0, 0x61, 0x46, 0x1d, + 0xea, 0x92, 0x42, 0x8b, 0x1e, 0x17, 0x8e, 0x77, 0xf8, 0xbf, 0x7c, 0xc7, 0xa3, 0x8c, 0xa2, 0x8c, + 0xb4, 0xe4, 0xf9, 0xd2, 0xf1, 0xce, 0x6a, 0xb6, 0x49, 0x7d, 0x87, 0xfa, 0x85, 0x06, 0xf6, 0x49, + 0xe1, 0x78, 0xa7, 0x41, 0x18, 0xde, 0x29, 0x34, 0xa9, 0xed, 0x86, 0xf8, 0xd5, 0xe5, 0x16, 0x6d, + 0x51, 0xf1, 0x58, 0xe0, 0x4f, 0x72, 0x75, 0xa3, 0x45, 0x69, 0xab, 0x4d, 0x0a, 0xe2, 0xad, 0x11, + 0x3c, 0x2c, 0x30, 0xdb, 0x21, 0x3e, 0xc3, 0x4e, 0x47, 0x02, 0x56, 0x46, 0x01, 0xd8, 0xed, 0x4a, + 0x53, 0x76, 0xd4, 0x64, 0x05, 0x1e, 0x66, 0x36, 0x8d, 0x22, 0xae, 0x84, 0x19, 0x99, 0x61, 0xd0, + 0xf0, 0x45, 0x9a, 0x16, 0xb1, 0x63, 0xbb, 0xb4, 0x20, 0xfe, 0x86, 0x4b, 0xb9, 0x0e, 0xa0, 0x2f, + 0x89, 0xdd, 0x3a, 0x62, 0xc4, 0x3a, 0xa4, 0x8c, 0x54, 0x3a, 0xdc, 0x13, 0xda, 0x85, 0x14, 0x15, + 0x4f, 0x9a, 0xb2, 0xa9, 0x6c, 0x65, 0x76, 0x57, 0xf3, 0xc3, 0xdb, 0xce, 0xf7, 0xb1, 0x86, 0x44, + 0xa2, 0x2b, 0x90, 0x3a, 0x11, 0x9e, 0xb4, 0xd8, 0xa6, 0xb2, 0x35, 0xb3, 0x97, 0x79, 0xf1, 0x74, + 0x1b, 0x64, 0xf8, 0x12, 0x69, 0x1a, 0xd2, 0x9a, 0xfb, 0x41, 0x81, 0xe9, 0x12, 0xe9, 0x50, 0xdf, + 0x66, 0x68, 0x03, 0x66, 0x3b, 0x1e, 0xed, 0x50, 0x1f, 0xb7, 0x4d, 0xdb, 0x12, 0xc1, 0x12, 0x06, + 0x44, 0x4b, 0x65, 0x0b, 0xdd, 0x80, 0x19, 0x2b, 0xc4, 0x52, 0x4f, 0xfa, 0xd5, 0x5e, 0x3c, 0xdd, + 0x5e, 0x96, 0x7e, 0x8b, 0x96, 0xe5, 0x11, 0xdf, 0xaf, 0x31, 0xcf, 0x76, 0x5b, 0x46, 0x1f, 0x8a, + 0x6e, 0x42, 0x0a, 0x3b, 0x34, 0x70, 0x99, 0x16, 0xdf, 0x8c, 0x6f, 0xcd, 0xee, 0xae, 0xe4, 0x25, + 0x83, 0xf7, 0x29, 0x2f, 0xfb, 0x94, 0xdf, 0xa7, 0xb6, 0xbb, 0x37, 0xf3, 0xec, 0xe5, 0xc6, 0xd4, + 0x8f, 0x7f, 0xff, 0x74, 0x55, 0x31, 0x24, 0x27, 0xf7, 0x4b, 0x12, 0xd2, 0x55, 0x99, 0x04, 0xca, + 0x40, 0xac, 0x97, 0x5a, 0xcc, 0xb6, 0xd0, 0x27, 0x90, 0x76, 0x88, 0xef, 0xe3, 0x16, 0xf1, 0xb5, + 0x98, 0x70, 0xbe, 0x9c, 0x0f, 0x5b, 0x92, 0x8f, 0x5a, 0x92, 0x2f, 0xba, 0x5d, 0xa3, 0x87, 0x42, + 0x37, 0x20, 0xe5, 0x33, 0xcc, 0x02, 0x5f, 0x8b, 0x8b, 0x6a, 0x66, 0x47, 0xab, 0x19, 0xc5, 0xaa, + 0x09, 0x94, 0x21, 0xd1, 0xa8, 0x0c, 0xe8, 0xa1, 0xed, 0xe2, 0xb6, 0xc9, 0x70, 0xbb, 0xdd, 0x35, + 0x3d, 0xe2, 0x07, 0x6d, 0xa6, 0x25, 0x36, 0x95, 0xad, 0xd9, 0xdd, 0xb5, 0x51, 0x1f, 0x75, 0x8e, + 0x31, 0x04, 0xc4, 0x50, 0x05, 0x6d, 0x60, 0x05, 0x15, 0x61, 0xd6, 0x0f, 0x1a, 0x8e, 0xcd, 0x4c, + 0xae, 0x34, 0x2d, 0x29, 0x7c, 0xac, 0x9e, 0xc9, 0xbb, 0x1e, 0xc9, 0x70, 0x2f, 0xf1, 0xe4, 0xcf, + 0x0d, 0xc5, 0x80, 0x90, 0xc4, 0x97, 0xd1, 0x1d, 0x50, 0x65, 0x7d, 0x4d, 0xe2, 0x5a, 0xa1, 0x9f, + 0xd4, 0x39, 0xfd, 0x64, 0x24, 0x53, 0x77, 0x2d, 0xe1, 0xab, 0x0c, 0xf3, 0x8c, 0x32, 0xdc, 0x36, + 0xe5, 0xba, 0x36, 0xfd, 0x0e, 0x5d, 0x9a, 0x13, 0xd4, 0x48, 0x42, 0x77, 0x61, 0xf1, 0x98, 0x32, + 0xdb, 0x6d, 0x99, 0x3e, 0xc3, 0x9e, 0xdc, 0x5f, 0xfa, 0x9c, 0x79, 0x2d, 0x84, 0xd4, 0x1a, 0x67, + 0x8a, 0xc4, 0x3e, 0x07, 0xb9, 0xd4, 0xdf, 0xe3, 0xcc, 0x39, 0x7d, 0xcd, 0x87, 0xc4, 0x68, 0x8b, + 0xab, 0x5c, 0x26, 0x0c, 0x5b, 0x98, 0x61, 0x0d, 0xb8, 0x70, 0x8d, 0xde, 0x3b, 0x5a, 0x86, 0x24, + 0xb3, 0x59, 0x9b, 0x68, 0xb3, 0xc2, 0x10, 0xbe, 0x20, 0x0d, 0xa6, 0xfd, 0xc0, 0x71, 0xb0, 0xd7, + 0xd5, 0xe6, 0xc4, 0x7a, 0xf4, 0x8a, 0x3e, 0x85, 0x74, 0x38, 0x13, 0xc4, 0xd3, 0xe6, 0x27, 0x0c, + 0x41, 0x0f, 0x99, 0xfb, 0x5e, 0x81, 0xd9, 0x41, 0x0d, 0x7c, 0x0c, 0x33, 0x5d, 0xe2, 0x9b, 0x4d, + 0x31, 0x16, 0xca, 0x99, 0x19, 0x2d, 0xbb, 0xcc, 0x48, 0x77, 0x89, 0xbf, 0xcf, 0xed, 0xe8, 0x3a, + 0xcc, 0xe3, 0x86, 0xcf, 0xb0, 0xed, 0x4a, 0x42, 0x6c, 0x2c, 0x61, 0x4e, 0x82, 0x42, 0xd2, 0x47, + 0x90, 0x76, 0xa9, 0xc4, 0xc7, 0xc7, 0xe2, 0xa7, 0x5d, 0x2a, 0xa0, 0xb9, 0x9f, 0x15, 0x48, 0xf0, + 0x43, 0x64, 0xf2, 0x11, 0x90, 0x87, 0xe4, 0x31, 0x65, 0x64, 0xf2, 0xf8, 0x87, 0x30, 0x74, 0x13, + 0xa6, 0xc3, 0x13, 0xc9, 0xd7, 0x12, 0x42, 0x55, 0xb9, 0xd1, 0x51, 0x39, 0x7b, 0xe0, 0x19, 0x11, + 0x65, 0xa8, 0x6d, 0xc9, 0xe1, 0xb6, 0xdd, 0x49, 0xa4, 0xe3, 0x6a, 0x22, 0xf7, 0xab, 0x02, 0x17, + 0xee, 0x05, 0xd4, 0x0b, 0x9c, 0xfd, 0x23, 0xd2, 0x7c, 0x74, 0x2f, 0x20, 0x01, 0xd1, 0x5d, 0xe6, + 0x75, 0x51, 0x15, 0x96, 0x1e, 0x0b, 0x83, 0x10, 0x0e, 0x0d, 0xa4, 0x18, 0x95, 0x73, 0x0a, 0x68, + 0x31, 0x24, 0xd7, 0x43, 0xae, 0x10, 0xd1, 0x35, 0x40, 0xd2, 0x63, 0x93, 0xc7, 0x1a, 0x68, 0x45, + 0xc2, 0x50, 0x1f, 0xf7, 0x93, 0x08, 0xcb, 0x3f, 0x82, 0xf6, 0x4d, 0x8b, 0xba, 0x44, 0x34, 0x62, + 0x18, 0xed, 0x97, 0xa8, 0x4b, 0x72, 0x7f, 0x28, 0x30, 0x2f, 0x87, 0xa8, 0x8a, 0x3d, 0xec, 0xf8, + 0xe8, 0x01, 0xcc, 0x3a, 0xb6, 0xdb, 0x9b, 0x49, 0x65, 0xd2, 0x4c, 0xae, 0xf3, 0x99, 0x7c, 0xf3, + 0x72, 0xe3, 0xc2, 0x00, 0xeb, 0x1a, 0x75, 0x6c, 0x46, 0x9c, 0x0e, 0xeb, 0x1a, 0xe0, 0xd8, 0x6e, + 0x34, 0xa5, 0x0e, 0x20, 0x07, 0x9f, 0x46, 0x20, 0xb3, 0x43, 0x3c, 0x9b, 0x5a, 0x62, 0x23, 0x3c, + 0xc2, 0x68, 0x65, 0x4a, 0xf2, 0x46, 0xdb, 0xfb, 0xf0, 0xcd, 0xcb, 0x8d, 0xcb, 0x67, 0x89, 0xfd, + 0x20, 0xdf, 0xf1, 0xc2, 0xa9, 0x0e, 0x3e, 0x8d, 0x76, 0x22, 0xec, 0xb9, 0x3a, 0xcc, 0x1d, 0x8a, + 0x69, 0x94, 0x3b, 0x2b, 0x81, 0x9c, 0xce, 0x28, 0xb2, 0x32, 0x29, 0x72, 0x42, 0x78, 0x9e, 0x0b, + 0x59, 0xd2, 0xeb, 0x3f, 0x31, 0x39, 0x50, 0xd2, 0xeb, 0x15, 0x48, 0x85, 0x55, 0x1d, 0x33, 0x4d, + 0xe2, 0xc6, 0x0b, 0xad, 0xe8, 0x1a, 0xcc, 0xb0, 0x23, 0x8f, 0xf8, 0x47, 0xb4, 0x6d, 0xbd, 0xe5, + 0x72, 0xec, 0x03, 0x90, 0x01, 0xeb, 0x4d, 0xea, 0xfa, 0xcc, 0x66, 0x01, 0xcf, 0xc4, 0xc4, 0x0e, + 0x71, 0x2d, 0x87, 0xb8, 0xcc, 0x94, 0xc1, 0xe2, 0x63, 0x3d, 0xac, 0x0d, 0x92, 0x8a, 0x11, 0x27, + 0x14, 0x2a, 0xfa, 0x0a, 0x36, 0xdf, 0xe2, 0xb3, 0x9f, 0x58, 0x62, 0xac, 0xdb, 0xec, 0x58, 0xb7, + 0xf5, 0x5e, 0xb6, 0xdb, 0x00, 0x6d, 0x7c, 0x12, 0xa5, 0x96, 0x1c, 0xbf, 0xb9, 0x36, 0x3e, 0x91, + 0x89, 0x5c, 0x87, 0x79, 0x0e, 0xef, 0x47, 0x4d, 0x8d, 0x65, 0xcc, 0xb5, 0xf1, 0x49, 0x2f, 0x46, + 0xee, 0xdb, 0x34, 0xa4, 0x64, 0xc9, 0xf5, 0x77, 0x94, 0xe8, 0xc0, 0xb5, 0x31, 0x28, 0xc7, 0x2f, + 0xde, 0x4f, 0x8e, 0x89, 0xf1, 0x72, 0x3b, 0x2b, 0xaf, 0xf8, 0x7b, 0xc8, 0x6b, 0x40, 0x4e, 0x89, + 0xf3, 0xcb, 0x29, 0x39, 0x49, 0x4e, 0x65, 0x58, 0xe1, 0x15, 0xb3, 0x5d, 0x9b, 0xd9, 0xfd, 0x0b, + 0xd7, 0x14, 0x79, 0x68, 0xd3, 0x63, 0xd9, 0x17, 0x1d, 0xdb, 0x2d, 0x87, 0x78, 0xb9, 0x4f, 0x83, + 0xa3, 0xd1, 0x16, 0xa8, 0x8d, 0xc0, 0x73, 0x4d, 0x7e, 0xce, 0x46, 0x1d, 0xe7, 0xd7, 0x51, 0xda, + 0xc8, 0xf0, 0x75, 0x7e, 0x9c, 0xca, 0x36, 0x17, 0x61, 0x5d, 0x20, 0x7b, 0x27, 0x7b, 0xaf, 0xd2, + 0x1e, 0xe1, 0x6c, 0x2d, 0x23, 0x68, 0xab, 0x1c, 0x14, 0xfd, 0xf8, 0x89, 0x4a, 0x1a, 0x22, 0xd0, + 0x67, 0xb0, 0x38, 0xd0, 0x69, 0x99, 0xef, 0xc2, 0xd8, 0x7c, 0x17, 0xfa, 0x9d, 0x0d, 0x13, 0x9d, + 0x38, 0x42, 0xea, 0xff, 0x33, 0x42, 0x8b, 0xff, 0xc1, 0x08, 0xa1, 0x77, 0x1e, 0xa1, 0xa5, 0xc9, + 0x23, 0x84, 0x6e, 0x41, 0x66, 0xf8, 0x6a, 0xd2, 0x96, 0xcf, 0x27, 0xd1, 0xf9, 0xa1, 0x4b, 0x09, + 0x7d, 0x03, 0x6b, 0x7c, 0x70, 0x86, 0xd4, 0x6e, 0x92, 0x53, 0x46, 0x5c, 0x9f, 0x7f, 0x2d, 0x5c, + 0x38, 0x9f, 0x53, 0xcd, 0xc1, 0xa7, 0x87, 0x03, 0xd2, 0xd7, 0x23, 0x07, 0x6f, 0xb9, 0xf0, 0x2e, + 0x8e, 0xbf, 0xf0, 0xae, 0x3e, 0x02, 0x18, 0xf8, 0x68, 0x59, 0x83, 0x4b, 0x87, 0x95, 0xba, 0x6e, + 0x56, 0xaa, 0xf5, 0x72, 0xe5, 0xc0, 0xbc, 0x7f, 0x50, 0xab, 0xea, 0xfb, 0xe5, 0x5b, 0x65, 0xbd, + 0xa4, 0x4e, 0xa1, 0x25, 0x58, 0x18, 0x34, 0x3e, 0xd0, 0x6b, 0xaa, 0x82, 0x2e, 0xc1, 0xd2, 0xe0, + 0x62, 0x71, 0xaf, 0x56, 0x2f, 0x96, 0x0f, 0xd4, 0x18, 0x42, 0x90, 0x19, 0x34, 0x1c, 0x54, 0xd4, + 0xf8, 0xd5, 0xdf, 0x14, 0xc8, 0x0c, 0xff, 0x50, 0x47, 0x1b, 0xb0, 0x56, 0x35, 0x2a, 0xd5, 0x4a, + 0xad, 0x78, 0xd7, 0xac, 0xd5, 0x8b, 0xf5, 0xfb, 0xb5, 0x91, 0xa8, 0x39, 0xc8, 0x8e, 0x02, 0x4a, + 0x7a, 0xb5, 0x52, 0x2b, 0xd7, 0xcd, 0xaa, 0x6e, 0x94, 0x2b, 0x25, 0x55, 0x41, 0x1f, 0xc0, 0xfa, + 0x28, 0xe6, 0xb0, 0x52, 0x2f, 0x1f, 0xdc, 0x8e, 0x20, 0x31, 0xb4, 0x0a, 0x17, 0x47, 0x21, 0xd5, + 0x62, 0xad, 0xa6, 0x97, 0xd4, 0x38, 0xba, 0x0c, 0xda, 0xa8, 0xcd, 0xd0, 0xef, 0xe8, 0xfb, 0x75, + 0xbd, 0xa4, 0x26, 0xc6, 0x31, 0x6f, 0x15, 0xcb, 0x77, 0xf5, 0x92, 0x9a, 0xdc, 0xbb, 0xfd, 0xec, + 0x55, 0x56, 0x79, 0xfe, 0x2a, 0xab, 0xfc, 0xf5, 0x2a, 0xab, 0x3c, 0x79, 0x9d, 0x9d, 0x7a, 0xfe, + 0x3a, 0x3b, 0xf5, 0xfb, 0xeb, 0xec, 0xd4, 0xd7, 0xdb, 0x2d, 0x9b, 0x1d, 0x05, 0x8d, 0x7c, 0x93, + 0x3a, 0x05, 0xf9, 0xdb, 0x69, 0xfb, 0x28, 0x68, 0x44, 0xcf, 0x85, 0x53, 0xf1, 0x5d, 0xcc, 0xba, + 0x1d, 0xe2, 0xf3, 0x6f, 0xde, 0x94, 0xe8, 0xf3, 0xf5, 0x7f, 0x03, 0x00, 0x00, 0xff, 0xff, 0x62, + 0x28, 0x2b, 0x9a, 0x36, 0x0f, 0x00, 0x00, } func (m *WeightedVoteOption) Marshal() (dAtA []byte, err error) { @@ -1324,7 +1430,7 @@ func (m *Vote) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *DepositParams) Marshal() (dAtA []byte, err error) { +func (m *QuorumCheckQueueEntry) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) @@ -1334,24 +1440,67 @@ func (m *DepositParams) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *DepositParams) MarshalTo(dAtA []byte) (int, error) { +func (m *QuorumCheckQueueEntry) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } -func (m *DepositParams) MarshalToSizedBuffer(dAtA []byte) (int, error) { +func (m *QuorumCheckQueueEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l - if m.MaxDepositPeriod != nil { - n6, err6 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.MaxDepositPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.MaxDepositPeriod):]) + if m.QuorumChecksDone != 0 { + i = encodeVarintGov(dAtA, i, uint64(m.QuorumChecksDone)) + i-- + dAtA[i] = 0x18 + } + if m.QuorumCheckCount != 0 { + i = encodeVarintGov(dAtA, i, uint64(m.QuorumCheckCount)) + i-- + dAtA[i] = 0x10 + } + if m.QuorumTimeoutTime != nil { + n6, err6 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.QuorumTimeoutTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.QuorumTimeoutTime):]) if err6 != nil { return 0, err6 } i -= n6 i = encodeVarintGov(dAtA, i, uint64(n6)) i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *DepositParams) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *DepositParams) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *DepositParams) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.MaxDepositPeriod != nil { + n7, err7 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.MaxDepositPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.MaxDepositPeriod):]) + if err7 != nil { + return 0, err7 + } + i -= n7 + i = encodeVarintGov(dAtA, i, uint64(n7)) + i-- dAtA[i] = 0x12 } if len(m.MinDeposit) > 0 { @@ -1392,12 +1541,12 @@ func (m *VotingParams) MarshalToSizedBuffer(dAtA []byte) (int, error) { var l int _ = l if m.VotingPeriod != nil { - n7, err7 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.VotingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.VotingPeriod):]) - if err7 != nil { - return 0, err7 + n8, err8 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.VotingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.VotingPeriod):]) + if err8 != nil { + return 0, err8 } - i -= n7 - i = encodeVarintGov(dAtA, i, uint64(n7)) + i -= n8 + i = encodeVarintGov(dAtA, i, uint64(n8)) i-- dAtA[i] = 0xa } @@ -1489,6 +1638,37 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.QuorumCheckCount != 0 { + i = encodeVarintGov(dAtA, i, uint64(m.QuorumCheckCount)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xb0 + } + if m.MaxVotingPeriodExtension != nil { + n9, err9 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.MaxVotingPeriodExtension, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.MaxVotingPeriodExtension):]) + if err9 != nil { + return 0, err9 + } + i -= n9 + i = encodeVarintGov(dAtA, i, uint64(n9)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xaa + } + if m.QuorumTimeout != nil { + n10, err10 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.QuorumTimeout, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.QuorumTimeout):]) + if err10 != nil { + return 0, err10 + } + i -= n10 + i = encodeVarintGov(dAtA, i, uint64(n10)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xa2 + } if len(m.LawThreshold) > 0 { i -= len(m.LawThreshold) copy(dAtA[i:], m.LawThreshold) @@ -1574,22 +1754,22 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { dAtA[i] = 0x22 } if m.VotingPeriod != nil { - n8, err8 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.VotingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.VotingPeriod):]) - if err8 != nil { - return 0, err8 + n11, err11 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.VotingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.VotingPeriod):]) + if err11 != nil { + return 0, err11 } - i -= n8 - i = encodeVarintGov(dAtA, i, uint64(n8)) + i -= n11 + i = encodeVarintGov(dAtA, i, uint64(n11)) i-- dAtA[i] = 0x1a } if m.MaxDepositPeriod != nil { - n9, err9 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.MaxDepositPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.MaxDepositPeriod):]) - if err9 != nil { - return 0, err9 + n12, err12 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.MaxDepositPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.MaxDepositPeriod):]) + if err12 != nil { + return 0, err12 } - i -= n9 - i = encodeVarintGov(dAtA, i, uint64(n9)) + i -= n12 + i = encodeVarintGov(dAtA, i, uint64(n12)) i-- dAtA[i] = 0x12 } @@ -1769,6 +1949,25 @@ func (m *Vote) Size() (n int) { return n } +func (m *QuorumCheckQueueEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.QuorumTimeoutTime != nil { + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.QuorumTimeoutTime) + n += 1 + l + sovGov(uint64(l)) + } + if m.QuorumCheckCount != 0 { + n += 1 + sovGov(uint64(m.QuorumCheckCount)) + } + if m.QuorumChecksDone != 0 { + n += 1 + sovGov(uint64(m.QuorumChecksDone)) + } + return n +} + func (m *DepositParams) Size() (n int) { if m == nil { return 0 @@ -1892,6 +2091,17 @@ func (m *Params) Size() (n int) { if l > 0 { n += 2 + l + sovGov(uint64(l)) } + if m.QuorumTimeout != nil { + l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.QuorumTimeout) + n += 2 + l + sovGov(uint64(l)) + } + if m.MaxVotingPeriodExtension != nil { + l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.MaxVotingPeriodExtension) + n += 2 + l + sovGov(uint64(l)) + } + if m.QuorumCheckCount != 0 { + n += 2 + sovGov(uint64(m.QuorumCheckCount)) + } return n } @@ -2914,6 +3124,130 @@ func (m *Vote) Unmarshal(dAtA []byte) error { } return nil } +func (m *QuorumCheckQueueEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QuorumCheckQueueEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QuorumCheckQueueEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field QuorumTimeoutTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGov + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGov + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.QuorumTimeoutTime == nil { + m.QuorumTimeoutTime = new(time.Time) + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(m.QuorumTimeoutTime, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field QuorumCheckCount", wireType) + } + m.QuorumCheckCount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.QuorumCheckCount |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field QuorumChecksDone", wireType) + } + m.QuorumChecksDone = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.QuorumChecksDone |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipGov(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGov + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *DepositParams) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -3793,6 +4127,97 @@ func (m *Params) Unmarshal(dAtA []byte) error { } m.LawThreshold = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 20: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field QuorumTimeout", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGov + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGov + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.QuorumTimeout == nil { + m.QuorumTimeout = new(time.Duration) + } + if err := github_com_cosmos_gogoproto_types.StdDurationUnmarshal(m.QuorumTimeout, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 21: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxVotingPeriodExtension", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGov + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGov + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.MaxVotingPeriodExtension == nil { + m.MaxVotingPeriodExtension = new(time.Duration) + } + if err := github_com_cosmos_gogoproto_types.StdDurationUnmarshal(m.MaxVotingPeriodExtension, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 22: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field QuorumCheckCount", wireType) + } + m.QuorumCheckCount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.QuorumCheckCount |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipGov(dAtA[iNdEx:]) diff --git a/x/gov/types/v1/params.go b/x/gov/types/v1/params.go index 559c89367..dde05945c 100644 --- a/x/gov/types/v1/params.go +++ b/x/gov/types/v1/params.go @@ -27,9 +27,13 @@ var ( DefaultLawQuorum = sdk.NewDecWithPrec(4, 1) DefaultLawThreshold = sdk.NewDecWithPrec(9, 1) DefaultMinInitialDepositRatio = sdk.ZeroDec() - DefaultBurnProposalPrevote = false // set to false to replicate behavior of when this change was made (0.47) - DefaultBurnVoteQuorom = false // set to false to replicate behavior of when this change was made (0.47) - DefaultMinDepositRatio = sdk.MustNewDecFromStr("0.01") // NOTE: backport from v50 + DefaultBurnProposalPrevote = false // set to false to replicate behavior of when this change was made (0.47) + DefaultBurnVoteQuorom = false // set to false to replicate behavior of when this change was made (0.47) + DefaultMinDepositRatio = sdk.NewDecWithPrec(1, 2) // NOTE: backport from v50 + + DefaultQuorumTimeout time.Duration = DefaultVotingPeriod - (time.Hour * 24 * 1) // disabled by default (DefaultQuorumCheckCount must be set to a non-zero value to enable) + DefaultMaxVotingPeriodExtension time.Duration = DefaultVotingPeriod - DefaultQuorumTimeout // disabled by default (DefaultQuorumCheckCount must be set to a non-zero value to enable) + DefaultQuorumCheckCount uint64 = 0 // disabled by default (0 means no check) ) // Deprecated: NewDepositParams creates a new DepositParams object @@ -60,6 +64,7 @@ func NewParams( minDeposit sdk.Coins, maxDepositPeriod, votingPeriod time.Duration, quorum, threshold, constitutionAmendmentQuorum, constitutionAmendmentThreshold, lawQuorum, lawThreshold, minInitialDepositRatio string, burnProposalDeposit, burnVoteQuorum bool, minDepositRatio string, + quorumTimeout, maxVotingPeriodExtension time.Duration, quorumCheckCount uint64, ) Params { return Params{ MinDeposit: minDeposit, @@ -75,6 +80,9 @@ func NewParams( BurnProposalDepositPrevote: burnProposalDeposit, BurnVoteQuorum: burnVoteQuorum, MinDepositRatio: minDepositRatio, + QuorumTimeout: &quorumTimeout, + MaxVotingPeriodExtension: &maxVotingPeriodExtension, + QuorumCheckCount: quorumCheckCount, } } @@ -94,6 +102,9 @@ func DefaultParams() Params { DefaultBurnProposalPrevote, DefaultBurnVoteQuorom, DefaultMinDepositRatio.String(), + DefaultQuorumTimeout, + DefaultMaxVotingPeriodExtension, + DefaultQuorumCheckCount, ) } @@ -214,5 +225,25 @@ func (p Params) ValidateBasic() error { return fmt.Errorf("mininum initial deposit ratio of proposal is too large: %s", minInitialDepositRatio) } + if p.QuorumCheckCount > 0 { + // If quorum check is enabled, validate quorum check params + if p.QuorumTimeout == nil { + return fmt.Errorf("quorum timeout must not be nil: %d", p.QuorumTimeout) + } + if p.QuorumTimeout.Seconds() < 0 { + return fmt.Errorf("quorum timeout must be 0 or greater: %s", p.QuorumTimeout) + } + if p.QuorumTimeout.Nanoseconds() >= p.VotingPeriod.Nanoseconds() { + return fmt.Errorf("quorum timeout %s must be strictly less than the voting period %s", p.QuorumTimeout, p.VotingPeriod) + } + + if p.MaxVotingPeriodExtension == nil { + return fmt.Errorf("max voting period extension must not be nil: %d", p.MaxVotingPeriodExtension) + } + if p.MaxVotingPeriodExtension.Nanoseconds() < p.VotingPeriod.Nanoseconds()-p.QuorumTimeout.Nanoseconds() { + return fmt.Errorf("max voting period extension %s must be greater than or equal to the difference between the voting period %s and the quorum timeout %s", p.MaxVotingPeriodExtension, p.VotingPeriod, p.QuorumTimeout) + } + } + return nil } diff --git a/x/gov/types/v1/quorum_check.go b/x/gov/types/v1/quorum_check.go new file mode 100644 index 000000000..3722d2e6a --- /dev/null +++ b/x/gov/types/v1/quorum_check.go @@ -0,0 +1,13 @@ +package v1 + +import ( + time "time" +) + +func NewQuorumCheckQueueEntry(quorumTimeoutTime time.Time, quorumCheckCount uint64) QuorumCheckQueueEntry { + return QuorumCheckQueueEntry{ + QuorumTimeoutTime: &quorumTimeoutTime, + QuorumCheckCount: quorumCheckCount, + QuorumChecksDone: 0, + } +}