/
ballot.go
183 lines (157 loc) · 5.63 KB
/
ballot.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package keeper
import (
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/NibiruChain/collections"
"github.com/NibiruChain/nibiru/x/common/asset"
"github.com/NibiruChain/nibiru/x/common/omap"
"github.com/NibiruChain/nibiru/x/common/set"
"github.com/NibiruChain/nibiru/x/oracle/types"
)
// groupVotesByPair takes a collection of votes and groups them by their
// associated asset pair. This method only considers votes from active validators
// and disregards votes from validators that are not in the provided validator set.
//
// Note that any abstain votes (votes with a non-positive exchange rate) are
// assigned zero vote power. This function then returns a map where each
// asset pair is associated with its collection of ExchangeRateVotes.
func (k Keeper) groupVotesByPair(
ctx sdk.Context,
validatorPerformances types.ValidatorPerformances,
) (pairVotes map[asset.Pair]types.ExchangeRateVotes) {
pairVotes = map[asset.Pair]types.ExchangeRateVotes{}
for _, value := range k.Votes.Iterate(ctx, collections.Range[sdk.ValAddress]{}).KeyValues() {
voterAddr, aggregateVote := value.Key, value.Value
// skip votes from inactive validators
validatorPerformance, exists := validatorPerformances[aggregateVote.Voter]
if !exists {
continue
}
for _, tuple := range aggregateVote.ExchangeRateTuples {
power := validatorPerformance.Power
if !tuple.ExchangeRate.IsPositive() {
// Make the power of abstain vote zero
power = 0
}
pairVotes[tuple.Pair] = append(
pairVotes[tuple.Pair],
types.NewExchangeRateVote(
tuple.ExchangeRate,
tuple.Pair,
voterAddr,
power,
),
)
}
}
return
}
// clearVotesAndPrevotes clears all tallied prevotes and votes from the store
func (k Keeper) clearVotesAndPrevotes(ctx sdk.Context, votePeriod uint64) {
// Clear all aggregate prevotes
for _, prevote := range k.Prevotes.Iterate(ctx, collections.Range[sdk.ValAddress]{}).KeyValues() {
valAddr, aggregatePrevote := prevote.Key, prevote.Value
if ctx.BlockHeight() >= int64(aggregatePrevote.SubmitBlock+votePeriod) {
err := k.Prevotes.Delete(ctx, valAddr)
if err != nil {
k.Logger(ctx).Error("failed to delete prevote", "error", err)
}
}
}
// Clear all aggregate votes
for _, valAddr := range k.Votes.Iterate(ctx, collections.Range[sdk.ValAddress]{}).Keys() {
err := k.Votes.Delete(ctx, valAddr)
if err != nil {
k.Logger(ctx).Error("failed to delete vote", "error", err)
}
}
}
// isPassingVoteThreshold votes is passing the threshold amount of voting power
func isPassingVoteThreshold(
votes types.ExchangeRateVotes, thresholdVotingPower sdkmath.Int, minVoters uint64,
) bool {
totalPower := sdk.NewInt(votes.Power())
if totalPower.IsZero() {
return false
}
if totalPower.LT(thresholdVotingPower) {
return false
}
if votes.NumValidVoters() < minVoters {
return false
}
return true
}
// removeInvalidVotes removes the votes which have not reached the vote
// threshold or which are not part of the whitelisted pairs anymore: example
// when params change during a vote period but some votes were already made.
//
// ALERT: This function mutates the pairVotes map, it removes the votes for
// the pair which is not passing the threshold or which is not whitelisted
// anymore.
func (k Keeper) removeInvalidVotes(
ctx sdk.Context,
pairVotes map[asset.Pair]types.ExchangeRateVotes,
whitelistedPairs set.Set[asset.Pair],
) {
totalBondedPower := sdk.TokensToConsensusPower(
k.StakingKeeper.TotalBondedTokens(ctx), k.StakingKeeper.PowerReduction(ctx),
)
// Iterate through sorted keys for deterministic ordering.
orderedPairVotes := omap.OrderedMap_Pair[types.ExchangeRateVotes](pairVotes)
for pair := range orderedPairVotes.Range() {
// If pair is not whitelisted, or the votes for it has failed, then skip
// and remove it from pairBallotsMap for iteration efficiency
if !whitelistedPairs.Has(pair) {
delete(pairVotes, pair)
}
// If the votes is not passed, remove it from the whitelistedPairs set
// to prevent slashing validators who did valid vote.
if !isPassingVoteThreshold(
pairVotes[pair],
k.VoteThreshold(ctx).MulInt64(totalBondedPower).RoundInt(),
k.MinVoters(ctx),
) {
delete(whitelistedPairs, pair)
delete(pairVotes, pair)
continue
}
}
}
// Tally calculates the median and returns it. Sets the set of voters to be
// rewarded, i.e. voted within a reasonable spread from the weighted median to
// the store.
//
// ALERT: This function mutates validatorPerformances slice based on the votes
// made by the validators.
func Tally(
votes types.ExchangeRateVotes,
rewardBand sdk.Dec,
validatorPerformances types.ValidatorPerformances,
) sdk.Dec {
weightedMedian := votes.WeightedMedianWithAssertion()
standardDeviation := votes.StandardDeviation(weightedMedian)
rewardSpread := weightedMedian.Mul(rewardBand.QuoInt64(2))
if standardDeviation.GT(rewardSpread) {
rewardSpread = standardDeviation
}
for _, v := range votes {
// Filter votes winners & abstain voters
isInsideSpread := v.ExchangeRate.GTE(weightedMedian.Sub(rewardSpread)) &&
v.ExchangeRate.LTE(weightedMedian.Add(rewardSpread))
isAbstainVote := !v.ExchangeRate.IsPositive() // strictly less than zero, don't want to include zero
isMiss := !isInsideSpread && !isAbstainVote
validatorPerformance := validatorPerformances[v.Voter.String()]
switch {
case isInsideSpread:
validatorPerformance.RewardWeight += v.Power
validatorPerformance.WinCount++
case isMiss:
validatorPerformance.MissCount++
case isAbstainVote:
validatorPerformance.AbstainCount++
}
validatorPerformances[v.Voter.String()] = validatorPerformance
}
return weightedMedian
}