-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathagenda.go
288 lines (249 loc) · 9.68 KB
/
agenda.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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
// Copyright (c) 2017-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package main
import (
"context"
"fmt"
"html/template"
"log"
"regexp"
"strings"
"github.com/decred/dcrd/rpc/jsonrpc/types/v4"
"github.com/decred/dcrd/rpcclient/v8"
)
// Agenda contains all of the data representing an agenda for the html
// template programming.
type Agenda struct {
ID string
Status string
// Title is the main heading for the agenda card, hard-coded in main.go.
Title string
// Description is the short description from dcrd GetVoteInfo RPC response.
Description string
// LongDescription is a more verbose description, hard-coded in main.go.
LongDescription template.HTML
Mask uint16
VoteVersion uint32
QuorumThreshold int64
StartHeight int64
EndHeight int64
VoteChoices map[string]VoteChoice
VoteCounts map[string]int64
}
// VoteChoice contains the details of a vote choice from an agenda,
// Each agenda will have 3 choices - yes/no/maybe
type VoteChoice struct {
ID string
Description string
Bits uint16
}
var dcpRE = regexp.MustCompile(`(?i)DCP\-?(\d{4})`)
// Agenda status may be: started, defined, lockedin, failed, active
// VotingStarted always returns true after the vote for this agenda has started,
// regardless of whether the vote has passed or failed.
func (a *Agenda) VotingStarted() bool {
return a.IsStarted() || a.IsActive() || a.IsFailed() || a.IsLockedIn()
}
// IsActive indicates if the agenda is active
func (a *Agenda) IsActive() bool {
return a.Status == "active"
}
// IsStarted indicates if the agenda is started
func (a *Agenda) IsStarted() bool {
return a.Status == "started"
}
// IsDefined indicates if the agenda is defined
func (a *Agenda) IsDefined() bool {
return a.Status == "defined"
}
// IsLockedIn indicates if the agenda is lockedin
func (a *Agenda) IsLockedIn() bool {
return a.Status == "lockedin"
}
// IsFailed indicates if the agenda is failed
func (a *Agenda) IsFailed() bool {
return a.Status == "failed"
}
// QuorumMet indicates if the total number of yes/note
// votes has surpassed the quorum threshold
func (a *Agenda) QuorumMet() bool {
return a.TotalNonAbstainVotes() >= a.QuorumThreshold
}
// BlockLockedIn returns the height of the first block of this agenda's lock-in period. -1 if this agenda has not been locked-in.
func (a *Agenda) BlockLockedIn() int64 {
if a.IsLockedIn() || a.IsActive() {
return a.EndHeight + 1
}
return -1
}
// ActivationBlock returns the height of the first block with this agenda active. -1 if this agenda vote has not been locked-in.
func (a *Agenda) ActivationBlock() int64 {
if a.IsLockedIn() || a.IsActive() {
return a.BlockLockedIn() + int64(activeNetParams.RuleChangeActivationInterval)
}
return -1
}
// TotalNonAbstainVotes returns the sum of Yes votes and No votes
func (a *Agenda) TotalNonAbstainVotes() int64 {
return a.VoteCounts["yes"] + a.VoteCounts["no"]
}
// TotalVotes returns the total number of No, Yes and Abstain votes cast against this agenda
func (a *Agenda) TotalVotes() int64 {
return a.TotalNonAbstainVotes() + a.VoteCounts["abstain"]
}
// VotePercent returns the the number of yes/no/abstains votes, as a percentage of
// all votes cast against this agenda
func (a *Agenda) VotePercent(voteID string) float64 {
return 100 * float64(a.VoteCounts[voteID]) / float64(a.TotalVotes())
}
// VoteCountPercentage returns the number of yes/no/abstain votes cast against this agenda as
// a percentage of the theoretical maximum number of possible votes
func (a *Agenda) VoteCountPercentage(voteID string) float64 {
maxPossibleVotes := float64(activeNetParams.RuleChangeActivationInterval) * float64(activeNetParams.TicketsPerBlock)
return 100 * float64(a.VoteCounts[voteID]) / maxPossibleVotes
}
// ApprovalRating returns the number of yes votes cast against this agenda as
// a percentage of all non-abstain votes
func (a *Agenda) ApprovalRating() float64 {
approvalRating := float64(a.VoteCounts["yes"]) / float64(a.TotalNonAbstainVotes())
return 100 * approvalRating
}
// DescriptionWithDCPURL writes a new description with an link to any DCP that
// is detected in the text. It is written to a template.HTML type so the link
// is not escaped when the template is executed.
func (a *Agenda) DescriptionWithDCPURL() template.HTML {
subst := `<a href="https://github.com/decred/dcps/blob/master/dcp-${1}/dcp-${1}.mediawiki" target="_blank" rel="noopener noreferrer">${0}</a>`
// #nosec: this method will not auto-escape HTML. Verify data is well formed.
return template.HTML(dcpRE.ReplaceAllString(a.Description, subst))
}
// CountVotes uses the dcrd client to find all yes/no/abstain votes
// cast against this agenda. It will count the votes and store the
// totals inside the Agenda
func (a *Agenda) countVotes(ctx context.Context, dcrdClient *rpcclient.Client, votingStartHeight int64, votingEndHeight int64) error {
// Find the last block hash of this voting period
// Required to call GetStakeVersions
lastBlockHash, err := dcrdClient.GetBlockHash(ctx, votingEndHeight)
if err != nil {
return fmt.Errorf("GetBlockHash error: %v", err)
}
// Retrieve all votes for this voting period
stakeVersions, err := dcrdClient.GetStakeVersions(ctx, lastBlockHash.String(), int32(votingEndHeight-votingStartHeight)+1)
if err != nil {
return fmt.Errorf("GetStakeVersions error: %v", err)
}
// Collect all votes of the correct version
var votes []types.VersionBits
for _, sVer := range stakeVersions.StakeVersions {
for _, vote := range sVer.Votes {
if vote.Version == a.VoteVersion {
votes = append(votes, vote)
}
}
}
// Count the votes and store the total
for vID := range a.VoteChoices {
var matchingVotes int64
for _, vote := range votes {
if vote.Bits&a.Mask == a.VoteChoices[vID].Bits {
matchingVotes++
}
}
a.VoteCounts[vID] = matchingVotes
log.Printf("\t%s: %d", vID, matchingVotes)
}
return nil
}
// agendasFromJSON parses the response from GetVoteInfo, and
// uses the data to create a set of Agenda objects
func agendasFromJSON(getVoteInfo types.GetVoteInfoResult) []Agenda {
parsedAgendas := make([]Agenda, 0, len(getVoteInfo.Agendas))
for _, a := range getVoteInfo.Agendas {
voteChoices := make(map[string]VoteChoice)
for _, choice := range a.Choices {
vote := VoteChoice{
ID: choice.ID,
Description: choice.Description,
Bits: choice.Bits,
}
voteChoices[vote.ID] = vote
}
parsedAgendas = append(parsedAgendas, Agenda{
ID: a.ID,
Status: a.Status,
Title: agendaTitles[a.ID],
Description: a.Description,
// #nosec: this method will not auto-escape HTML. Verify data is well formed.
LongDescription: template.HTML(longAgendaDescriptions[a.ID]),
Mask: a.Mask,
VoteVersion: getVoteInfo.VoteVersion,
QuorumThreshold: int64(getVoteInfo.Quorum),
VoteChoices: voteChoices,
VoteCounts: make(map[string]int64),
})
}
return parsedAgendas
}
func agendasForVersions(ctx context.Context, dcrdClient *rpcclient.Client, currentHeight int64, svis StakeVersionIntervals) ([]Agenda, error) {
var allAgendas []Agenda
for version := svis.MinVoteVersion; version <= svis.MaxVoteVersion; version++ {
// Retrieve Agendas for this voting period
getVoteInfo, err := dcrdClient.GetVoteInfo(ctx, version)
if err != nil {
if strings.Contains(err.Error(), "unrecognized vote version") {
continue
}
return nil, err
}
agendas := agendasFromJSON(*getVoteInfo)
// Check if upgrade to this version has occurred yet
upgradeOccurred, upgradeSVI := svis.GetStakeVersionUpgradeSVI(version)
if !upgradeOccurred {
// Haven't upgraded to this stake version yet. Therefore
// we dont know when the voting start/end heights will be.
// Nothing more to do with these agendas
log.Printf("Upgrade to stake version %d has not happened", version)
allAgendas = append(allAgendas, agendas...)
break
}
upgradeHeight := upgradeSVI.EndHeight
log.Printf("Upgrade to version %d happened at height %d", version, upgradeHeight)
// Find the start of the next RCI after the threshold was met
nextRCIStartHeight := activeNetParams.StakeValidationHeight
for nextRCIStartHeight < upgradeHeight {
nextRCIStartHeight += int64(activeNetParams.RuleChangeActivationInterval)
}
// Next RCI height tells us the voting start/end heights, and we can add these to the agendas
votingStartHeight := nextRCIStartHeight
votingEndHeight := nextRCIStartHeight + int64(activeNetParams.RuleChangeActivationInterval) - 1
for i := range agendas {
agendas[i].StartHeight = votingStartHeight
agendas[i].EndHeight = votingEndHeight
log.Printf("Voting on %s will occur between %d-%d", agendas[i].ID, votingStartHeight, votingEndHeight)
}
if votingStartHeight > currentHeight {
// Voting hasnt started yet. So we cannot count the votes.
// Nothing more to do with these agendas
log.Printf("v%d voting is in the future so not counting votes yet", version)
allAgendas = append(allAgendas, agendas...)
break
}
// If agenda voting is currently in progress, only check votes up to the latest block
if votingEndHeight > currentHeight {
log.Printf("v%d voting is currently on-going", version)
votingEndHeight = currentHeight
}
// Count votes and store totals within Agenda struct
for _, agenda := range agendas {
log.Printf("Counting votes for %s between blocks %d-%d",
agenda.ID, votingStartHeight, votingEndHeight)
err = agenda.countVotes(ctx, dcrdClient, votingStartHeight, votingEndHeight)
if err != nil {
log.Printf("Error counting agenda %q votes: %v", agenda.ID, err)
return nil, err
}
}
allAgendas = append(allAgendas, agendas...)
}
return allAgendas, nil
}