Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DONT MERGE] initial draft for vote extension for late quorum #202

Closed
wants to merge 40 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c0ad0c5
initial draft for vote extension for late quorum
giunatale Jun 28, 2023
399868d
fix compilation
tbruyelle Jul 5, 2023
8531f8e
use unique prefix for QuorumCheckQueuePrefix
tbruyelle Jul 5, 2023
7e84cd6
test: gov.UpdateParams with enabled quorum check
tbruyelle Jul 6, 2023
cfbfe21
test: gov.ActivateVotingPeriod with quorum check enabled
tbruyelle Jul 6, 2023
18c7b51
properly extend proposal voting end time
giunatale Jul 5, 2023
58294c8
fix params default values and validation
giunatale Jul 6, 2023
c739a87
Remove proposal from QuorumCheckQueue when deleted
giunatale Jul 6, 2023
4c841a2
rename variable to clearer name
giunatale Jul 7, 2023
9466733
test: gov.DeleteProposal with quorum checks
giunatale Jul 7, 2023
69474ee
add genesis code for quorum check queue
giunatale Jul 7, 2023
28ac1ab
add event for quorum check in gov.EndBlocker
giunatale Jul 7, 2023
0eed393
properly log event when quorum check is skipped
giunatale Jul 10, 2023
81db91a
replace counter in QuorumCheckQueue with struct
giunatale Jul 10, 2023
500fbc6
exit loop when proposal is found in quorumCheckQueue in DeleteProposal
giunatale Jul 11, 2023
d61cf4c
use quorumCheckEntry.QuorumCheckCount instead of params.QuorumCheckCount
giunatale Jul 11, 2023
2d21153
fix check when quorum is met that trigger vote period extension
giunatale Jul 11, 2023
72744f8
remove unnecessary for loop
giunatale Jul 12, 2023
206f8e0
move and refactor insertion in quorum check queue in InitGenesis
giunatale Jul 12, 2023
53451ba
only add to quorum check queue if quorum not met in InitGenesis
giunatale Jul 13, 2023
9f604c8
fix ActiveProposalsQueue not being updated on voting period extension
tbruyelle Jul 13, 2023
4cc8a4b
fix missing err check
tbruyelle Jul 13, 2023
6bf665f
wip: TestInitGenesis
tbruyelle Jul 18, 2023
17cfd20
fix(InitGenesis): check if quorum check is enabled
tbruyelle Jul 19, 2023
a2670ae
add quorumCheckQueue testing in TestExpeditedProposals
giunatale Jul 20, 2023
eb96840
change quorumCheckTimestamps to simpler quorumChecksDone counter
giunatale Jul 20, 2023
69bd381
fix HasReachedQuorum compilation error
giunatale Jul 20, 2023
ae7afa4
add code for migrations
giunatale Jul 21, 2023
4b1b864
refine and test new params generation in simulation
giunatale Jul 25, 2023
54ed757
lint pass
giunatale Jul 25, 2023
32672f1
edit proto comments
giunatale Jul 28, 2023
96a16b4
add TestHasReachedQuorum + some refactor with Tally
tbruyelle Jul 28, 2023
9e70a33
add approx but faster validator-only first check in HasReachedQuorum
giunatale Jul 31, 2023
92abfae
fix HasReachedQuorum approx quorum computation
giunatale Aug 1, 2023
b0cc996
use Has instead of Get, as vote content is not needed
giunatale Aug 1, 2023
d646c20
fix TestHasReachedQuorum after validator votes optimization
tbruyelle Aug 1, 2023
3c3d45e
add test with delegator votes for HasReachedQuorum
giunatale Aug 1, 2023
17ca569
refac(gov): merge common code between Tally and HasReachedQuorum
tbruyelle Aug 2, 2023
c5c48eb
test(gov): integration test for HasReachedQuorum
tbruyelle Aug 2, 2023
9833e67
refactor `tallyVotes`
giunatale Aug 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,195 changes: 1,034 additions & 161 deletions api/cosmos/gov/v1/gov.pulsar.go

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions proto/cosmos/gov/v1/gov.proto
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,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 {
option deprecated = true;
Expand Down Expand Up @@ -259,4 +274,16 @@ message Params {

// burn deposits if quorum with vote type no_veto is met
bool burn_vote_veto = 15;

// 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 = 16 [(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 = 17 [(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 = 18;
}
85 changes: 85 additions & 0 deletions tests/integration/gov/keeper/tally_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,3 +517,88 @@ func TestTallyValidatorMultipleDelegations(t *testing.T) {

assert.Assert(t, tallyResults.Equals(expectedTallyResult))
}

func TestHasReachQuorum(t *testing.T) {
type suite struct {
*fixture
proposal v1.Proposal
valAddrs []sdk.AccAddress
accAddrs []sdk.AccAddress
}
tests := []struct {
name string
setup func(suite)
expectedQuorum bool
}{
{
name: "no vote",
setup: func(suite) {},
expectedQuorum: false,
},
{
name: "quorum not reached",
setup: func(s suite) {
assert.NilError(t, s.govKeeper.AddVote(s.ctx, s.proposal.Id, s.valAddrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), ""))

assert.NilError(t, s.govKeeper.AddVote(s.ctx, s.proposal.Id, s.accAddrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), ""))
},
expectedQuorum: false,
},
{
name: "quorum reached with only validator vote",
setup: func(s suite) {
assert.NilError(t, s.govKeeper.AddVote(s.ctx, s.proposal.Id, s.valAddrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), ""))
assert.NilError(t, s.govKeeper.AddVote(s.ctx, s.proposal.Id, s.valAddrs[1], v1.NewNonSplitVoteOption(v1.OptionNo), ""))
},
expectedQuorum: true,
},
{
name: "quorum reached with validator & delegator vote",
setup: func(s suite) {
assert.NilError(t, s.govKeeper.AddVote(s.ctx, s.proposal.Id, s.valAddrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), ""))
assert.NilError(t, s.govKeeper.AddVote(s.ctx, s.proposal.Id, s.accAddrs[0], v1.NewNonSplitVoteOption(v1.OptionNo), ""))
assert.NilError(t, s.govKeeper.AddVote(s.ctx, s.proposal.Id, s.accAddrs[1], v1.NewNonSplitVoteOption(v1.OptionAbstain), ""))
},
expectedQuorum: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// t.Parallel()
f := initFixture(t)
ctx := f.ctx
// Create 3 validators
valAccAddrs, valAddrs := createValidators(t, f, []int64{5, 5, 5})
// Create 3 delegators
delegation := math.NewInt(10000000)
accAddrs := simtestutil.AddTestAddrsIncremental(f.bankKeeper, f.stakingKeeper, f.ctx, 3, delegation)
for i, accAddr := range accAddrs {
val, err := f.stakingKeeper.GetValidator(ctx, valAddrs[i])
assert.NilError(t, err)
_, err = f.stakingKeeper.Delegate(ctx, accAddr, delegation, stakingtypes.Unbonded, val, true)
assert.NilError(t, err)
}
// Create and activate proposal
proposal, err := f.govKeeper.SubmitProposal(ctx, TestProposal, "", "test",
"description", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r"), false)
assert.NilError(t, err)
proposal.Status = v1.StatusVotingPeriod
err = f.govKeeper.SetProposal(ctx, proposal)
assert.NilError(t, err)
tt.setup(suite{
fixture: f,
proposal: proposal,
valAddrs: valAccAddrs,
accAddrs: accAddrs,
})

proposal, ok := f.govKeeper.Proposals.Get(ctx, proposal.Id)
assert.Assert(t, ok)

quorum, err := f.govKeeper.HasReachedQuorum(ctx, proposal)

assert.NilError(t, err)
assert.Assert(t, quorum == tt.expectedQuorum)
})
}
}
114 changes: 114 additions & 0 deletions x/gov/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,111 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) error {
return err
}

