/
deployments.go
294 lines (250 loc) · 8.17 KB
/
deployments.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
289
290
291
292
293
294
// Copyright (c) 2018-2021, The Decred developers
// See LICENSE for details.
// Package agendas manages the various deployment agendas that are directly
// voted upon with the vote bits in vote transactions.
package agendas
import (
"context"
"fmt"
"os"
"strings"
"github.com/EXCCoin/exccd/dcrjson/v4"
chainjson "github.com/EXCCoin/exccd/rpc/jsonrpc/types/v3"
"github.com/EXCCoin/exccdata/v8/db/dbtypes"
"github.com/EXCCoin/exccdata/v8/semver"
"github.com/asdine/storm/v3"
"github.com/asdine/storm/v3/q"
)
// AgendaDB represents the data for the stored DB.
type AgendaDB struct {
sdb *storm.DB
stakeVersions []uint32
deploySource DeploymentSource
}
// AgendaTagged has the same fields as chainjson.Agenda plus the VoteVersion
// field, but with the ID field marked as the primary key via the `storm:"id"`
// tag. Fields tagged for indexing by the DB are: StartTime, ExpireTime, Status,
// and QuorumProgress.
type AgendaTagged struct {
ID string `json:"id" storm:"id"`
Description string `json:"description"`
Mask uint16 `json:"mask"`
StartTime uint64 `json:"starttime" storm:"index"`
ExpireTime uint64 `json:"expiretime" storm:"index"`
Status dbtypes.AgendaStatusType `json:"status" storm:"index"`
QuorumProgress float64 `json:"quorumprogress" storm:"index"`
Choices []chainjson.Choice `json:"choices"`
VoteVersion uint32 `json:"voteversion"`
}
var (
// dbVersion is the current required version of the agendas.db.
dbVersion = semver.NewSemver(1, 0, 0)
)
// dbInfo defines the property that holds the db version.
const dbInfo = "_agendas.db_"
// DeploymentSource provides a cleaner way to track the rpcclient methods used
// in this package. It also allows usage of alternative implementations to
// satisfy the interface.
type DeploymentSource interface {
GetVoteInfo(ctx context.Context, version uint32) (*chainjson.GetVoteInfoResult, error)
}
// NewAgendasDB opens an existing database or create a new one using with the
// specified file name. It also checks the DB version, reindexes the DB if need
// be, and sets the required DB version.
func NewAgendasDB(client DeploymentSource, dbPath string) (*AgendaDB, error) {
if dbPath == "" {
return nil, fmt.Errorf("empty db Path found")
}
if client == DeploymentSource(nil) {
return nil, fmt.Errorf("invalid deployment source found")
}
_, err := os.Stat(dbPath)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
db, err := storm.Open(dbPath)
if err != nil {
return nil, err
}
// Check if the correct DB version has been set.
var version string
err = db.Get(dbInfo, "version", &version)
if err != nil && err != storm.ErrNotFound {
return nil, err
}
// Check if the versions match.
if version != dbVersion.String() {
// Attempt to delete AgendaTagged bucket.
if err = db.Drop(&AgendaTagged{}); err != nil {
// If error due bucket not found was returned ignore it.
if !strings.Contains(err.Error(), "not found") {
return nil, fmt.Errorf("delete bucket struct failed: %v", err)
}
}
// Set the required db version.
err = db.Set(dbInfo, "version", dbVersion.String())
if err != nil {
return nil, err
}
log.Infof("agendas.db version %v was set", dbVersion)
}
// Determine stake versions known by dcrd.
stakeVersions, err := listStakeVersions(client)
if err != nil {
return nil, err
}
adb := &AgendaDB{
sdb: db,
deploySource: client,
stakeVersions: stakeVersions,
}
return adb, nil
}
func listStakeVersions(client DeploymentSource) ([]uint32, error) {
agendaIDs := func(agendas []chainjson.Agenda) (ids []string) {
for i := range agendas {
ids = append(ids, agendas[i].ID)
}
return
}
var firstVer uint32
for {
voteInfo, err := client.GetVoteInfo(context.TODO(), firstVer)
if err == nil {
// That's the first version.
log.Debugf("Stake version %d: %v", firstVer, agendaIDs(voteInfo.Agendas))
// startTime = voteInfo.Agendas[0].StartTime
break
}
if jerr, ok := err.(*dcrjson.RPCError); ok &&
jerr.Code == dcrjson.ErrRPCInvalidParameter {
firstVer++
if firstVer == 10 {
log.Warnf("No stake versions found < 10. aborting scan")
return nil, nil
}
continue
}
return nil, err
}
versions := []uint32{firstVer}
for i := firstVer + 1; ; i++ {
voteInfo, err := client.GetVoteInfo(context.TODO(), i)
if err == nil {
log.Debugf("Stake version %d: %v", i, agendaIDs(voteInfo.Agendas))
versions = append(versions, i)
continue
}
if jerr, ok := err.(*dcrjson.RPCError); ok &&
jerr.Code == dcrjson.ErrRPCInvalidParameter {
break
}
// Something went wrong.
return nil, err
}
return versions, nil
}
// Close should be called when you are done with the AgendaDB to close the
// underlying database.
func (db *AgendaDB) Close() error {
if db == nil || db.sdb == nil {
return nil
}
return db.sdb.Close()
}
// loadAgenda retrieves an agenda corresponding to the specified unique agenda
// ID, or returns nil if it does not exist.
func (db *AgendaDB) loadAgenda(agendaID string) (*AgendaTagged, error) {
agenda := new(AgendaTagged)
if err := db.sdb.One("ID", agendaID, agenda); err != nil {
return nil, err
}
return agenda, nil
}
// agendasForVoteVersion fetches the agendas using the vote versions provided.
func agendasForVoteVersion(ver uint32, client DeploymentSource) ([]AgendaTagged, error) {
voteInfo, err := client.GetVoteInfo(context.TODO(), ver)
if err != nil {
return nil, err
}
// Set the agendas slice capacity.
agendas := make([]AgendaTagged, 0, len(voteInfo.Agendas))
for i := range voteInfo.Agendas {
v := &voteInfo.Agendas[i]
agendas = append(agendas, AgendaTagged{
ID: v.ID,
Description: v.Description,
Mask: v.Mask,
StartTime: v.StartTime,
ExpireTime: v.ExpireTime,
Status: dbtypes.AgendaStatusFromStr(v.Status),
QuorumProgress: v.QuorumProgress,
Choices: v.Choices,
VoteVersion: voteInfo.VoteVersion,
})
}
return agendas, nil
}
// updateDB updates the agenda data for all configured vote versions.
// chainjson.GetVoteInfoResult and chaincfg.ConsensusDeployment hold almost
// similar data contents but chaincfg.Vote does not contain the important vote
// status field that is found in chainjson.Agenda.
func (db *AgendaDB) updateDB() (int, error) {
agendas := make([]AgendaTagged, 0, len(db.stakeVersions))
for _, voteVersion := range db.stakeVersions {
taggedAgendas, err := agendasForVoteVersion(voteVersion, db.deploySource)
if err != nil || len(taggedAgendas) == 0 {
return -1, fmt.Errorf("vote version %d agendas retrieval failed: %v",
voteVersion, err)
}
agendas = append(agendas, taggedAgendas...)
}
for i := range agendas {
agenda := &agendas[i]
err := db.storeAgenda(agenda)
if err != nil {
return -1, fmt.Errorf("agenda '%s' was not saved: %v",
agenda.Description, err)
}
}
return len(agendas), nil
}
// storeAgenda saves an agenda in the database.
func (db *AgendaDB) storeAgenda(agenda *AgendaTagged) error {
return db.sdb.Save(agenda)
}
// UpdateAgendas updates agenda data for all configured vote versions.
func (db *AgendaDB) UpdateAgendas() error {
if db.stakeVersions == nil {
log.Debugf("skipping agendas update")
return nil
}
numRecords, err := db.updateDB()
if err != nil {
return fmt.Errorf("agendas.UpdateAgendas failed: %v", err)
}
log.Infof("%d agenda records (agendas) were updated", numRecords)
return nil
}
// AgendaInfo fetches an agenda's details given its agendaID.
func (db *AgendaDB) AgendaInfo(agendaID string) (*AgendaTagged, error) {
if db.stakeVersions == nil {
return nil, fmt.Errorf("No deployments")
}
agenda, err := db.loadAgenda(agendaID)
if err != nil {
return nil, err
}
return agenda, nil
}
// AllAgendas returns all agendas and their info in the db.
func (db *AgendaDB) AllAgendas() (agendas []*AgendaTagged, err error) {
if db.stakeVersions == nil {
return []*AgendaTagged{}, nil
}
err = db.sdb.Select(q.True()).OrderBy("VoteVersion", "ID").Reverse().Find(&agendas)
if err != nil {
log.Errorf("Failed to fetch data from Agendas DB: %v", err)
}
return
}