github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/ticketvote/ticketvote.go (about) 1 // Copyright (c) 2020-2021 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package ticketvote 6 7 import ( 8 "encoding/json" 9 "fmt" 10 "os" 11 "path/filepath" 12 "strconv" 13 14 "github.com/decred/dcrd/chaincfg/v3" 15 "github.com/decred/politeia/politeiad/api/v1/identity" 16 backend "github.com/decred/politeia/politeiad/backendv2" 17 "github.com/decred/politeia/politeiad/backendv2/tstorebe/plugins" 18 "github.com/decred/politeia/politeiad/plugins/dcrdata" 19 "github.com/decred/politeia/politeiad/plugins/ticketvote" 20 "github.com/pkg/errors" 21 ) 22 23 var ( 24 _ plugins.PluginClient = (*ticketVotePlugin)(nil) 25 ) 26 27 // ticketVotePlugin is the tstore backend implementation of the ticketvote 28 // plugin. The ticketvote plugin extends a record with dcr ticket voting 29 // functionality. 30 // 31 // ticketVotePlugin satisfies the plugins PluginClient interface. 32 type ticketVotePlugin struct { 33 backend backend.Backend 34 tstore plugins.TstoreClient 35 activeNetParams *chaincfg.Params 36 37 // dataDir is the ticket vote plugin data directory. The only data 38 // that is stored here is cached data that can be re-created at any 39 // time by walking the trillian trees. Ex, the vote summary once a 40 // record vote has ended. 41 dataDir string 42 43 // identity contains the full identity that the plugin uses to 44 // create receipts, i.e. signatures of user provided data that 45 // prove the backend received and processed a plugin command. 46 identity *identity.FullIdentity 47 48 // activeVotes is a memeory cache that contains data required to 49 // validate vote ballots in a time efficient manner. 50 activeVotes *activeVotes 51 52 // inv provides an API for managing the cached vote inventory. The 53 // data is cached in the tstore provided plugin cache. 54 inv *invClient 55 56 // summaries provides an API for interacting with the vote summaries 57 // cache. The data is saved to the tstore provided plugin cache. 58 summaries *summariesClient 59 60 // subs provides an API for interacting with the runoff vote submissions 61 // cache. The data is saved to the tstore provided plugin cache. 62 subs *subsClient 63 64 // Plugin settings 65 linkByPeriodMin int64 // In seconds 66 linkByPeriodMax int64 // In seconds 67 voteDurationMin uint32 // In blocks 68 voteDurationMax uint32 // In blocks 69 summariesPageSize uint32 70 inventoryPageSize uint32 71 timestampsPageSize uint32 72 } 73 74 // Setup performs any plugin setup that is required. 75 // 76 // This function satisfies the plugins PluginClient interface. 77 func (p *ticketVotePlugin) Setup() error { 78 log.Tracef("ticketvote Setup") 79 80 // Verify plugin dependencies 81 var dcrdataFound bool 82 for _, v := range p.backend.PluginInventory() { 83 if v.ID == dcrdata.PluginID { 84 dcrdataFound = true 85 } 86 } 87 if !dcrdataFound { 88 return errors.Errorf("%v plugin dependency not registered", 89 dcrdata.PluginID) 90 } 91 92 // Build the active votes cache 93 log.Infof("Building active votes cache") 94 95 var ( 96 // started is populated with the tokens of all records 97 // that have a vote status of VoteStatusStarted. 98 started = make([]string, 0, 256) 99 100 page uint32 = 1 101 ) 102 bestBlock, err := p.bestBlock() 103 if err != nil { 104 return err 105 } 106 for { 107 entries, err := p.inv.GetPageForStatus(bestBlock, 108 ticketvote.VoteStatusStarted, page) 109 if err != nil { 110 return err 111 } 112 if len(entries) == 0 { 113 // We've reached the end of the inventory 114 // for the VoteStatusStarted entries. 115 break 116 } 117 started = append(started, entryTokens(entries)...) 118 page++ 119 } 120 // Retrieve the data required to build the active 121 // votes cache for the records with ongoing votes. 122 for _, v := range started { 123 // Get the vote details 124 token, err := tokenDecode(v) 125 if err != nil { 126 return err 127 } 128 129 reply, err := p.backend.PluginRead(token, ticketvote.PluginID, 130 ticketvote.CmdDetails, "") 131 if err != nil { 132 return errors.Errorf("PluginRead %x %v %v: %v", token, 133 ticketvote.PluginID, ticketvote.CmdDetails, err) 134 } 135 var dr ticketvote.DetailsReply 136 err = json.Unmarshal([]byte(reply), &dr) 137 if err != nil { 138 return err 139 } 140 if dr.Vote == nil { 141 // Sanity check 142 return errors.Errorf("vote details not found "+ 143 "for record in started inventory %x", token) 144 } 145 146 // Add the record to the active votes cache 147 p.activeVotesAdd(*dr.Vote) 148 149 // Get the cast votes 150 reply, err = p.backend.PluginRead(token, ticketvote.PluginID, 151 ticketvote.CmdResults, "") 152 if err != nil { 153 return errors.Errorf("PluginRead %x %v %v: %v", token, 154 ticketvote.PluginID, ticketvote.CmdResults, err) 155 } 156 var rr ticketvote.ResultsReply 157 err = json.Unmarshal([]byte(reply), &rr) 158 if err != nil { 159 return err 160 } 161 162 // Add the cast votes to the cached active vote entry 163 for _, v := range rr.Votes { 164 p.activeVotes.AddCastVote(v.Token, v.Ticket, v.VoteBit) 165 } 166 } 167 168 return nil 169 } 170 171 // Cmd executes a plugin command. 172 // 173 // This function satisfies the plugins PluginClient interface. 174 func (p *ticketVotePlugin) Cmd(token []byte, cmd, payload string) (string, error) { 175 log.Tracef("ticketvote Cmd: %x %v %v", token, cmd, payload) 176 177 switch cmd { 178 case ticketvote.CmdAuthorize: 179 return p.cmdAuthorize(token, payload) 180 case ticketvote.CmdStart: 181 return p.cmdStart(token, payload) 182 case ticketvote.CmdCastBallot: 183 return p.cmdCastBallot(token, payload) 184 case ticketvote.CmdDetails: 185 return p.cmdDetails(token) 186 case ticketvote.CmdResults: 187 return p.cmdResults(token) 188 case ticketvote.CmdSummary: 189 return p.cmdSummary(token) 190 case ticketvote.CmdSubmissions: 191 return p.cmdSubmissions(token) 192 case ticketvote.CmdInventory: 193 return p.cmdInventory(payload) 194 case ticketvote.CmdTimestamps: 195 return p.cmdTimestamps(token, payload) 196 197 // Internal plugin commands 198 case cmdStartRunoffSubmission: 199 return p.cmdStartRunoffSubmission(token, payload) 200 case cmdRunoffDetails: 201 return p.cmdRunoffDetails(token) 202 } 203 204 return "", backend.ErrPluginCmdInvalid 205 } 206 207 // Hook executes a plugin hook. 208 // 209 // This function satisfies the plugins PluginClient interface. 210 func (p *ticketVotePlugin) Hook(h plugins.HookT, payload string) error { 211 log.Tracef("ticketvote Hook: %v", plugins.Hooks[h]) 212 213 switch h { 214 case plugins.HookTypeNewRecordPre: 215 return p.hookNewRecordPre(payload) 216 case plugins.HookTypeEditRecordPre: 217 return p.hookEditRecordPre(payload) 218 case plugins.HookTypeSetRecordStatusPre: 219 return p.hookSetRecordStatusPre(payload) 220 case plugins.HookTypeSetRecordStatusPost: 221 return p.hookSetRecordStatusPost(payload) 222 } 223 224 return nil 225 } 226 227 // Fsck performs a plugin filesystem check. The plugin is provided with the 228 // tokens for all records in the backend. 229 // 230 // This function satisfies the plugins PluginClient interface. 231 func (p *ticketVotePlugin) Fsck(tokens [][]byte) error { 232 log.Tracef("ticketvote Fsck") 233 234 return p.fsck(tokens) 235 } 236 237 // Settings returns the plugin's settings. 238 // 239 // This function satisfies the plugins PluginClient interface. 240 func (p *ticketVotePlugin) Settings() []backend.PluginSetting { 241 log.Tracef("ticketvote Settings") 242 243 return []backend.PluginSetting{ 244 { 245 Key: ticketvote.SettingKeyLinkByPeriodMin, 246 Value: strconv.FormatInt(p.linkByPeriodMin, 10), 247 }, 248 { 249 Key: ticketvote.SettingKeyLinkByPeriodMax, 250 Value: strconv.FormatInt(p.linkByPeriodMax, 10), 251 }, 252 { 253 Key: ticketvote.SettingKeyVoteDurationMin, 254 Value: strconv.FormatUint(uint64(p.voteDurationMin), 10), 255 }, 256 { 257 Key: ticketvote.SettingKeyVoteDurationMax, 258 Value: strconv.FormatUint(uint64(p.voteDurationMax), 10), 259 }, 260 { 261 Key: ticketvote.SettingKeySummariesPageSize, 262 Value: strconv.FormatUint(uint64(p.summariesPageSize), 10), 263 }, 264 { 265 Key: ticketvote.SettingKeyInventoryPageSize, 266 Value: strconv.FormatUint(uint64(p.inventoryPageSize), 10), 267 }, 268 { 269 Key: ticketvote.SettingKeyTimestampsPageSize, 270 Value: strconv.FormatUint(uint64(p.timestampsPageSize), 10), 271 }, 272 } 273 } 274 275 func New(backend backend.Backend, tstore plugins.TstoreClient, settings []backend.PluginSetting, dataDir string, id *identity.FullIdentity, activeNetParams *chaincfg.Params) (*ticketVotePlugin, error) { 276 // Plugin settings 277 var ( 278 linkByPeriodMin int64 279 linkByPeriodMax int64 280 voteDurationMin uint32 281 voteDurationMax uint32 282 summariesPageSize = ticketvote.SettingSummariesPageSize 283 inventoryPageSize = ticketvote.SettingInventoryPageSize 284 timestampsPageSize = ticketvote.SettingTimestampsPageSize 285 ) 286 287 // Set plugin settings to defaults. These will be overwritten if 288 // the setting was specified by the user. 289 switch activeNetParams.Name { 290 case chaincfg.MainNetParams().Name: 291 linkByPeriodMin = ticketvote.SettingMainNetLinkByPeriodMin 292 linkByPeriodMax = ticketvote.SettingMainNetLinkByPeriodMax 293 voteDurationMin = ticketvote.SettingMainNetVoteDurationMin 294 voteDurationMax = ticketvote.SettingMainNetVoteDurationMax 295 case chaincfg.TestNet3Params().Name: 296 linkByPeriodMin = ticketvote.SettingTestNetLinkByPeriodMin 297 linkByPeriodMax = ticketvote.SettingTestNetLinkByPeriodMax 298 voteDurationMin = ticketvote.SettingTestNetVoteDurationMin 299 voteDurationMax = ticketvote.SettingTestNetVoteDurationMax 300 case chaincfg.SimNetParams().Name: 301 // Use testnet defaults for simnet 302 linkByPeriodMin = ticketvote.SettingTestNetLinkByPeriodMin 303 linkByPeriodMax = ticketvote.SettingTestNetLinkByPeriodMax 304 voteDurationMin = ticketvote.SettingTestNetVoteDurationMin 305 voteDurationMax = ticketvote.SettingTestNetVoteDurationMax 306 default: 307 return nil, fmt.Errorf("unknown active net: %v", activeNetParams.Name) 308 } 309 310 // Override defaults with any passed in settings 311 for _, v := range settings { 312 switch v.Key { 313 case ticketvote.SettingKeyLinkByPeriodMin: 314 i, err := strconv.ParseInt(v.Value, 10, 64) 315 if err != nil { 316 return nil, fmt.Errorf("plugin setting '%v': ParseInt(%v): %v", 317 v.Key, v.Value, err) 318 } 319 linkByPeriodMin = i 320 log.Infof("Plugin setting updated: ticketvote %v %v", 321 ticketvote.SettingKeyLinkByPeriodMin, linkByPeriodMin) 322 323 case ticketvote.SettingKeyLinkByPeriodMax: 324 i, err := strconv.ParseInt(v.Value, 10, 64) 325 if err != nil { 326 return nil, fmt.Errorf("plugin setting '%v': ParseInt(%v): %v", 327 v.Key, v.Value, err) 328 } 329 linkByPeriodMax = i 330 log.Infof("Plugin setting updated: ticketvote %v %v", 331 ticketvote.SettingKeyLinkByPeriodMax, linkByPeriodMax) 332 333 case ticketvote.SettingKeyVoteDurationMin: 334 u, err := strconv.ParseUint(v.Value, 10, 64) 335 if err != nil { 336 return nil, fmt.Errorf("plugin setting '%v': ParseUint(%v): %v", 337 v.Key, v.Value, err) 338 } 339 voteDurationMin = uint32(u) 340 log.Infof("Plugin setting updated: ticketvote %v %v", 341 ticketvote.SettingKeyVoteDurationMin, voteDurationMin) 342 343 case ticketvote.SettingKeyVoteDurationMax: 344 u, err := strconv.ParseUint(v.Value, 10, 64) 345 if err != nil { 346 return nil, fmt.Errorf("plugin setting '%v': ParseUint(%v): %v", 347 v.Key, v.Value, err) 348 } 349 voteDurationMax = uint32(u) 350 log.Infof("Plugin setting updated: ticketvote %v %v", 351 ticketvote.SettingKeyVoteDurationMax, voteDurationMax) 352 353 case ticketvote.SettingKeySummariesPageSize: 354 u, err := strconv.ParseUint(v.Value, 10, 64) 355 if err != nil { 356 return nil, fmt.Errorf("plugin setting '%v': ParseUint(%v): %v", 357 v.Key, v.Value, err) 358 } 359 summariesPageSize = uint32(u) 360 log.Infof("Plugin setting updated: ticketvote %v %v", 361 ticketvote.SettingKeySummariesPageSize, summariesPageSize) 362 363 case ticketvote.SettingKeyInventoryPageSize: 364 u, err := strconv.ParseUint(v.Value, 10, 64) 365 if err != nil { 366 return nil, fmt.Errorf("plugin setting '%v': ParseUint(%v): %v", 367 v.Key, v.Value, err) 368 } 369 inventoryPageSize = uint32(u) 370 log.Infof("Plugin setting updated: ticketvote %v %v", 371 ticketvote.SettingKeyInventoryPageSize, inventoryPageSize) 372 373 case ticketvote.SettingKeyTimestampsPageSize: 374 u, err := strconv.ParseUint(v.Value, 10, 64) 375 if err != nil { 376 return nil, fmt.Errorf("plugin setting '%v': ParseUint(%v): %v", 377 v.Key, v.Value, err) 378 } 379 timestampsPageSize = uint32(u) 380 log.Infof("Plugin setting updated: ticketvote %v %v", 381 ticketvote.SettingKeyTimestampsPageSize, timestampsPageSize) 382 383 default: 384 return nil, fmt.Errorf("invalid plugin setting '%v'", v.Key) 385 } 386 } 387 388 // Create the plugin data directory 389 dataDir = filepath.Join(dataDir, ticketvote.PluginID) 390 err := os.MkdirAll(dataDir, 0700) 391 if err != nil { 392 return nil, err 393 } 394 395 return &ticketVotePlugin{ 396 activeNetParams: activeNetParams, 397 backend: backend, 398 tstore: tstore, 399 dataDir: dataDir, 400 identity: id, 401 activeVotes: newActiveVotes(), 402 inv: newInvClient(tstore, backend, inventoryPageSize), 403 summaries: newSummariesClient(tstore), 404 subs: newSubsClient(tstore), 405 linkByPeriodMin: linkByPeriodMin, 406 linkByPeriodMax: linkByPeriodMax, 407 voteDurationMin: voteDurationMin, 408 voteDurationMax: voteDurationMax, 409 summariesPageSize: summariesPageSize, 410 inventoryPageSize: inventoryPageSize, 411 timestampsPageSize: timestampsPageSize, 412 }, nil 413 }