// fetch proposals that are due to be checked for quorum
rng = collections.NewPrefixUntilPairRange[time.Time, uint64](ctx.BlockTime())
err = keeper.QuorumCheckQueue.Walk(ctx, rng, func(key collections.Pair[time.Time, uint64], quorumCheckEntry v1.QuorumCheckQueueEntry) (bool, error) {
proposal, err := keeper.Proposals.Get(ctx, key.K2())
if err != nil {
return false, err
}

var tagValue, logMsg string

params, err := keeper.Params.Get(ctx)
if err != nil {
return false, err
}

// remove from queue
err = keeper.QuorumCheckQueue.Remove(ctx, key)
if err != nil {
return false, err
}

// check if proposal passed quorum
quorum, err := keeper.HasReachedQuorum(ctx, proposal)
if err != nil {
return false, err
}

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
err = keeper.ActiveProposalsQueue.Remove(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id))
if err != nil {
return false, err
}
proposal.VotingEndTime = &endTime
err = keeper.ActiveProposalsQueue.Set(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id), proposal.Id)
if err != nil {
return false, err
}
err = keeper.SetProposal(ctx, proposal)
if err != nil {
return false, err
}
}
}
} 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 := key.K1().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++
err = keeper.QuorumCheckQueue.Set(ctx, collections.Join(nextQuorumCheckTime, proposal.Id), quorumCheckEntry)
if err != nil {
return false, err
}

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.AttributeKeyProposalQuorumResult, tagValue),
sdk.NewAttribute(types.AttributeKeyProposalLog, logMsg),
),
)

return false, nil
})
if err != nil && !errors.Is(err, collections.ErrInvalidIterator) {
return err
}

// fetch active proposals whose voting periods have ended (are passed the block time)
rng = collections.NewPrefixUntilPairRange[time.Time, uint64](ctx.BlockTime())
err = keeper.ActiveProposalsQueue.Walk(ctx, rng, func(key collections.Pair[time.Time, uint64], _ uint64) (bool, error) {
Expand Down Expand Up @@ -175,6 +280,15 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) error {
endTime := proposal.VotingStartTime.Add(*params.VotingPeriod)
proposal.VotingEndTime = &endTime

if params.QuorumCheckCount > 0 {
// add proposal to quorum check queue
quorumTimeoutTime := proposal.VotingStartTime.Add(*params.QuorumTimeout)
err = keeper.QuorumCheckQueue.Set(ctx, collections.Join(quorumTimeoutTime, proposal.Id), v1.NewQuorumCheckQueueEntry(quorumTimeoutTime, params.QuorumCheckCount))
if err != nil {
return false, err
}
tbruyelle marked this conversation as resolved.
Show resolved Hide resolved
}

err = keeper.ActiveProposalsQueue.Set(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id), proposal.Id)
if err != nil {
return false, err
Expand Down
Loading