github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/ticketvote/cmds.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 "bytes" 9 "encoding/base64" 10 "encoding/hex" 11 "encoding/json" 12 "fmt" 13 "sort" 14 "strconv" 15 "strings" 16 "sync" 17 "time" 18 19 "github.com/decred/dcrd/chaincfg/v3" 20 backend "github.com/decred/politeia/politeiad/backendv2" 21 "github.com/decred/politeia/politeiad/backendv2/tstorebe/store" 22 "github.com/decred/politeia/politeiad/plugins/dcrdata" 23 "github.com/decred/politeia/politeiad/plugins/ticketvote" 24 "github.com/decred/politeia/util" 25 "github.com/pkg/errors" 26 ) 27 28 const ( 29 pluginID = ticketvote.PluginID 30 31 // Blob entry data descriptors 32 dataDescriptorAuthDetails = pluginID + "-auth-v1" 33 dataDescriptorVoteDetails = pluginID + "-vote-v1" 34 dataDescriptorCastVoteDetails = pluginID + "-castvote-v1" 35 dataDescriptorVoteCollider = pluginID + "-vcollider-v1" 36 dataDescriptorStartRunoff = pluginID + "-startrunoff-v1" 37 ) 38 39 // cmdAuthorize authorizes a ticket vote or revokes a previous authorization. 40 func (p *ticketVotePlugin) cmdAuthorize(token []byte, payload string) (string, error) { 41 // Decode payload 42 var a ticketvote.Authorize 43 err := json.Unmarshal([]byte(payload), &a) 44 if err != nil { 45 return "", err 46 } 47 48 // Verify token 49 err = tokenVerify(token, a.Token) 50 if err != nil { 51 return "", err 52 } 53 54 // Verify signature 55 version := strconv.FormatUint(uint64(a.Version), 10) 56 msg := a.Token + version + string(a.Action) 57 err = util.VerifySignature(a.Signature, a.PublicKey, msg) 58 if err != nil { 59 return "", convertSignatureError(err) 60 } 61 62 // Verify action 63 switch a.Action { 64 case ticketvote.AuthActionAuthorize: 65 // This is allowed 66 case ticketvote.AuthActionRevoke: 67 // This is allowed 68 default: 69 return "", backend.PluginError{ 70 PluginID: ticketvote.PluginID, 71 ErrorCode: uint32(ticketvote.ErrorCodeAuthorizationInvalid), 72 ErrorContext: fmt.Sprintf("%v not a valid action", 73 a.Action), 74 } 75 } 76 77 // Verify record status and version 78 r, err := p.tstore.RecordPartial(token, 0, nil, true) 79 if err != nil { 80 return "", fmt.Errorf("RecordPartial: %v", err) 81 } 82 if r.RecordMetadata.Status != backend.StatusPublic { 83 return "", backend.PluginError{ 84 PluginID: ticketvote.PluginID, 85 ErrorCode: uint32(ticketvote.ErrorCodeRecordStatusInvalid), 86 ErrorContext: "record is not public", 87 } 88 } 89 if a.Version != r.RecordMetadata.Version { 90 return "", backend.PluginError{ 91 PluginID: ticketvote.PluginID, 92 ErrorCode: uint32(ticketvote.ErrorCodeRecordVersionInvalid), 93 ErrorContext: fmt.Sprintf("version is not latest: "+ 94 "got %v, want %v", a.Version, 95 r.RecordMetadata.Version), 96 } 97 } 98 99 // Get any previous authorizations to verify that the new action 100 // is allowed based on the previous action. 101 auths, err := p.auths(token) 102 if err != nil { 103 return "", err 104 } 105 var prevAction ticketvote.AuthActionT 106 if len(auths) > 0 { 107 prevAction = ticketvote.AuthActionT(auths[len(auths)-1].Action) 108 } 109 switch { 110 case len(auths) == 0: 111 // No previous actions. New action must be an authorize. 112 if a.Action != ticketvote.AuthActionAuthorize { 113 return "", backend.PluginError{ 114 PluginID: ticketvote.PluginID, 115 ErrorCode: uint32(ticketvote.ErrorCodeAuthorizationInvalid), 116 ErrorContext: "no prev action; action must " + 117 "be authorize", 118 } 119 } 120 case prevAction == ticketvote.AuthActionAuthorize && 121 a.Action != ticketvote.AuthActionRevoke: 122 // Previous action was a authorize. This action must be revoke. 123 return "", backend.PluginError{ 124 PluginID: ticketvote.PluginID, 125 ErrorCode: uint32(ticketvote.ErrorCodeAuthorizationInvalid), 126 ErrorContext: "prev action was authorize", 127 } 128 case prevAction == ticketvote.AuthActionRevoke && 129 a.Action != ticketvote.AuthActionAuthorize: 130 // Previous action was a revoke. This action must be authorize. 131 return "", backend.PluginError{ 132 PluginID: ticketvote.PluginID, 133 ErrorCode: uint32(ticketvote.ErrorCodeAuthorizationInvalid), 134 ErrorContext: "prev action was revoke", 135 } 136 } 137 138 // Prepare authorize vote 139 receipt := p.identity.SignMessage([]byte(a.Signature)) 140 auth := ticketvote.AuthDetails{ 141 Token: a.Token, 142 Version: a.Version, 143 Action: string(a.Action), 144 PublicKey: a.PublicKey, 145 Signature: a.Signature, 146 Timestamp: time.Now().Unix(), 147 Receipt: hex.EncodeToString(receipt[:]), 148 } 149 150 // Save authorize vote 151 err = p.authSave(token, auth) 152 if err != nil { 153 return "", err 154 } 155 156 // Update the cached inventory 157 var status ticketvote.VoteStatusT 158 switch a.Action { 159 case ticketvote.AuthActionAuthorize: 160 status = ticketvote.VoteStatusAuthorized 161 case ticketvote.AuthActionRevoke: 162 status = ticketvote.VoteStatusUnauthorized 163 default: 164 // Action has already been validated. This should not happen. 165 return "", errors.Errorf("invalid action %v", a.Action) 166 } 167 p.inv.UpdateEntryPreVote(auth.Token, status, auth.Timestamp) 168 169 // Prepare reply 170 ar := ticketvote.AuthorizeReply{ 171 Timestamp: auth.Timestamp, 172 Receipt: auth.Receipt, 173 } 174 reply, err := json.Marshal(ar) 175 if err != nil { 176 return "", err 177 } 178 179 return string(reply), nil 180 } 181 182 // voteBitVerify verifies that the vote bit corresponds to a valid vote option. 183 func voteBitVerify(options []ticketvote.VoteOption, mask, bit uint64) error { 184 if len(options) == 0 { 185 return fmt.Errorf("no vote options found") 186 } 187 if bit == 0 { 188 return fmt.Errorf("invalid bit 0x%x", bit) 189 } 190 191 // Verify bit is included in mask 192 if mask&bit != bit { 193 return fmt.Errorf("invalid mask 0x%x bit 0x%x", mask, bit) 194 } 195 196 // Verify bit is included in vote options 197 for _, v := range options { 198 if v.Bit == bit { 199 // Bit matches one of the options. We're done. 200 return nil 201 } 202 } 203 204 return fmt.Errorf("bit 0x%x not found in vote options", bit) 205 } 206 207 // voteParamsVerify verifies that the params of a ticket vote are within 208 // acceptable values. 209 func voteParamsVerify(vote ticketvote.VoteParams, voteDurationMin, voteDurationMax uint32) error { 210 // Verify vote type 211 switch vote.Type { 212 case ticketvote.VoteTypeStandard: 213 // This is allowed 214 case ticketvote.VoteTypeRunoff: 215 // This is allowed 216 default: 217 return backend.PluginError{ 218 PluginID: ticketvote.PluginID, 219 ErrorCode: uint32(ticketvote.ErrorCodeVoteTypeInvalid), 220 } 221 } 222 223 // Verify vote params 224 switch { 225 case vote.Duration > voteDurationMax: 226 return backend.PluginError{ 227 PluginID: ticketvote.PluginID, 228 ErrorCode: uint32(ticketvote.ErrorCodeVoteDurationInvalid), 229 ErrorContext: fmt.Sprintf("duration %v exceeds max "+ 230 "duration %v", vote.Duration, voteDurationMax), 231 } 232 case vote.Duration < voteDurationMin: 233 return backend.PluginError{ 234 PluginID: ticketvote.PluginID, 235 ErrorCode: uint32(ticketvote.ErrorCodeVoteDurationInvalid), 236 ErrorContext: fmt.Sprintf("duration %v under min "+ 237 "duration %v", vote.Duration, voteDurationMin), 238 } 239 case vote.QuorumPercentage > 100: 240 return backend.PluginError{ 241 PluginID: ticketvote.PluginID, 242 ErrorCode: uint32(ticketvote.ErrorCodeVoteQuorumInvalid), 243 ErrorContext: fmt.Sprintf("quorum percent %v exceeds "+ 244 "100 percent", vote.QuorumPercentage), 245 } 246 case vote.PassPercentage > 100: 247 return backend.PluginError{ 248 PluginID: ticketvote.PluginID, 249 ErrorCode: uint32(ticketvote.ErrorCodeVotePassRateInvalid), 250 ErrorContext: fmt.Sprintf("pass percent %v exceeds "+ 251 "100 percent", vote.PassPercentage), 252 } 253 } 254 255 // Verify vote options. Different vote types have different 256 // requirements. 257 if len(vote.Options) == 0 { 258 return backend.PluginError{ 259 PluginID: ticketvote.PluginID, 260 ErrorCode: uint32(ticketvote.ErrorCodeVoteOptionsInvalid), 261 ErrorContext: "no vote options found", 262 } 263 } 264 switch vote.Type { 265 case ticketvote.VoteTypeStandard, ticketvote.VoteTypeRunoff: 266 // These vote types only allow for approve/reject votes. Ensure 267 // that the only options present are approve/reject and that they 268 // use the vote option IDs specified by the ticketvote API. 269 if len(vote.Options) != 2 { 270 return backend.PluginError{ 271 PluginID: ticketvote.PluginID, 272 ErrorCode: uint32(ticketvote.ErrorCodeVoteOptionsInvalid), 273 ErrorContext: fmt.Sprintf("vote options "+ 274 "count got %v, want 2", 275 len(vote.Options)), 276 } 277 } 278 // map[optionID]found 279 options := map[string]bool{ 280 ticketvote.VoteOptionIDApprove: false, 281 ticketvote.VoteOptionIDReject: false, 282 } 283 for _, v := range vote.Options { 284 switch v.ID { 285 case ticketvote.VoteOptionIDApprove: 286 options[v.ID] = true 287 case ticketvote.VoteOptionIDReject: 288 options[v.ID] = true 289 } 290 } 291 missing := make([]string, 0, 2) 292 for k, v := range options { 293 if !v { 294 // Option ID was not found 295 missing = append(missing, k) 296 } 297 } 298 if len(missing) > 0 { 299 return backend.PluginError{ 300 PluginID: ticketvote.PluginID, 301 ErrorCode: uint32(ticketvote.ErrorCodeVoteOptionsInvalid), 302 ErrorContext: fmt.Sprintf("vote option IDs "+ 303 "not found: %v", 304 strings.Join(missing, ",")), 305 } 306 } 307 } 308 309 // Verify vote bits are somewhat sane 310 for _, v := range vote.Options { 311 err := voteBitVerify(vote.Options, vote.Mask, v.Bit) 312 if err != nil { 313 return backend.PluginError{ 314 PluginID: ticketvote.PluginID, 315 ErrorCode: uint32(ticketvote.ErrorCodeVoteBitsInvalid), 316 ErrorContext: err.Error(), 317 } 318 } 319 } 320 321 // Verify parent token 322 switch { 323 case vote.Type == ticketvote.VoteTypeStandard && vote.Parent != "": 324 return backend.PluginError{ 325 PluginID: ticketvote.PluginID, 326 ErrorCode: uint32(ticketvote.ErrorCodeVoteParentInvalid), 327 ErrorContext: "parent token should not be provided " + 328 "for a standard vote", 329 } 330 case vote.Type == ticketvote.VoteTypeRunoff: 331 _, err := tokenDecode(vote.Parent) 332 if err != nil { 333 return backend.PluginError{ 334 PluginID: ticketvote.PluginID, 335 ErrorCode: uint32(ticketvote.ErrorCodeVoteParentInvalid), 336 ErrorContext: fmt.Sprintf("invalid parent %v", 337 vote.Parent), 338 } 339 } 340 } 341 342 return nil 343 } 344 345 // voteChainParams represent the dcr blockchain parameters for a ticket vote. 346 type voteChainParams struct { 347 StartBlockHeight uint32 `json:"startblockheight"` 348 StartBlockHash string `json:"startblockhash"` 349 EndBlockHeight uint32 `json:"endblockheight"` 350 EligibleTickets []string `json:"eligibletickets"` // Ticket hashes 351 } 352 353 // voteChainParams fetches and returns the voteChainParams for a ticket vote. 354 func (p *ticketVotePlugin) voteChainParams(duration uint32) (*voteChainParams, error) { 355 // Get the best block height 356 bb, err := p.bestBlock() 357 if err != nil { 358 return nil, fmt.Errorf("bestBlock: %v", err) 359 } 360 361 // Find the snapshot height. Subtract the ticket maturity from the 362 // block height to get into unforkable territory. 363 ticketMaturity := uint32(p.activeNetParams.TicketMaturity) 364 snapshotHeight := bb - ticketMaturity 365 366 // Fetch the block details for the snapshot height. We need the 367 // block hash in order to fetch the ticket pool snapshot. 368 bd := dcrdata.BlockDetails{ 369 Height: snapshotHeight, 370 } 371 payload, err := json.Marshal(bd) 372 if err != nil { 373 return nil, err 374 } 375 reply, err := p.backend.PluginRead(nil, dcrdata.PluginID, 376 dcrdata.CmdBlockDetails, string(payload)) 377 if err != nil { 378 return nil, fmt.Errorf("PluginRead %v %v: %v", 379 dcrdata.PluginID, dcrdata.CmdBlockDetails, err) 380 } 381 var bdr dcrdata.BlockDetailsReply 382 err = json.Unmarshal([]byte(reply), &bdr) 383 if err != nil { 384 return nil, err 385 } 386 if bdr.Block.Hash == "" { 387 return nil, fmt.Errorf("invalid block hash for height %v", 388 snapshotHeight) 389 } 390 snapshotHash := bdr.Block.Hash 391 392 // Fetch the ticket pool snapshot 393 tp := dcrdata.TicketPool{ 394 BlockHash: snapshotHash, 395 } 396 payload, err = json.Marshal(tp) 397 if err != nil { 398 return nil, err 399 } 400 reply, err = p.backend.PluginRead(nil, dcrdata.PluginID, 401 dcrdata.CmdTicketPool, string(payload)) 402 if err != nil { 403 return nil, fmt.Errorf("PluginRead %v %v: %v", 404 dcrdata.PluginID, dcrdata.CmdTicketPool, err) 405 } 406 var tpr dcrdata.TicketPoolReply 407 err = json.Unmarshal([]byte(reply), &tpr) 408 if err != nil { 409 return nil, err 410 } 411 if len(tpr.Tickets) == 0 { 412 return nil, fmt.Errorf("no tickets found for block %v %v", 413 snapshotHeight, snapshotHash) 414 } 415 416 // The start block height has the ticket maturity subtracted from 417 // it to prevent forking issues. This means we the vote starts in 418 // the past. The ticket maturity needs to be added to the end block 419 // height to correct for this. 420 endBlockHeight := snapshotHeight + duration + ticketMaturity 421 422 return &voteChainParams{ 423 StartBlockHeight: snapshotHeight, 424 StartBlockHash: snapshotHash, 425 EndBlockHeight: endBlockHeight, 426 EligibleTickets: tpr.Tickets, 427 }, nil 428 } 429 430 // startStandard starts a standard vote. 431 func (p *ticketVotePlugin) startStandard(token []byte, s ticketvote.Start) (*ticketvote.StartReply, error) { 432 // Verify there is only one start details 433 if len(s.Starts) != 1 { 434 return nil, backend.PluginError{ 435 PluginID: ticketvote.PluginID, 436 ErrorCode: uint32(ticketvote.ErrorCodeStartDetailsInvalid), 437 ErrorContext: "more than one start details found for " + 438 "standard vote", 439 } 440 } 441 sd := s.Starts[0] 442 443 // Verify token 444 err := tokenVerify(token, sd.Params.Token) 445 if err != nil { 446 return nil, err 447 } 448 449 // Verify signature 450 vb, err := json.Marshal(sd.Params) 451 if err != nil { 452 return nil, err 453 } 454 msg := hex.EncodeToString(util.Digest(vb)) 455 err = util.VerifySignature(sd.Signature, sd.PublicKey, msg) 456 if err != nil { 457 return nil, convertSignatureError(err) 458 } 459 460 // Verify vote options and params 461 err = voteParamsVerify(sd.Params, p.voteDurationMin, p.voteDurationMax) 462 if err != nil { 463 return nil, err 464 } 465 466 // Verify record status and version 467 r, err := p.tstore.RecordPartial(token, 0, nil, true) 468 if err != nil { 469 return nil, fmt.Errorf("RecordPartial: %v", err) 470 } 471 if r.RecordMetadata.Status != backend.StatusPublic { 472 return nil, backend.PluginError{ 473 PluginID: ticketvote.PluginID, 474 ErrorCode: uint32(ticketvote.ErrorCodeRecordStatusInvalid), 475 ErrorContext: "record is not public", 476 } 477 } 478 if sd.Params.Version != r.RecordMetadata.Version { 479 return nil, backend.PluginError{ 480 PluginID: ticketvote.PluginID, 481 ErrorCode: uint32(ticketvote.ErrorCodeRecordVersionInvalid), 482 ErrorContext: fmt.Sprintf("version is not latest: "+ 483 "got %v, want %v", sd.Params.Version, 484 r.RecordMetadata.Version), 485 } 486 } 487 488 // Get vote blockchain data 489 vcp, err := p.voteChainParams(sd.Params.Duration) 490 if err != nil { 491 return nil, err 492 } 493 494 // Verify vote authorization 495 auths, err := p.auths(token) 496 if err != nil { 497 return nil, err 498 } 499 if len(auths) == 0 { 500 return nil, backend.PluginError{ 501 PluginID: ticketvote.PluginID, 502 ErrorCode: uint32(ticketvote.ErrorCodeVoteStatusInvalid), 503 ErrorContext: "not authorized", 504 } 505 } 506 action := ticketvote.AuthActionT(auths[len(auths)-1].Action) 507 if action != ticketvote.AuthActionAuthorize { 508 return nil, backend.PluginError{ 509 PluginID: ticketvote.PluginID, 510 ErrorCode: uint32(ticketvote.ErrorCodeVoteStatusInvalid), 511 ErrorContext: "not authorized", 512 } 513 } 514 515 // Verify vote has not already been started 516 svp, err := p.voteDetails(token) 517 if err != nil { 518 return nil, err 519 } 520 if svp != nil { 521 // Vote has already been started 522 return nil, backend.PluginError{ 523 PluginID: ticketvote.PluginID, 524 ErrorCode: uint32(ticketvote.ErrorCodeVoteStatusInvalid), 525 ErrorContext: "vote already started", 526 } 527 } 528 529 // Prepare vote details 530 receipt := p.identity.SignMessage([]byte(sd.Signature + vcp.StartBlockHash)) 531 vd := ticketvote.VoteDetails{ 532 Params: sd.Params, 533 PublicKey: sd.PublicKey, 534 Signature: sd.Signature, 535 Receipt: hex.EncodeToString(receipt[:]), 536 StartBlockHeight: vcp.StartBlockHeight, 537 StartBlockHash: vcp.StartBlockHash, 538 EndBlockHeight: vcp.EndBlockHeight, 539 EligibleTickets: vcp.EligibleTickets, 540 } 541 542 // Save vote details 543 err = p.voteDetailsSave(token, vd) 544 if err != nil { 545 return nil, err 546 } 547 548 // Update the cached inventory 549 p.inv.UpdateEntryPostVote(vd.Params.Token, 550 ticketvote.VoteStatusStarted, vd.EndBlockHeight) 551 552 // Update active votes cache 553 p.activeVotesAdd(vd) 554 555 return &ticketvote.StartReply{ 556 Receipt: vd.Receipt, 557 StartBlockHeight: vd.StartBlockHeight, 558 StartBlockHash: vd.StartBlockHash, 559 EndBlockHeight: vd.EndBlockHeight, 560 EligibleTickets: vd.EligibleTickets, 561 }, nil 562 } 563 564 // startRunoffRecordSave saves a startRunoffRecord to the backend. 565 func (p *ticketVotePlugin) startRunoffRecordSave(token []byte, srr startRunoffRecord) error { 566 be, err := convertBlobEntryFromStartRunoff(srr) 567 if err != nil { 568 return err 569 } 570 err = p.tstore.BlobSave(token, *be) 571 if err != nil { 572 return err 573 } 574 return nil 575 } 576 577 // startRunoffRecord returns the startRunoff record if one exists. Nil is 578 // returned if a startRunoff record is not found. 579 func (p *ticketVotePlugin) startRunoffRecord(token []byte) (*startRunoffRecord, error) { 580 blobs, err := p.tstore.BlobsByDataDesc(token, 581 []string{dataDescriptorStartRunoff}) 582 if err != nil { 583 return nil, err 584 } 585 586 var srr *startRunoffRecord 587 switch len(blobs) { 588 case 0: 589 // Nothing found 590 return nil, nil 591 case 1: 592 // A start runoff record was found 593 srr, err = convertStartRunoffFromBlobEntry(blobs[0]) 594 if err != nil { 595 return nil, err 596 } 597 default: 598 // This should not be possible 599 e := fmt.Sprintf("%v start runoff blobs found", len(blobs)) 600 panic(e) 601 } 602 603 return srr, nil 604 } 605 606 // startRunoffForSub starts the voting period for a runoff vote submission. 607 func (p *ticketVotePlugin) startRunoffForSub(token []byte, srs startRunoffSubmission) error { 608 // Sanity check 609 sd := srs.StartDetails 610 t, err := tokenDecode(sd.Params.Token) 611 if err != nil { 612 return err 613 } 614 if !bytes.Equal(token, t) { 615 return fmt.Errorf("invalid token") 616 } 617 618 // Get the start runoff record from the parent record 619 parent, err := tokenDecode(srs.ParentToken) 620 if err != nil { 621 return err 622 } 623 srr, err := p.startRunoffRecord(parent) 624 if err != nil { 625 return err 626 } 627 628 // Sanity check. Verify token is part of the start runoff record 629 // submissions. 630 var found bool 631 for _, v := range srr.Submissions { 632 if hex.EncodeToString(token) == v { 633 found = true 634 break 635 } 636 } 637 if !found { 638 // This submission should not be here 639 return fmt.Errorf("record not in submission list") 640 } 641 642 // If the vote has already been started, exit gracefully. This 643 // allows us to recover from unexpected errors to the start runoff 644 // vote call as it updates the state of multiple records. If the 645 // call were to fail before completing, we can simply call the 646 // command again with the same arguments and it will pick up where 647 // it left off. 648 svp, err := p.voteDetails(token) 649 if err != nil { 650 return err 651 } 652 if svp != nil { 653 // Vote has already been started. Exit gracefully. 654 return nil 655 } 656 657 // Verify record version 658 r, err := p.tstore.RecordPartial(token, 0, nil, true) 659 if err != nil { 660 return fmt.Errorf("RecordPartial: %v", err) 661 } 662 if r.RecordMetadata.State != backend.StateVetted { 663 // This should not be possible 664 return fmt.Errorf("record is unvetted") 665 } 666 if sd.Params.Version != r.RecordMetadata.Version { 667 return backend.PluginError{ 668 PluginID: ticketvote.PluginID, 669 ErrorCode: uint32(ticketvote.ErrorCodeRecordVersionInvalid), 670 ErrorContext: fmt.Sprintf("version is not latest %v: "+ 671 "got %v, want %v", sd.Params.Token, 672 sd.Params.Version, r.RecordMetadata.Version), 673 } 674 } 675 676 // Prepare vote details 677 receipt := p.identity.SignMessage([]byte(sd.Signature + srr.StartBlockHash)) 678 vd := ticketvote.VoteDetails{ 679 Params: sd.Params, 680 PublicKey: sd.PublicKey, 681 Signature: sd.Signature, 682 Receipt: hex.EncodeToString(receipt[:]), 683 StartBlockHeight: srr.StartBlockHeight, 684 StartBlockHash: srr.StartBlockHash, 685 EndBlockHeight: srr.EndBlockHeight, 686 EligibleTickets: srr.EligibleTickets, 687 } 688 689 // Save vote details 690 err = p.voteDetailsSave(token, vd) 691 if err != nil { 692 return err 693 } 694 695 // Update the cached inventory 696 p.inv.UpdateEntryPostVote(vd.Params.Token, 697 ticketvote.VoteStatusStarted, vd.EndBlockHeight) 698 699 // Update active votes cache 700 p.activeVotesAdd(vd) 701 702 return nil 703 } 704 705 // startRunoffForParent saves a startRunoffRecord to the parent record. Once 706 // this has been saved the runoff vote is considered to be started and the 707 // voting period on individual runoff vote submissions can be started. 708 func (p *ticketVotePlugin) startRunoffForParent(token []byte, s ticketvote.Start) (*startRunoffRecord, error) { 709 // Check if the runoff vote data already exists on the parent tree. 710 srr, err := p.startRunoffRecord(token) 711 if err != nil { 712 return nil, err 713 } 714 if srr != nil { 715 // We already have a start runoff record for this runoff vote. 716 // This can happen if the previous call failed due to an 717 // unexpected error such as a network error. Return the start 718 // runoff record so we can pick up where we left off. 719 return srr, nil 720 } 721 722 // Get blockchain data 723 var ( 724 mask = s.Starts[0].Params.Mask 725 duration = s.Starts[0].Params.Duration 726 quorum = s.Starts[0].Params.QuorumPercentage 727 pass = s.Starts[0].Params.PassPercentage 728 ) 729 vcp, err := p.voteChainParams(duration) 730 if err != nil { 731 return nil, err 732 } 733 734 // Verify parent has a LinkBy and the LinkBy deadline is expired. 735 files := []string{ 736 ticketvote.FileNameVoteMetadata, 737 } 738 r, err := p.tstore.RecordPartial(token, 0, files, false) 739 if err != nil { 740 if errors.Is(err, backend.ErrRecordNotFound) { 741 return nil, backend.PluginError{ 742 PluginID: ticketvote.PluginID, 743 ErrorCode: uint32(ticketvote.ErrorCodeVoteParentInvalid), 744 ErrorContext: fmt.Sprintf("parent record not "+ 745 "found %x", token), 746 } 747 } 748 return nil, fmt.Errorf("RecordPartial: %v", err) 749 } 750 if r.RecordMetadata.State != backend.StateVetted { 751 // This should not be possible 752 return nil, fmt.Errorf("record is unvetted") 753 } 754 vm, err := voteMetadataDecode(r.Files) 755 if err != nil { 756 return nil, err 757 } 758 if vm == nil || vm.LinkBy == 0 { 759 return nil, backend.PluginError{ 760 PluginID: ticketvote.PluginID, 761 ErrorCode: uint32(ticketvote.ErrorCodeVoteParentInvalid), 762 ErrorContext: fmt.Sprintf("%x is not a runoff vote "+ 763 "parent", token), 764 } 765 } 766 if vm.LinkBy > time.Now().Unix() { 767 return nil, backend.PluginError{ 768 PluginID: ticketvote.PluginID, 769 ErrorCode: uint32(ticketvote.ErrorCodeLinkByNotExpired), 770 ErrorContext: fmt.Sprintf("parent record %x linkby "+ 771 "deadline (%v) has not expired yet", token, vm.LinkBy), 772 } 773 } 774 775 // Compile a list of the expected submissions that should be in the 776 // runoff vote. This will be all of the public records that have 777 // linked to the parent record. The parent record's submissions 778 // list will include abandoned proposals that need to be filtered 779 // out. 780 ss, err := p.subs.Get(tokenEncode(token)) 781 if err != nil { 782 return nil, err 783 } 784 expected := make(map[string]struct{}, len(ss.Tokens)) // [token]struct{} 785 for k := range ss.Tokens { 786 token, err := tokenDecode(k) 787 if err != nil { 788 return nil, err 789 } 790 r, err := p.recordAbridged(token) 791 if err != nil { 792 return nil, err 793 } 794 if r.RecordMetadata.Status != backend.StatusPublic { 795 // This record is not public and should not be included 796 // in the runoff vote. 797 continue 798 } 799 800 // This is a public record that is part of the parent record's 801 // submissions list. It is required to be in the runoff vote. 802 expected[k] = struct{}{} 803 } 804 805 // Verify that there are no extra submissions in the runoff vote 806 for _, v := range s.Starts { 807 _, ok := expected[v.Params.Token] 808 if !ok { 809 // This submission should not be here 810 return nil, backend.PluginError{ 811 PluginID: ticketvote.PluginID, 812 ErrorCode: uint32(ticketvote.ErrorCodeStartDetailsInvalid), 813 ErrorContext: fmt.Sprintf("record %v should "+ 814 "not be included", v.Params.Token), 815 } 816 } 817 } 818 819 // Verify that the runoff vote is not missing any submissions 820 subs := make(map[string]struct{}, len(s.Starts)) 821 for _, v := range s.Starts { 822 subs[v.Params.Token] = struct{}{} 823 } 824 for k := range expected { 825 _, ok := subs[k] 826 if !ok { 827 // This records is missing from the runoff vote 828 return nil, backend.PluginError{ 829 PluginID: ticketvote.PluginID, 830 ErrorCode: uint32(ticketvote.ErrorCodeStartDetailsMissing), 831 ErrorContext: k, 832 } 833 } 834 } 835 836 // Prepare start runoff record 837 submissions := make([]string, 0, len(subs)) 838 for k := range subs { 839 submissions = append(submissions, k) 840 } 841 srr = &startRunoffRecord{ 842 Submissions: submissions, 843 Mask: mask, 844 Duration: duration, 845 QuorumPercentage: quorum, 846 PassPercentage: pass, 847 StartBlockHeight: vcp.StartBlockHeight, 848 StartBlockHash: vcp.StartBlockHash, 849 EndBlockHeight: vcp.EndBlockHeight, 850 EligibleTickets: vcp.EligibleTickets, 851 } 852 853 // Save start runoff record 854 err = p.startRunoffRecordSave(token, *srr) 855 if err != nil { 856 return nil, err 857 } 858 859 return srr, nil 860 } 861 862 // startRunoff starts the voting period for all submissions in a runoff vote. 863 // It does this by first adding a startRunoffRecord to the runoff vote parent 864 // record. Once this has been successfully added the runoff vote is considered 865 // to have started. The voting period must now be started on all of the runoff 866 // vote submissions individually. If any of these calls fail, they can be 867 // retried. This function will pick up where it left off. 868 func (p *ticketVotePlugin) startRunoff(token []byte, s ticketvote.Start) (*ticketvote.StartReply, error) { 869 // Sanity check 870 if len(s.Starts) == 0 { 871 return nil, fmt.Errorf("no start details found") 872 } 873 874 // Perform validation that can be done without fetching any records 875 // from the backend. 876 var ( 877 mask = s.Starts[0].Params.Mask 878 duration = s.Starts[0].Params.Duration 879 quorum = s.Starts[0].Params.QuorumPercentage 880 pass = s.Starts[0].Params.PassPercentage 881 parent = s.Starts[0].Params.Parent 882 ) 883 for _, v := range s.Starts { 884 // Verify vote params are the same for all submissions 885 switch { 886 case v.Params.Type != ticketvote.VoteTypeRunoff: 887 return nil, backend.PluginError{ 888 PluginID: ticketvote.PluginID, 889 ErrorCode: uint32(ticketvote.ErrorCodeVoteTypeInvalid), 890 ErrorContext: fmt.Sprintf("%v got %v, want %v", 891 v.Params.Token, v.Params.Type, 892 ticketvote.VoteTypeRunoff), 893 } 894 case v.Params.Mask != mask: 895 return nil, backend.PluginError{ 896 PluginID: ticketvote.PluginID, 897 ErrorCode: uint32(ticketvote.ErrorCodeVoteBitsInvalid), 898 ErrorContext: fmt.Sprintf("%v mask invalid: "+ 899 "all must be the same", v.Params.Token), 900 } 901 case v.Params.Duration != duration: 902 return nil, backend.PluginError{ 903 PluginID: ticketvote.PluginID, 904 ErrorCode: uint32(ticketvote.ErrorCodeVoteDurationInvalid), 905 ErrorContext: fmt.Sprintf("%v duration does "+ 906 "not match; all must be the same", 907 v.Params.Token), 908 } 909 case v.Params.QuorumPercentage != quorum: 910 return nil, backend.PluginError{ 911 PluginID: ticketvote.PluginID, 912 ErrorCode: uint32(ticketvote.ErrorCodeVoteQuorumInvalid), 913 ErrorContext: fmt.Sprintf("%v quorum does "+ 914 "not match; all must be the same", 915 v.Params.Token), 916 } 917 case v.Params.PassPercentage != pass: 918 return nil, backend.PluginError{ 919 PluginID: ticketvote.PluginID, 920 ErrorCode: uint32(ticketvote.ErrorCodeVotePassRateInvalid), 921 ErrorContext: fmt.Sprintf("%v pass rate does "+ 922 "not match; all must be the same", 923 v.Params.Token), 924 } 925 case v.Params.Parent != parent: 926 return nil, backend.PluginError{ 927 PluginID: ticketvote.PluginID, 928 ErrorCode: uint32(ticketvote.ErrorCodeVoteParentInvalid), 929 ErrorContext: fmt.Sprintf("%v parent does "+ 930 "not match; all must be the same", 931 v.Params.Token), 932 } 933 } 934 935 // Verify token 936 _, err := tokenDecode(v.Params.Token) 937 if err != nil { 938 return nil, backend.PluginError{ 939 PluginID: ticketvote.PluginID, 940 ErrorCode: uint32(ticketvote.ErrorCodeTokenInvalid), 941 ErrorContext: v.Params.Token, 942 } 943 } 944 945 // Verify parent token 946 _, err = tokenDecode(v.Params.Parent) 947 if err != nil { 948 return nil, backend.PluginError{ 949 PluginID: ticketvote.PluginID, 950 ErrorCode: uint32(ticketvote.ErrorCodeTokenInvalid), 951 ErrorContext: fmt.Sprintf("parent token %v", 952 v.Params.Parent), 953 } 954 } 955 956 // Verify signature 957 vb, err := json.Marshal(v.Params) 958 if err != nil { 959 return nil, err 960 } 961 msg := hex.EncodeToString(util.Digest(vb)) 962 err = util.VerifySignature(v.Signature, v.PublicKey, msg) 963 if err != nil { 964 return nil, convertSignatureError(err) 965 } 966 967 // Verify vote options and params. Vote optoins are required to 968 // be approve and reject. 969 err = voteParamsVerify(v.Params, p.voteDurationMin, 970 p.voteDurationMax) 971 if err != nil { 972 return nil, err 973 } 974 } 975 976 // Verify plugin command is being executed on the parent record 977 if hex.EncodeToString(token) != parent { 978 return nil, backend.PluginError{ 979 PluginID: ticketvote.PluginID, 980 ErrorCode: uint32(ticketvote.ErrorCodeVoteParentInvalid), 981 ErrorContext: fmt.Sprintf("runoff vote must be "+ 982 "started on the parent record %v", parent), 983 } 984 } 985 986 // This function is being invoked on the runoff vote parent record. 987 // Create and save a start runoff record onto the parent record's tree. 988 srr, err := p.startRunoffForParent(token, s) 989 if err != nil { 990 return nil, err 991 } 992 993 // Start the voting period of each runoff vote submissions by using the 994 // internal plugin command startRunoffSubmission. 995 for _, v := range s.Starts { 996 token, err = tokenDecode(v.Params.Token) 997 if err != nil { 998 return nil, err 999 } 1000 srs := startRunoffSubmission{ 1001 ParentToken: v.Params.Parent, 1002 StartDetails: v, 1003 } 1004 b, err := json.Marshal(srs) 1005 if err != nil { 1006 return nil, err 1007 } 1008 _, err = p.backend.PluginWrite(token, ticketvote.PluginID, 1009 cmdStartRunoffSubmission, string(b)) 1010 if err != nil { 1011 var ue backend.PluginError 1012 if errors.As(err, &ue) { 1013 return nil, err 1014 } 1015 return nil, fmt.Errorf("PluginWrite %x %v %v: %v", 1016 token, ticketvote.PluginID, 1017 cmdStartRunoffSubmission, err) 1018 } 1019 } 1020 1021 return &ticketvote.StartReply{ 1022 StartBlockHeight: srr.StartBlockHeight, 1023 StartBlockHash: srr.StartBlockHash, 1024 EndBlockHeight: srr.EndBlockHeight, 1025 EligibleTickets: srr.EligibleTickets, 1026 }, nil 1027 } 1028 1029 // cmdStartRunoffSubmission is an internal plugin command that is used to start 1030 // the voting period on a runoff vote submission. 1031 func (p *ticketVotePlugin) cmdStartRunoffSubmission(token []byte, payload string) (string, error) { 1032 // Decode payload 1033 var srs startRunoffSubmission 1034 err := json.Unmarshal([]byte(payload), &srs) 1035 if err != nil { 1036 return "", err 1037 } 1038 1039 // Start voting period on runoff vote submission 1040 err = p.startRunoffForSub(token, srs) 1041 if err != nil { 1042 return "", err 1043 } 1044 1045 return "", nil 1046 } 1047 1048 // cmdStart starts a ticket vote. 1049 func (p *ticketVotePlugin) cmdStart(token []byte, payload string) (string, error) { 1050 // Decode payload 1051 var s ticketvote.Start 1052 err := json.Unmarshal([]byte(payload), &s) 1053 if err != nil { 1054 return "", err 1055 } 1056 1057 // Parse vote type 1058 if len(s.Starts) == 0 { 1059 return "", backend.PluginError{ 1060 PluginID: ticketvote.PluginID, 1061 ErrorCode: uint32(ticketvote.ErrorCodeStartDetailsMissing), 1062 ErrorContext: "no start details found", 1063 } 1064 } 1065 vtype := s.Starts[0].Params.Type 1066 1067 // Start vote 1068 var sr *ticketvote.StartReply 1069 switch vtype { 1070 case ticketvote.VoteTypeStandard: 1071 sr, err = p.startStandard(token, s) 1072 if err != nil { 1073 return "", err 1074 } 1075 case ticketvote.VoteTypeRunoff: 1076 sr, err = p.startRunoff(token, s) 1077 if err != nil { 1078 return "", err 1079 } 1080 default: 1081 return "", backend.PluginError{ 1082 PluginID: ticketvote.PluginID, 1083 ErrorCode: uint32(ticketvote.ErrorCodeVoteTypeInvalid), 1084 } 1085 } 1086 1087 // Prepare reply 1088 reply, err := json.Marshal(*sr) 1089 if err != nil { 1090 return "", err 1091 } 1092 1093 return string(reply), nil 1094 } 1095 1096 // commitmentAddr represents the largest commitment address for a dcr ticket. 1097 type commitmentAddr struct { 1098 addr string // Commitment address 1099 err error // Error if one occurred 1100 } 1101 1102 // largestCommitmentAddrs retrieves the largest commitment addresses for each 1103 // of the provided tickets from dcrdata. A map[ticket]commitmentAddr is 1104 // returned. If an error is encountered while retrieving a commitment address, 1105 // the error will be included in the commitmentAddr struct in the returned 1106 // map. 1107 func (p *ticketVotePlugin) largestCommitmentAddrs(tickets []string) (map[string]commitmentAddr, error) { 1108 // Get tx details 1109 tt := dcrdata.TxsTrimmed{ 1110 TxIDs: tickets, 1111 } 1112 payload, err := json.Marshal(tt) 1113 if err != nil { 1114 return nil, err 1115 } 1116 reply, err := p.backend.PluginRead(nil, dcrdata.PluginID, 1117 dcrdata.CmdTxsTrimmed, string(payload)) 1118 if err != nil { 1119 return nil, fmt.Errorf("PluginRead %v %v: %v", 1120 dcrdata.PluginID, dcrdata.CmdTxsTrimmed, err) 1121 } 1122 var ttr dcrdata.TxsTrimmedReply 1123 err = json.Unmarshal([]byte(reply), &ttr) 1124 if err != nil { 1125 return nil, err 1126 } 1127 1128 // Find the largest commitment address for each tx 1129 addrs := make(map[string]commitmentAddr, len(ttr.Txs)) 1130 for _, tx := range ttr.Txs { 1131 var ( 1132 bestAddr string // Addr with largest commitment amount 1133 bestAmt float64 // Largest commitment amount 1134 addrErr error // Error if one is encountered 1135 ) 1136 for _, vout := range tx.Vout { 1137 scriptPubKey := vout.ScriptPubKeyDecoded 1138 switch { 1139 case scriptPubKey.CommitAmt == nil: 1140 // No commitment amount; continue 1141 case len(scriptPubKey.Addresses) == 0: 1142 // No commitment address; continue 1143 case *scriptPubKey.CommitAmt > bestAmt: 1144 // New largest commitment address found 1145 bestAddr = scriptPubKey.Addresses[0] 1146 bestAmt = *scriptPubKey.CommitAmt 1147 } 1148 } 1149 if bestAddr == "" || bestAmt == 0.0 { 1150 addrErr = fmt.Errorf("no largest commitment address " + 1151 "found") 1152 } 1153 1154 // Store result 1155 addrs[tx.TxID] = commitmentAddr{ 1156 addr: bestAddr, 1157 err: addrErr, 1158 } 1159 } 1160 1161 return addrs, nil 1162 } 1163 1164 // voteCollider is used to prevent duplicate votes at the tlog level. The 1165 // backend saves a digest of the data to the trillian log (tlog). Tlog does not 1166 // allow leaves with duplicate values, so once a vote colider is saved to the 1167 // backend for a ticket it should be impossible for another vote collider to be 1168 // saved to the backend that is voting with the same ticket on the same record, 1169 // regardless of what the vote bits are. The vote collider and the full cast 1170 // vote are saved to the backend at the same time. A cast vote is not 1171 // considered valid unless a corresponding vote collider is present. 1172 type voteCollider struct { 1173 Token string `json:"token"` // Record token 1174 Ticket string `json:"ticket"` // Ticket hash 1175 } 1176 1177 // voteColliderSave saves a voteCollider to the backend. 1178 func (p *ticketVotePlugin) voteColliderSave(token []byte, vc voteCollider) error { 1179 // Prepare blob 1180 be, err := convertBlobEntryFromVoteCollider(vc) 1181 if err != nil { 1182 return err 1183 } 1184 1185 // Save blob 1186 return p.tstore.BlobSave(token, *be) 1187 } 1188 1189 // ballotResults is used to aggregate data for votes that are cast 1190 // concurrently. 1191 type ballotResults struct { 1192 sync.RWMutex 1193 addrs map[string]string // [ticket]commitmentAddr 1194 replies map[string]ticketvote.CastVoteReply // [ticket]CastVoteReply 1195 } 1196 1197 // newBallotResults returns a new ballotResults context. 1198 func newBallotResults() ballotResults { 1199 return ballotResults{ 1200 addrs: make(map[string]string, 40960), 1201 replies: make(map[string]ticketvote.CastVoteReply, 40960), 1202 } 1203 } 1204 1205 // addrSet sets the largest commitment addresss for a ticket. 1206 func (r *ballotResults) addrSet(ticket, commitmentAddr string) { 1207 r.Lock() 1208 defer r.Unlock() 1209 1210 r.addrs[ticket] = commitmentAddr 1211 } 1212 1213 // addrGet returns the largest commitment address for a ticket. 1214 func (r *ballotResults) addrGet(ticket string) (string, bool) { 1215 r.RLock() 1216 defer r.RUnlock() 1217 1218 a, ok := r.addrs[ticket] 1219 return a, ok 1220 } 1221 1222 // replySet sets the CastVoteReply for a ticket. 1223 func (r *ballotResults) replySet(ticket string, cvr ticketvote.CastVoteReply) { 1224 r.Lock() 1225 defer r.Unlock() 1226 1227 r.replies[ticket] = cvr 1228 } 1229 1230 // replyGet returns the CastVoteReply for a ticket. 1231 func (r *ballotResults) replyGet(ticket string) (ticketvote.CastVoteReply, bool) { 1232 r.RLock() 1233 defer r.RUnlock() 1234 1235 cvr, ok := r.replies[ticket] 1236 return cvr, ok 1237 } 1238 1239 // repliesLen returns the number of replies in the ballot results. 1240 func (r *ballotResults) repliesLen() int { 1241 r.RLock() 1242 defer r.RUnlock() 1243 1244 return len(r.replies) 1245 } 1246 1247 // castVoteDetailsSave saves a CastVoteDetails to the backend. 1248 func (p *ticketVotePlugin) castVoteDetailsSave(token []byte, cv ticketvote.CastVoteDetails) error { 1249 // Prepare blob 1250 be, err := convertBlobEntryFromCastVoteDetails(cv) 1251 if err != nil { 1252 return err 1253 } 1254 1255 // Save blob 1256 return p.tstore.BlobSave(token, *be) 1257 } 1258 1259 // castVoteVerifySignature verifies the signature of a CastVote. The signature 1260 // must be created using the largest commitment address from the ticket that is 1261 // casting a vote. 1262 func castVoteVerifySignature(cv ticketvote.CastVote, addr string, net *chaincfg.Params) error { 1263 msg := cv.Token + cv.Ticket + cv.VoteBit 1264 1265 // Convert hex signature to base64. This is what the verify 1266 // message function expects. 1267 b, err := hex.DecodeString(cv.Signature) 1268 if err != nil { 1269 return fmt.Errorf("invalid hex") 1270 } 1271 sig := base64.StdEncoding.EncodeToString(b) 1272 1273 // Verify message 1274 validated, err := util.VerifyMessage(addr, msg, sig, net) 1275 if err != nil { 1276 return err 1277 } 1278 if !validated { 1279 return fmt.Errorf("could not verify message") 1280 } 1281 1282 return nil 1283 } 1284 1285 // ballot casts the provided votes concurrently. The vote results are passed 1286 // back through the results channel to the calling function. This function 1287 // waits until all provided votes have been cast before returning. 1288 func (p *ticketVotePlugin) ballot(token []byte, votes []ticketvote.CastVote, br *ballotResults) { 1289 // Cast the votes concurrently 1290 var wg sync.WaitGroup 1291 for _, v := range votes { 1292 // Increment the wait group counter 1293 wg.Add(1) 1294 1295 go func(v ticketvote.CastVote, br *ballotResults) { 1296 // Decrement wait group counter once vote is cast 1297 defer wg.Done() 1298 1299 // Declare here to prevent goto errors 1300 var ( 1301 cvd ticketvote.CastVoteDetails 1302 cvr ticketvote.CastVoteReply 1303 vc voteCollider 1304 err error 1305 1306 receipt = p.identity.SignMessage([]byte(v.Signature)) 1307 ) 1308 1309 addr, ok := br.addrGet(v.Ticket) 1310 if !ok || addr == "" { 1311 // Something went wrong. The largest commitment 1312 // address could not be found for this ticket. 1313 t := time.Now().Unix() 1314 log.Errorf("cmdCastBallot: commitment addr not "+ 1315 "found %v", t) 1316 e := ticketvote.VoteErrorInternalError 1317 cvr.Ticket = v.Ticket 1318 cvr.ErrorCode = &e 1319 cvr.ErrorContext = fmt.Sprintf("%v: %v", 1320 ticketvote.VoteErrors[e], t) 1321 goto saveReply 1322 } 1323 1324 // Setup cast vote details 1325 cvd = ticketvote.CastVoteDetails{ 1326 Token: v.Token, 1327 Ticket: v.Ticket, 1328 VoteBit: v.VoteBit, 1329 Signature: v.Signature, 1330 Address: addr, 1331 Receipt: hex.EncodeToString(receipt[:]), 1332 Timestamp: time.Now().Unix(), 1333 } 1334 1335 // Save cast vote details 1336 err = p.castVoteDetailsSave(token, cvd) 1337 if errors.Is(err, backend.ErrDuplicatePayload) { 1338 // This cast vote has already been saved. Its 1339 // possible that a previous attempt to vote 1340 // with this ticket failed before the vote 1341 // collider could be saved. Continue execution 1342 // so that we re-attempt to save the vote 1343 // collider. 1344 } else if err != nil { 1345 t := time.Now().Unix() 1346 log.Errorf("cmdCastBallot: castVoteSave %v: "+ 1347 "%v", t, err) 1348 e := ticketvote.VoteErrorInternalError 1349 cvr.Ticket = v.Ticket 1350 cvr.ErrorCode = &e 1351 cvr.ErrorContext = fmt.Sprintf("%v: %v", 1352 ticketvote.VoteErrors[e], t) 1353 goto saveReply 1354 } 1355 1356 // Save vote collider 1357 vc = voteCollider{ 1358 Token: v.Token, 1359 Ticket: v.Ticket, 1360 } 1361 err = p.voteColliderSave(token, vc) 1362 if err != nil { 1363 t := time.Now().Unix() 1364 log.Errorf("cmdCastBallot: voteColliderSave %v: %v", t, err) 1365 e := ticketvote.VoteErrorInternalError 1366 cvr.Ticket = v.Ticket 1367 cvr.ErrorCode = &e 1368 cvr.ErrorContext = fmt.Sprintf("%v: %v", 1369 ticketvote.VoteErrors[e], t) 1370 goto saveReply 1371 } 1372 1373 // Update receipt 1374 cvr.Ticket = v.Ticket 1375 cvr.Receipt = cvd.Receipt 1376 1377 // Update cast votes cache 1378 p.activeVotes.AddCastVote(v.Token, v.Ticket, v.VoteBit) 1379 1380 saveReply: 1381 // Save the reply 1382 br.replySet(v.Ticket, cvr) 1383 }(v, br) 1384 } 1385 1386 // Wait for the full ballot to be cast before returning. 1387 wg.Wait() 1388 } 1389 1390 // cmdCastBallot casts a ballot of votes. This function will not return a user 1391 // error if one occurs for an individual vote. It will instead return the 1392 // ballot reply with the error included in the individual cast vote reply. 1393 func (p *ticketVotePlugin) cmdCastBallot(token []byte, payload string) (string, error) { 1394 // Decode payload 1395 var cb ticketvote.CastBallot 1396 err := json.Unmarshal([]byte(payload), &cb) 1397 if err != nil { 1398 return "", err 1399 } 1400 votes := cb.Ballot 1401 1402 // Verify there is work to do 1403 if len(votes) == 0 { 1404 // Nothing to do 1405 cbr := ticketvote.CastBallotReply{ 1406 Receipts: []ticketvote.CastVoteReply{}, 1407 } 1408 reply, err := json.Marshal(cbr) 1409 if err != nil { 1410 return "", err 1411 } 1412 return string(reply), nil 1413 } 1414 1415 // Get the data that we need to validate the votes 1416 eligible := p.activeVotes.EligibleTickets(token) 1417 voteDetails := p.activeVotes.VoteDetails(token) 1418 bestBlock, err := p.bestBlock() 1419 if err != nil { 1420 return "", err 1421 } 1422 1423 // Perform all validation that does not require fetching the 1424 // commitment addresses. 1425 receipts := make([]ticketvote.CastVoteReply, len(votes)) 1426 for k, v := range votes { 1427 // Verify token is a valid token 1428 t, err := tokenDecode(v.Token) 1429 if err != nil { 1430 e := ticketvote.VoteErrorTokenInvalid 1431 receipts[k].Ticket = v.Ticket 1432 receipts[k].ErrorCode = &e 1433 receipts[k].ErrorContext = fmt.Sprintf("%v: not hex", 1434 ticketvote.VoteErrors[e]) 1435 continue 1436 } 1437 1438 // Verify vote token and command token are the same 1439 if !bytes.Equal(t, token) { 1440 e := ticketvote.VoteErrorMultipleRecordVotes 1441 receipts[k].Ticket = v.Ticket 1442 receipts[k].ErrorCode = &e 1443 receipts[k].ErrorContext = ticketvote.VoteErrors[e] 1444 continue 1445 } 1446 1447 // Verify vote is still active 1448 if voteDetails == nil { 1449 e := ticketvote.VoteErrorVoteStatusInvalid 1450 receipts[k].Ticket = v.Ticket 1451 receipts[k].ErrorCode = &e 1452 receipts[k].ErrorContext = fmt.Sprintf("%v: vote is "+ 1453 "not active", ticketvote.VoteErrors[e]) 1454 continue 1455 } 1456 if voteHasEnded(bestBlock, voteDetails.EndBlockHeight) { 1457 e := ticketvote.VoteErrorVoteStatusInvalid 1458 receipts[k].Ticket = v.Ticket 1459 receipts[k].ErrorCode = &e 1460 receipts[k].ErrorContext = fmt.Sprintf("%v: vote has "+ 1461 "ended", ticketvote.VoteErrors[e]) 1462 continue 1463 } 1464 1465 // Verify vote bit 1466 bit, err := strconv.ParseUint(v.VoteBit, 16, 64) 1467 if err != nil { 1468 e := ticketvote.VoteErrorVoteBitInvalid 1469 receipts[k].Ticket = v.Ticket 1470 receipts[k].ErrorCode = &e 1471 receipts[k].ErrorContext = ticketvote.VoteErrors[e] 1472 continue 1473 } 1474 err = voteBitVerify(voteDetails.Params.Options, 1475 voteDetails.Params.Mask, bit) 1476 if err != nil { 1477 e := ticketvote.VoteErrorVoteBitInvalid 1478 receipts[k].Ticket = v.Ticket 1479 receipts[k].ErrorCode = &e 1480 receipts[k].ErrorContext = fmt.Sprintf("%v: %v", 1481 ticketvote.VoteErrors[e], err) 1482 continue 1483 } 1484 1485 // Verify ticket is eligible to vote 1486 _, ok := eligible[v.Ticket] 1487 if !ok { 1488 e := ticketvote.VoteErrorTicketNotEligible 1489 receipts[k].Ticket = v.Ticket 1490 receipts[k].ErrorCode = &e 1491 receipts[k].ErrorContext = ticketvote.VoteErrors[e] 1492 continue 1493 } 1494 1495 // Verify ticket has not already voted 1496 isActive, isDup := p.activeVotes.VoteIsDuplicate(v.Token, v.Ticket) 1497 if !isActive { 1498 e := ticketvote.VoteErrorVoteStatusInvalid 1499 receipts[k].Ticket = v.Ticket 1500 receipts[k].ErrorCode = &e 1501 receipts[k].ErrorContext = fmt.Sprintf("%v: vote is "+ 1502 "not active", ticketvote.VoteErrors[e]) 1503 } 1504 if isDup { 1505 e := ticketvote.VoteErrorTicketAlreadyVoted 1506 receipts[k].Ticket = v.Ticket 1507 receipts[k].ErrorCode = &e 1508 receipts[k].ErrorContext = ticketvote.VoteErrors[e] 1509 continue 1510 } 1511 } 1512 1513 // Setup a ballotResults context. This is used to aggregate the 1514 // cast vote results when votes are cast concurrently. 1515 br := newBallotResults() 1516 1517 // Get the largest commitment address for each ticket and verify 1518 // that the vote was signed using the private key from this 1519 // address. We first check the active votes cache to see if the 1520 // commitment addresses have already been fetched. Any tickets 1521 // that are not found in the cache are fetched manually. 1522 tickets := make([]string, 0, len(cb.Ballot)) 1523 for k, v := range votes { 1524 if receipts[k].ErrorCode != nil { 1525 // Vote has an error. Skip it. 1526 continue 1527 } 1528 tickets = append(tickets, v.Ticket) 1529 } 1530 addrs := p.activeVotes.CommitmentAddrs(token, tickets) 1531 notInCache := make([]string, 0, len(tickets)) 1532 for _, v := range tickets { 1533 _, ok := addrs[v] 1534 if !ok { 1535 notInCache = append(notInCache, v) 1536 } 1537 } 1538 1539 log.Debugf("%v/%v commitment addresses found in cache", 1540 len(tickets)-len(notInCache), len(tickets)) 1541 1542 if len(notInCache) > 0 { 1543 // Get commitment addresses from dcrdata 1544 caddrs, err := p.largestCommitmentAddrs(tickets) 1545 if err != nil { 1546 return "", fmt.Errorf("largestCommitmentAddrs: %v", err) 1547 } 1548 1549 // Add addresses to the existing map 1550 for k, v := range caddrs { 1551 addrs[k] = v 1552 } 1553 } 1554 1555 // Verify the signatures 1556 for k, v := range votes { 1557 if receipts[k].ErrorCode != nil { 1558 // Vote has an error. Skip it. 1559 continue 1560 } 1561 1562 // Verify vote signature 1563 commitmentAddr, ok := addrs[v.Ticket] 1564 if !ok { 1565 t := time.Now().Unix() 1566 log.Errorf("cmdCastBallot: commitment addr not found "+ 1567 "%v: %v", t, v.Ticket) 1568 e := ticketvote.VoteErrorInternalError 1569 receipts[k].Ticket = v.Ticket 1570 receipts[k].ErrorCode = &e 1571 receipts[k].ErrorContext = fmt.Sprintf("%v: %v", 1572 ticketvote.VoteErrors[e], t) 1573 continue 1574 } 1575 if commitmentAddr.err != nil { 1576 t := time.Now().Unix() 1577 log.Errorf("cmdCastBallot: commitment addr error %v: "+ 1578 "%v %v", t, v.Ticket, commitmentAddr.err) 1579 e := ticketvote.VoteErrorInternalError 1580 receipts[k].Ticket = v.Ticket 1581 receipts[k].ErrorCode = &e 1582 receipts[k].ErrorContext = fmt.Sprintf("%v: %v", 1583 ticketvote.VoteErrors[e], t) 1584 continue 1585 } 1586 err = castVoteVerifySignature(v, commitmentAddr.addr, p.activeNetParams) 1587 if err != nil { 1588 e := ticketvote.VoteErrorSignatureInvalid 1589 receipts[k].Ticket = v.Ticket 1590 receipts[k].ErrorCode = &e 1591 receipts[k].ErrorContext = fmt.Sprintf("%v: %v", 1592 ticketvote.VoteErrors[e], err) 1593 continue 1594 } 1595 1596 // Stash the commitment address. This will be added to the 1597 // CastVoteDetails before the vote is written to disk. 1598 br.addrSet(v.Ticket, commitmentAddr.addr) 1599 } 1600 1601 // The votes that have passed validation will be cast in batches of 1602 // size batchSize. Each batch of votes is cast concurrently in order to 1603 // accommodate the trillian log signer bottleneck. The log signer picks 1604 // up queued leaves and appends them onto the trillian tree every xxx 1605 // ms, where xxx is a configurable value on the log signer, but is 1606 // typically a few hundred milliseconds. Lets use 200ms as an example. 1607 // If we don't cast the votes in batches then every vote in the ballot 1608 // will take 200 milliseconds since we wait for the leaf to be fully 1609 // appended before considering the trillian call successful. A person 1610 // casting hundreds of votes in a single ballot would cause UX issues 1611 // for all the voting clients since the backend locks the record during 1612 // any plugin write calls. Only one ballot can be cast at a time. 1613 // 1614 // The second variable that we must watch out for is the max trillian 1615 // queued leaf batch size. This is also a configurable trillian value 1616 // that represents the maximum number of leaves that can be waiting in 1617 // the queue for all trees in the trillian instance. This value is 1618 // typically around the order of magnitude of 1000s of queued leaves. 1619 // 1620 // The third variable that can cause errors is reaching the trillian 1621 // datastore max connection limits. Each vote being cast creates a 1622 // trillian connection. Overloading the trillian connections can cause 1623 // max connection exceeded errors. The max allowed connections is a 1624 // configurable trillian value, but should also be adjusted on the 1625 // key-value store database itself as well. 1626 // 1627 // This is why a vote batch size of 10 was chosen. It is large enough 1628 // to alleviate performance bottlenecks from the log signer interval, 1629 // but small enough to still allow multiple records votes to be held 1630 // concurrently without running into the queued leaf batch size limit. 1631 1632 // Prepare work 1633 var ( 1634 batchSize = 10 1635 batch = make([]ticketvote.CastVote, 0, batchSize) 1636 queue = make([][]ticketvote.CastVote, 0, 1637 len(votes)/batchSize) 1638 1639 // ballotCount is the number of votes that have passed 1640 // validation and are being cast in this ballot. 1641 ballotCount int 1642 ) 1643 for k, v := range votes { 1644 if receipts[k].ErrorCode != nil { 1645 // Vote has an error. Skip it. 1646 continue 1647 } 1648 1649 // Add vote to the current batch 1650 batch = append(batch, v) 1651 ballotCount++ 1652 1653 if len(batch) == batchSize { 1654 // This batch is full. Add the batch to the queue and 1655 // start a new batch. 1656 queue = append(queue, batch) 1657 batch = make([]ticketvote.CastVote, 0, batchSize) 1658 } 1659 } 1660 if len(batch) != 0 { 1661 // Add leftover batch to the queue 1662 queue = append(queue, batch) 1663 } 1664 1665 log.Debugf("Casting %v votes in %v batches of size %v", 1666 ballotCount, len(queue), batchSize) 1667 1668 // Cast ballot in batches 1669 for i, batch := range queue { 1670 log.Debugf("Casting %v votes in batch %v/%v", len(batch), i+1, 1671 len(queue)) 1672 1673 p.ballot(token, batch, &br) 1674 } 1675 if br.repliesLen() != ballotCount { 1676 log.Errorf("Missing results: got %v, want %v", 1677 br.repliesLen(), ballotCount) 1678 } 1679 1680 // Fill in the receipts 1681 for k, v := range votes { 1682 if receipts[k].ErrorCode != nil { 1683 // Vote has an error. Skip it. 1684 continue 1685 } 1686 cvr, ok := br.replyGet(v.Ticket) 1687 if !ok { 1688 t := time.Now().Unix() 1689 log.Errorf("cmdCastBallot: vote result not found %v: "+ 1690 "%v", t, v.Ticket) 1691 e := ticketvote.VoteErrorInternalError 1692 receipts[k].Ticket = v.Ticket 1693 receipts[k].ErrorCode = &e 1694 receipts[k].ErrorContext = fmt.Sprintf("%v: %v", 1695 ticketvote.VoteErrors[e], t) 1696 continue 1697 } 1698 1699 // Fill in receipt 1700 receipts[k] = cvr 1701 } 1702 1703 // Prepare reply 1704 cbr := ticketvote.CastBallotReply{ 1705 Receipts: receipts, 1706 } 1707 reply, err := json.Marshal(cbr) 1708 if err != nil { 1709 return "", err 1710 } 1711 1712 return string(reply), nil 1713 } 1714 1715 // cmdDetails returns the vote details for a record. 1716 func (p *ticketVotePlugin) cmdDetails(token []byte) (string, error) { 1717 // Get vote authorizations 1718 auths, err := p.auths(token) 1719 if err != nil { 1720 return "", fmt.Errorf("auths: %v", err) 1721 } 1722 1723 // Get vote details 1724 vd, err := p.voteDetails(token) 1725 if err != nil { 1726 return "", fmt.Errorf("voteDetails: %v", err) 1727 } 1728 1729 // Prepare rely 1730 dr := ticketvote.DetailsReply{ 1731 Auths: auths, 1732 Vote: vd, 1733 } 1734 reply, err := json.Marshal(dr) 1735 if err != nil { 1736 return "", err 1737 } 1738 1739 return string(reply), nil 1740 } 1741 1742 // cmdRunoffDetails is an internal plugin command that requests the details of 1743 // a runoff vote. 1744 func (p *ticketVotePlugin) cmdRunoffDetails(token []byte) (string, error) { 1745 // Get start runoff record 1746 srs, err := p.startRunoffRecord(token) 1747 if err != nil { 1748 return "", err 1749 } 1750 1751 // Prepare reply 1752 r := runoffDetailsReply{ 1753 Runoff: *srs, 1754 } 1755 reply, err := json.Marshal(r) 1756 if err != nil { 1757 return "", err 1758 } 1759 1760 return string(reply), nil 1761 } 1762 1763 // cmdResults requests the vote objects of all votes that were cast in a ticket 1764 // vote. 1765 func (p *ticketVotePlugin) cmdResults(token []byte) (string, error) { 1766 // Get vote results 1767 votes, err := p.voteResults(token) 1768 if err != nil { 1769 return "", err 1770 } 1771 1772 // Prepare reply 1773 rr := ticketvote.ResultsReply{ 1774 Votes: votes, 1775 } 1776 reply, err := json.Marshal(rr) 1777 if err != nil { 1778 return "", err 1779 } 1780 1781 return string(reply), nil 1782 } 1783 1784 // cmdSummary requests the vote summary for a record. 1785 func (p *ticketVotePlugin) cmdSummary(token []byte) (string, error) { 1786 // Get best block. This cmd does not write any data so we do not 1787 // have to use the safe best block. 1788 bb, err := p.bestBlockUnsafe() 1789 if err != nil { 1790 return "", fmt.Errorf("bestBlockUnsafe: %v", err) 1791 } 1792 1793 // Get summary 1794 sr, err := p.summary(token, bb) 1795 if err != nil { 1796 return "", fmt.Errorf("summary: %v", err) 1797 } 1798 1799 // Prepare reply 1800 reply, err := json.Marshal(sr) 1801 if err != nil { 1802 return "", err 1803 } 1804 1805 return string(reply), nil 1806 } 1807 1808 // cmdInventory requests a page of tokens for the provided status. If no status 1809 // is provided then a page for each status will be returned. 1810 func (p *ticketVotePlugin) cmdInventory(payload string) (string, error) { 1811 var i ticketvote.Inventory 1812 err := json.Unmarshal([]byte(payload), &i) 1813 if err != nil { 1814 return "", err 1815 } 1816 1817 // Get the best block. This command does not write 1818 // any data so we can use the unsafe best block. 1819 bestBlock, err := p.bestBlockUnsafe() 1820 if err != nil { 1821 return "", err 1822 } 1823 1824 // Get the inventory 1825 tokens := make(map[string][]string, 256) 1826 switch i.Status { 1827 case ticketvote.VoteStatusInvalid: 1828 // No vote status was provided. Return a 1829 // page of results for all vote statuses. 1830 inv, err := p.inv.GetPage(bestBlock) 1831 if err != nil { 1832 return "", err 1833 } 1834 for status, entries := range inv.Entries { 1835 statusStr := ticketvote.VoteStatuses[status] 1836 tokens[statusStr] = entryTokens(entries) 1837 } 1838 1839 default: 1840 // A vote status was provided. Return a page of results for the 1841 // provided status. 1842 entries, err := p.inv.GetPageForStatus(bestBlock, i.Status, i.Page) 1843 if err != nil { 1844 return "", err 1845 } 1846 statusStr := ticketvote.VoteStatuses[i.Status] 1847 tokens[statusStr] = entryTokens(entries) 1848 } 1849 1850 // Prepare the reply 1851 ir := ticketvote.InventoryReply{ 1852 Tokens: tokens, 1853 BestBlock: bestBlock, 1854 } 1855 reply, err := json.Marshal(ir) 1856 if err != nil { 1857 return "", err 1858 } 1859 1860 return string(reply), nil 1861 } 1862 1863 // cmdTimestamps requests the timestamps for a ticket vote. 1864 func (p *ticketVotePlugin) cmdTimestamps(token []byte, payload string) (string, error) { 1865 // Decode payload 1866 var t ticketvote.Timestamps 1867 err := json.Unmarshal([]byte(payload), &t) 1868 if err != nil { 1869 return "", err 1870 } 1871 1872 var ( 1873 auths = make([]ticketvote.Timestamp, 0, 32) 1874 details *ticketvote.Timestamp 1875 1876 pageSize = p.timestampsPageSize 1877 votes = make([]ticketvote.Timestamp, 0, pageSize) 1878 ) 1879 switch { 1880 case t.VotesPage > 0: 1881 // Return a page of vote timestamps 1882 1883 // Look for final vote timestamps in the key-value cache 1884 cachedVotes, err := p.cachedVoteTimestamps(token, t.VotesPage, pageSize) 1885 if err != nil { 1886 return "", err 1887 } 1888 1889 // Get all cast vote digests from tstore 1890 digests, err := p.tstore.DigestsByDataDesc(token, 1891 []string{dataDescriptorCastVoteDetails}) 1892 if err != nil { 1893 return "", fmt.Errorf("digestsByKeyPrefix %x %v: %v", 1894 token, dataDescriptorVoteDetails, err) 1895 } 1896 1897 startAt := (t.VotesPage - 1) * pageSize 1898 for i, v := range digests { 1899 if i < int(startAt) { 1900 continue 1901 } 1902 1903 // Check if current digest timestamp already exists in cache 1904 var foundInCache bool 1905 for _, t := range cachedVotes { 1906 if t.Digest == hex.EncodeToString(v) { 1907 // Digest timestamp found, collect it 1908 votes = append(votes, t) 1909 foundInCache = true 1910 break 1911 } 1912 } 1913 // If digest was found in cache, continue to next digest 1914 if foundInCache { 1915 continue 1916 } 1917 1918 // Digest was not found in cache, get timestamp 1919 ts, err := p.timestamp(token, v) 1920 if err != nil { 1921 return "", fmt.Errorf("timestamp %x %x: %v", 1922 token, v, err) 1923 } 1924 votes = append(votes, *ts) 1925 1926 if len(votes) == int(pageSize) { 1927 // We have a full page. We're done. 1928 break 1929 } 1930 } 1931 1932 // Cache final vote timestamps 1933 err = p.cacheFinalVoteTimestamps(token, votes, t.VotesPage) 1934 if err != nil { 1935 return "", err 1936 } 1937 1938 default: 1939 // Return authorization timestamps and the vote details 1940 // timestamp. 1941 1942 // Auth timestamps 1943 1944 // Look for final auth timestamps in the key-value cache 1945 cachedAuths, err := p.cachedAuthTimestamps(token) 1946 if err != nil { 1947 return "", err 1948 } 1949 1950 // Get all auth digests from tstore 1951 digests, err := p.tstore.DigestsByDataDesc(token, 1952 []string{dataDescriptorAuthDetails}) 1953 if err != nil { 1954 return "", fmt.Errorf("DigestByDataDesc %x %v: %v", 1955 token, dataDescriptorAuthDetails, err) 1956 } 1957 auths = make([]ticketvote.Timestamp, 0, len(digests)) 1958 for _, v := range digests { 1959 // Check if current digest timestamp already exists in cache 1960 var foundInCache bool 1961 for _, t := range cachedAuths { 1962 if t.Digest == hex.EncodeToString(v) { 1963 // Digest timestamp found, collect it 1964 auths = append(auths, t) 1965 foundInCache = true 1966 break 1967 } 1968 } 1969 // If digest was found in cache, continue to next digest 1970 if foundInCache { 1971 continue 1972 } 1973 1974 // Digest was not found in cache, get timestamp 1975 ts, err := p.timestamp(token, v) 1976 if err != nil { 1977 return "", fmt.Errorf("timestamp %x %x: %v", 1978 token, v, err) 1979 } 1980 auths = append(auths, *ts) 1981 } 1982 1983 // Cache final auth timestamps 1984 err = p.cacheFinalAuthTimestamps(token, auths) 1985 if err != nil { 1986 return "", err 1987 } 1988 1989 // Vote details timestamp 1990 1991 // Look for final vote details timestamp in the key-value cache 1992 cachedDetails, err := p.cachedDetailsTimestamp(token) 1993 if err != nil { 1994 return "", err 1995 } 1996 1997 // Get vote details digests from tstore 1998 digests, err = p.tstore.DigestsByDataDesc(token, 1999 []string{dataDescriptorVoteDetails}) 2000 if err != nil { 2001 return "", fmt.Errorf("DigestsByDataDesc %x %v: %v", 2002 token, dataDescriptorVoteDetails, err) 2003 } 2004 // There should never be more than a one vote details 2005 if len(digests) > 1 { 2006 return "", fmt.Errorf("invalid vote details count: "+ 2007 "got %v, want 1", len(digests)) 2008 } 2009 for _, v := range digests { 2010 // Check if vote details digest timestamp already exists in cache 2011 switch { 2012 case cachedDetails != nil: 2013 if cachedDetails.Digest == hex.EncodeToString(v) { 2014 // Digest timestamp found, collect it 2015 details = cachedDetails 2016 } 2017 2018 case cachedDetails == nil: 2019 // Vote details timestamp was not found in cache, get timestamp 2020 ts, err := p.timestamp(token, v) 2021 if err != nil { 2022 return "", fmt.Errorf("timestamp %x %x: %v", 2023 token, v, err) 2024 } 2025 details = ts 2026 } 2027 } 2028 2029 // Cache final vote details timestamp 2030 if details != nil { 2031 err = p.cacheFinalDetailsTimestamp(token, *details) 2032 if err != nil { 2033 return "", err 2034 } 2035 } 2036 } 2037 2038 // Prepare reply 2039 tr := ticketvote.TimestampsReply{ 2040 Auths: auths, 2041 Details: details, 2042 Votes: votes, 2043 } 2044 reply, err := json.Marshal(tr) 2045 if err != nil { 2046 return "", err 2047 } 2048 2049 return string(reply), nil 2050 } 2051 2052 // Submissions requests the submissions of a runoff vote. The only records that 2053 // will have a submissions list are the parent records in a runoff vote. The 2054 // list will contain all public runoff vote submissions, i.e. records that have 2055 // linked to the parent record using the VoteMetadata.LinkTo field. 2056 func (p *ticketVotePlugin) cmdSubmissions(token []byte) (string, error) { 2057 // Get submissions list 2058 s, err := p.subs.Get(tokenEncode(token)) 2059 if err != nil { 2060 return "", err 2061 } 2062 2063 // Prepare reply 2064 tokens := make([]string, 0, len(s.Tokens)) 2065 for k := range s.Tokens { 2066 tokens = append(tokens, k) 2067 } 2068 sr := ticketvote.SubmissionsReply{ 2069 Submissions: tokens, 2070 } 2071 reply, err := json.Marshal(sr) 2072 if err != nil { 2073 return "", err 2074 } 2075 2076 return string(reply), nil 2077 } 2078 2079 // authSave saves a AuthDetails to the backend. 2080 func (p *ticketVotePlugin) authSave(token []byte, ad ticketvote.AuthDetails) error { 2081 // Prepare blob 2082 be, err := convertBlobEntryFromAuthDetails(ad) 2083 if err != nil { 2084 return err 2085 } 2086 2087 // Save blob 2088 return p.tstore.BlobSave(token, *be) 2089 } 2090 2091 // auths returns all AuthDetails for a record. 2092 func (p *ticketVotePlugin) auths(token []byte) ([]ticketvote.AuthDetails, error) { 2093 // Retrieve blobs 2094 blobs, err := p.tstore.BlobsByDataDesc(token, 2095 []string{dataDescriptorAuthDetails}) 2096 if err != nil { 2097 return nil, err 2098 } 2099 2100 // Decode blobs 2101 auths := make([]ticketvote.AuthDetails, 0, len(blobs)) 2102 for _, v := range blobs { 2103 a, err := convertAuthDetailsFromBlobEntry(v) 2104 if err != nil { 2105 return nil, err 2106 } 2107 auths = append(auths, *a) 2108 } 2109 2110 // Sanity check. They should already be sorted from oldest to 2111 // newest. 2112 sort.SliceStable(auths, func(i, j int) bool { 2113 return auths[i].Timestamp < auths[j].Timestamp 2114 }) 2115 2116 return auths, nil 2117 } 2118 2119 // voteDetailsSave saves a VoteDetails to the backend. 2120 func (p *ticketVotePlugin) voteDetailsSave(token []byte, vd ticketvote.VoteDetails) error { 2121 // Prepare blob 2122 be, err := convertBlobEntryFromVoteDetails(vd) 2123 if err != nil { 2124 return err 2125 } 2126 2127 // Save blob 2128 return p.tstore.BlobSave(token, *be) 2129 } 2130 2131 // voteDetails returns the VoteDetails for a record. Nil is returned if a vote 2132 // details is not found. 2133 func (p *ticketVotePlugin) voteDetails(token []byte) (*ticketvote.VoteDetails, error) { 2134 // Retrieve blobs 2135 blobs, err := p.tstore.BlobsByDataDesc(token, 2136 []string{dataDescriptorVoteDetails}) 2137 if err != nil { 2138 return nil, err 2139 } 2140 switch len(blobs) { 2141 case 0: 2142 // A vote details does not exist 2143 return nil, nil 2144 case 1: 2145 // A vote details exists; continue 2146 default: 2147 // This should not happen. There should only ever be a max of 2148 // one vote details. 2149 return nil, fmt.Errorf("multiple vote details found (%v) on %x", 2150 len(blobs), token) 2151 } 2152 2153 // Decode blob 2154 vd, err := convertVoteDetailsFromBlobEntry(blobs[0]) 2155 if err != nil { 2156 return nil, err 2157 } 2158 2159 return vd, nil 2160 } 2161 2162 // voteDetailsByToken returns the VoteDetails for a record. Nil is returned 2163 // if the vote details are not found. 2164 func (p *ticketVotePlugin) voteDetailsByToken(token []byte) (*ticketvote.VoteDetails, error) { 2165 reply, err := p.backend.PluginRead(token, ticketvote.PluginID, 2166 ticketvote.CmdDetails, "") 2167 if err != nil { 2168 return nil, err 2169 } 2170 var dr ticketvote.DetailsReply 2171 err = json.Unmarshal([]byte(reply), &dr) 2172 if err != nil { 2173 return nil, err 2174 } 2175 return dr.Vote, nil 2176 } 2177 2178 // voteResults returns all votes that were cast in a ticket vote. 2179 func (p *ticketVotePlugin) voteResults(token []byte) ([]ticketvote.CastVoteDetails, error) { 2180 // Retrieve blobs 2181 desc := []string{ 2182 dataDescriptorCastVoteDetails, 2183 dataDescriptorVoteCollider, 2184 } 2185 blobs, err := p.tstore.BlobsByDataDesc(token, desc) 2186 if err != nil { 2187 return nil, err 2188 } 2189 2190 // Decode blobs. A cast vote is considered valid only if the vote 2191 // collider exists for it. If there are multiple votes using the same 2192 // ticket, the valid vote is the one that immediately precedes the vote 2193 // collider blob entry. 2194 var ( 2195 // map[ticket]CastVoteDetails 2196 votes = make(map[string]ticketvote.CastVoteDetails, len(blobs)) 2197 2198 // map[ticket][]index 2199 voteIndexes = make(map[string][]int, len(blobs)) 2200 2201 // map[ticket]index 2202 colliderIndexes = make(map[string]int, len(blobs)) 2203 ) 2204 for i, v := range blobs { 2205 // Decode data hint 2206 b, err := base64.StdEncoding.DecodeString(v.DataHint) 2207 if err != nil { 2208 return nil, err 2209 } 2210 var dd store.DataDescriptor 2211 err = json.Unmarshal(b, &dd) 2212 if err != nil { 2213 return nil, err 2214 } 2215 switch dd.Descriptor { 2216 case dataDescriptorCastVoteDetails: 2217 // Decode cast vote 2218 cv, err := convertCastVoteDetailsFromBlobEntry(v) 2219 if err != nil { 2220 return nil, err 2221 } 2222 2223 // Save index of the cast vote 2224 idx, ok := voteIndexes[cv.Ticket] 2225 if !ok { 2226 idx = make([]int, 0, 32) 2227 } 2228 idx = append(idx, i) 2229 voteIndexes[cv.Ticket] = idx 2230 2231 // Save the cast vote 2232 votes[cv.Ticket] = *cv 2233 2234 case dataDescriptorVoteCollider: 2235 // Decode vote collider 2236 vc, err := convertVoteColliderFromBlobEntry(v) 2237 if err != nil { 2238 return nil, err 2239 } 2240 2241 // Sanity check 2242 _, ok := colliderIndexes[vc.Ticket] 2243 if ok { 2244 return nil, fmt.Errorf("duplicate vote "+ 2245 "colliders found %v", vc.Ticket) 2246 } 2247 2248 // Save the ticket and index for the collider 2249 colliderIndexes[vc.Ticket] = i 2250 2251 default: 2252 return nil, fmt.Errorf("invalid data descriptor: %v", 2253 dd.Descriptor) 2254 } 2255 } 2256 2257 for ticket, indexes := range voteIndexes { 2258 // Remove any votes that do not have a collider blob 2259 colliderIndex, ok := colliderIndexes[ticket] 2260 if !ok { 2261 // This is not a valid vote 2262 delete(votes, ticket) 2263 continue 2264 } 2265 2266 // If multiple votes have been cast using the same ticket then 2267 // we must manually determine which vote is valid. 2268 if len(indexes) == 1 { 2269 // Only one cast vote exists for this ticket. This is 2270 // good. 2271 continue 2272 } 2273 2274 // Sanity check 2275 if len(indexes) == 0 { 2276 return nil, fmt.Errorf("no cast vote index found %v", 2277 ticket) 2278 } 2279 2280 log.Tracef("Multiple votes found for a single vote collider %v", 2281 ticket) 2282 2283 // Multiple votes exist for this ticket. The vote that is valid 2284 // is the one that immediately precedes the vote collider. 2285 // Start at the end of the vote indexes and find the first vote 2286 // index that precedes the collider index. 2287 var validVoteIndex int 2288 for i := len(indexes) - 1; i >= 0; i-- { 2289 voteIndex := indexes[i] 2290 if voteIndex < colliderIndex { 2291 // This is the valid vote 2292 validVoteIndex = voteIndex 2293 break 2294 } 2295 } 2296 2297 // Save the valid vote 2298 b := blobs[validVoteIndex] 2299 cv, err := convertCastVoteDetailsFromBlobEntry(b) 2300 if err != nil { 2301 return nil, err 2302 } 2303 votes[cv.Ticket] = *cv 2304 } 2305 2306 // Put votes into an array 2307 cvotes := make([]ticketvote.CastVoteDetails, 0, len(blobs)) 2308 for _, v := range votes { 2309 cvotes = append(cvotes, v) 2310 } 2311 2312 // Sort by ticket hash 2313 sort.SliceStable(cvotes, func(i, j int) bool { 2314 return cvotes[i].Ticket < cvotes[j].Ticket 2315 }) 2316 2317 return cvotes, nil 2318 } 2319 2320 // voteOptionResults tallies the results of a ticket vote and returns a 2321 // VoteOptionResult for each vote option in the ticket vote. 2322 func (p *ticketVotePlugin) voteOptionResults(token []byte, options []ticketvote.VoteOption) ([]ticketvote.VoteOptionResult, error) { 2323 // Ongoing votes will have the cast votes cached. Calculate the results 2324 // using the cached votes if we can since it will be much faster. 2325 var ( 2326 tally = make(map[string]uint32, len(options)) 2327 t = hex.EncodeToString(token) 2328 ctally = p.activeVotes.Tally(t) 2329 ) 2330 switch { 2331 case len(ctally) > 0: 2332 // Votes are in the cache. Use the cached results. 2333 tally = ctally 2334 2335 default: 2336 // Votes are not in the cache. Pull them from the backend. 2337 reply, err := p.backend.PluginRead(token, ticketvote.PluginID, 2338 ticketvote.CmdResults, "") 2339 if err != nil { 2340 return nil, err 2341 } 2342 var rr ticketvote.ResultsReply 2343 err = json.Unmarshal([]byte(reply), &rr) 2344 if err != nil { 2345 return nil, err 2346 } 2347 2348 // Tally the results 2349 for _, v := range rr.Votes { 2350 tally[v.VoteBit]++ 2351 } 2352 } 2353 2354 // Prepare reply 2355 results := make([]ticketvote.VoteOptionResult, 0, len(options)) 2356 for _, v := range options { 2357 bit := strconv.FormatUint(v.Bit, 16) 2358 results = append(results, ticketvote.VoteOptionResult{ 2359 ID: v.ID, 2360 Description: v.Description, 2361 VoteBit: v.Bit, 2362 Votes: uint64(tally[bit]), 2363 }) 2364 } 2365 2366 return results, nil 2367 } 2368 2369 // voteSummariesForRunoff calculates and returns the vote summaries of all 2370 // submissions in a runoff vote. This should only be called once the vote has 2371 // finished. 2372 func (p *ticketVotePlugin) summariesForRunoff(parentToken string) (map[string]ticketvote.SummaryReply, error) { 2373 // Get runoff vote details 2374 parent, err := tokenDecode(parentToken) 2375 if err != nil { 2376 return nil, err 2377 } 2378 reply, err := p.backend.PluginRead(parent, ticketvote.PluginID, 2379 cmdRunoffDetails, "") 2380 if err != nil { 2381 return nil, fmt.Errorf("PluginRead %x %v %v: %v", 2382 parent, ticketvote.PluginID, cmdRunoffDetails, err) 2383 } 2384 var rdr runoffDetailsReply 2385 err = json.Unmarshal([]byte(reply), &rdr) 2386 if err != nil { 2387 return nil, err 2388 } 2389 2390 // Verify submissions exist 2391 subs := rdr.Runoff.Submissions 2392 if len(subs) == 0 { 2393 return map[string]ticketvote.SummaryReply{}, nil 2394 } 2395 2396 // Compile summaries for all submissions 2397 var ( 2398 summaries = make(map[string]ticketvote.SummaryReply, 2399 len(subs)) 2400 2401 // Net number of approve votes of the winner 2402 winnerNetApprove int 2403 2404 // Token of the winner 2405 winnerToken string 2406 ) 2407 for _, v := range subs { 2408 token, err := tokenDecode(v) 2409 if err != nil { 2410 return nil, err 2411 } 2412 2413 // Get vote details 2414 vd, err := p.voteDetailsByToken(token) 2415 if err != nil { 2416 return nil, err 2417 } 2418 2419 // Get vote options results 2420 results, err := p.voteOptionResults(token, vd.Params.Options) 2421 if err != nil { 2422 return nil, err 2423 } 2424 2425 // Add summary to the reply 2426 s := ticketvote.SummaryReply{ 2427 Type: vd.Params.Type, 2428 Status: ticketvote.VoteStatusRejected, 2429 Duration: vd.Params.Duration, 2430 StartBlockHeight: vd.StartBlockHeight, 2431 StartBlockHash: vd.StartBlockHash, 2432 EndBlockHeight: vd.EndBlockHeight, 2433 EligibleTickets: uint32(len(vd.EligibleTickets)), 2434 QuorumPercentage: vd.Params.QuorumPercentage, 2435 PassPercentage: vd.Params.PassPercentage, 2436 Results: results, 2437 } 2438 summaries[v] = s 2439 2440 // We now check if this record has the most net yes votes. 2441 2442 // Verify the vote met quorum and pass requirements 2443 approved := voteIsApproved(*vd, results) 2444 if !approved { 2445 // Vote did not meet quorum and pass requirements. 2446 // Nothing else to do. Record vote is not approved. 2447 continue 2448 } 2449 2450 // Check if this record has more net approved votes then 2451 // current highest. 2452 var ( 2453 votesApprove uint64 // Number of approve votes 2454 votesReject uint64 // Number of reject votes 2455 ) 2456 for _, vor := range s.Results { 2457 switch vor.ID { 2458 case ticketvote.VoteOptionIDApprove: 2459 votesApprove = vor.Votes 2460 case ticketvote.VoteOptionIDReject: 2461 votesReject = vor.Votes 2462 default: 2463 // Runoff vote options can only be 2464 // approve/reject 2465 return nil, fmt.Errorf("unknown runoff vote "+ 2466 "option %v", vor.ID) 2467 } 2468 2469 netApprove := int(votesApprove) - int(votesReject) 2470 if netApprove > winnerNetApprove { 2471 // New winner! 2472 winnerToken = v 2473 winnerNetApprove = netApprove 2474 } 2475 2476 // This function doesn't handle the unlikely case that 2477 // the runoff vote results in a tie. If this happens 2478 // then we need to have a debate about how this should 2479 // be handled before implementing anything. The cached 2480 // vote summary would need to be removed and recreated 2481 // using whatever methodology is decided upon. 2482 } 2483 } 2484 if winnerToken != "" { 2485 // A winner was found. Mark their summary as approved. 2486 s := summaries[winnerToken] 2487 s.Status = ticketvote.VoteStatusApproved 2488 summaries[winnerToken] = s 2489 } 2490 2491 return summaries, nil 2492 } 2493 2494 // summary returns the vote summary for a record. 2495 func (p *ticketVotePlugin) summary(tokenB []byte, bestBlock uint32) (*ticketvote.SummaryReply, error) { 2496 // Check if a vote summary exists in the cache for 2497 // this record. Summaries are only cached once the 2498 // voting period for the record has ended. 2499 token := tokenEncode(tokenB) 2500 s, err := p.summaries.Get(token) 2501 switch { 2502 case err == nil: 2503 // A cached summary was found for the record. 2504 // Update the summary's best block and return 2505 // it. 2506 s.BestBlock = bestBlock 2507 return s, nil 2508 2509 case errors.Is(err, errSummaryNotFound): 2510 // A cached summary was not found for the record. 2511 // We must build it from scratch. Continue below. 2512 2513 case err != nil: 2514 // All other errors 2515 return nil, err 2516 } 2517 2518 // Build the vote summary from scratch. We will need 2519 // to pull various pieces of record data to do this, 2520 // starting with the abridged record. 2521 r, err := p.recordAbridged(tokenB) 2522 if err != nil { 2523 return nil, err 2524 } 2525 2526 // timestamp contains the timestamp of the most recent 2527 // vote status change. 2528 // 2529 // The timestamp for the unauthorized vote status and 2530 // the ineligible vote status will be the timestamp of 2531 // the record status change associated with that vote 2532 // status. 2533 timestamp := r.RecordMetadata.Timestamp 2534 2535 // Verify that the record is eligble for a vote. Only 2536 // public proposals can be voted on. 2537 if r.RecordMetadata.Status != backend.StatusPublic { 2538 return &ticketvote.SummaryReply{ 2539 Status: ticketvote.VoteStatusIneligible, 2540 Timestamp: timestamp, 2541 Results: []ticketvote.VoteOptionResult{}, 2542 BestBlock: bestBlock, 2543 }, nil 2544 } 2545 2546 // Assume the vote status is unauthorized. The vote 2547 // status is only updated when the appropriate data 2548 // has been found that proves otherwise. 2549 status := ticketvote.VoteStatusUnauthorized 2550 2551 // Check if the voting period has been authorized. 2552 // 2553 // Not all vote types require an authorization. For example, 2554 // RFP submissions do not require an authorization prior to 2555 // the runoff vote being started. 2556 auths, err := p.auths(tokenB) 2557 if err != nil { 2558 return nil, err 2559 } 2560 if len(auths) > 0 { 2561 lastAuth := auths[len(auths)-1] 2562 switch ticketvote.AuthActionT(lastAuth.Action) { 2563 case ticketvote.AuthActionAuthorize: 2564 // The vote has been authorized. Continue below 2565 // to see if the voting period has been started. 2566 status = ticketvote.VoteStatusAuthorized 2567 2568 case ticketvote.AuthActionRevoke: 2569 // The vote authorization has been revoked. It's 2570 // not possible for the vote to have been started. 2571 // We can stop looking. 2572 return &ticketvote.SummaryReply{ 2573 Status: status, 2574 Timestamp: lastAuth.Timestamp, 2575 Results: []ticketvote.VoteOptionResult{}, 2576 BestBlock: bestBlock, 2577 }, nil 2578 } 2579 } 2580 2581 // Check if the vote has been started 2582 vd, err := p.voteDetails(tokenB) 2583 if err != nil { 2584 return nil, err 2585 } 2586 if vd == nil { 2587 // Vote has not been started yet 2588 return &ticketvote.SummaryReply{ 2589 Status: status, 2590 Timestamp: timestamp, 2591 Results: []ticketvote.VoteOptionResult{}, 2592 BestBlock: bestBlock, 2593 }, nil 2594 } 2595 2596 // A vote details exists which means the voting period 2597 // has been started. We need to check the vote results 2598 // and if the vote has ended yet. 2599 status = ticketvote.VoteStatusStarted 2600 2601 // Tally the vote results 2602 results, err := p.voteOptionResults(tokenB, vd.Params.Options) 2603 if err != nil { 2604 return nil, err 2605 } 2606 2607 // Prepare the vote summary 2608 summary := ticketvote.SummaryReply{ 2609 Type: vd.Params.Type, 2610 Status: status, 2611 Duration: vd.Params.Duration, 2612 StartBlockHeight: vd.StartBlockHeight, 2613 StartBlockHash: vd.StartBlockHash, 2614 EndBlockHeight: vd.EndBlockHeight, 2615 EligibleTickets: uint32(len(vd.EligibleTickets)), 2616 QuorumPercentage: vd.Params.QuorumPercentage, 2617 PassPercentage: vd.Params.PassPercentage, 2618 Results: results, 2619 BestBlock: bestBlock, 2620 } 2621 2622 // If the vote has not finished yet then we are done for now. 2623 if !voteHasEnded(bestBlock, vd.EndBlockHeight) { 2624 return &summary, nil 2625 } 2626 2627 // The vote has finished. Determine the vote result and 2628 // save the vote summary to the cache. 2629 switch vd.Params.Type { 2630 case ticketvote.VoteTypeStandard: 2631 // Standard votes use a simple approve/reject result 2632 if voteIsApproved(*vd, results) { 2633 summary.Status = ticketvote.VoteStatusApproved 2634 } else { 2635 summary.Status = ticketvote.VoteStatusRejected 2636 } 2637 2638 // Save the summary to the cache 2639 err = p.summaries.Save(token, summary) 2640 if err != nil { 2641 return nil, err 2642 } 2643 2644 // Remove the record from the active votes cache 2645 p.activeVotes.Del(vd.Params.Token) 2646 2647 case ticketvote.VoteTypeRunoff: 2648 // A runoff vote requires that we pull all other runoff 2649 // vote submissions to determine if the vote passed. 2650 summaries, err := p.summariesForRunoff(vd.Params.Parent) 2651 if err != nil { 2652 return nil, err 2653 } 2654 for k, v := range summaries { 2655 // Save the summary to the cache 2656 err = p.summaries.Save(k, v) 2657 if err != nil { 2658 return nil, err 2659 } 2660 2661 // Remove the record from the active votes cache 2662 p.activeVotes.Del(k) 2663 } 2664 2665 summary = summaries[vd.Params.Token] 2666 2667 default: 2668 return nil, errors.Errorf("unknown vote type") 2669 } 2670 2671 return &summary, nil 2672 } 2673 2674 // summaryByToken returns the vote summary for a record. 2675 func (p *ticketVotePlugin) summaryByToken(token []byte) (*ticketvote.SummaryReply, error) { 2676 reply, err := p.backend.PluginRead(token, ticketvote.PluginID, 2677 ticketvote.CmdSummary, "") 2678 if err != nil { 2679 return nil, fmt.Errorf("PluginRead %x %v %v: %v", 2680 token, ticketvote.PluginID, ticketvote.CmdSummary, err) 2681 } 2682 var sr ticketvote.SummaryReply 2683 err = json.Unmarshal([]byte(reply), &sr) 2684 if err != nil { 2685 return nil, err 2686 } 2687 return &sr, nil 2688 } 2689 2690 // timestamp returns the timestamp for a specific piece of data. 2691 func (p *ticketVotePlugin) timestamp(token []byte, digest []byte) (*ticketvote.Timestamp, error) { 2692 t, err := p.tstore.Timestamp(token, digest) 2693 if err != nil { 2694 return nil, fmt.Errorf("timestamp %x %x: %v", 2695 token, digest, err) 2696 } 2697 2698 // Convert response 2699 proofs := make([]ticketvote.Proof, 0, len(t.Proofs)) 2700 for _, v := range t.Proofs { 2701 proofs = append(proofs, ticketvote.Proof{ 2702 Type: v.Type, 2703 Digest: v.Digest, 2704 MerkleRoot: v.MerkleRoot, 2705 MerklePath: v.MerklePath, 2706 ExtraData: v.ExtraData, 2707 }) 2708 } 2709 return &ticketvote.Timestamp{ 2710 Data: t.Data, 2711 Digest: t.Digest, 2712 TxID: t.TxID, 2713 MerkleRoot: t.MerkleRoot, 2714 Proofs: proofs, 2715 }, nil 2716 } 2717 2718 // recordAbridged returns a record where the only record file returned is the 2719 // vote metadata file if one exists. 2720 func (p *ticketVotePlugin) recordAbridged(token []byte) (*backend.Record, error) { 2721 reqs := []backend.RecordRequest{ 2722 { 2723 Token: token, 2724 Filenames: []string{ 2725 ticketvote.FileNameVoteMetadata, 2726 }, 2727 }, 2728 } 2729 rs, err := p.backend.Records(reqs) 2730 if err != nil { 2731 return nil, err 2732 } 2733 r, ok := rs[hex.EncodeToString(token)] 2734 if !ok { 2735 return nil, backend.ErrRecordNotFound 2736 } 2737 return &r, nil 2738 } 2739 2740 // bestBlock fetches the best block from the dcrdata plugin and returns it. If 2741 // the dcrdata connection is not active, an error will be returned. 2742 func (p *ticketVotePlugin) bestBlock() (uint32, error) { 2743 // Get best block 2744 payload, err := json.Marshal(dcrdata.BestBlock{}) 2745 if err != nil { 2746 return 0, err 2747 } 2748 reply, err := p.backend.PluginRead(nil, dcrdata.PluginID, 2749 dcrdata.CmdBestBlock, string(payload)) 2750 if err != nil { 2751 return 0, fmt.Errorf("PluginRead %v %v: %v", 2752 dcrdata.PluginID, dcrdata.CmdBestBlock, err) 2753 } 2754 2755 // Handle response 2756 var bbr dcrdata.BestBlockReply 2757 err = json.Unmarshal([]byte(reply), &bbr) 2758 if err != nil { 2759 return 0, err 2760 } 2761 if bbr.Status != dcrdata.StatusConnected { 2762 // The dcrdata connection is down. The best block cannot be 2763 // trusted as being accurate. 2764 return 0, fmt.Errorf("dcrdata connection is down") 2765 } 2766 if bbr.Height == 0 { 2767 return 0, fmt.Errorf("invalid best block height 0") 2768 } 2769 2770 return bbr.Height, nil 2771 } 2772 2773 // bestBlockUnsafe fetches the best block from the dcrdata plugin and returns 2774 // it. If the dcrdata connection is not active, an error WILL NOT be returned. 2775 // The dcrdata cached best block height will be returned even though it may be 2776 // stale. Use bestBlock() if the caller requires a guarantee that the best 2777 // block is not stale. 2778 func (p *ticketVotePlugin) bestBlockUnsafe() (uint32, error) { 2779 // Get best block 2780 payload, err := json.Marshal(dcrdata.BestBlock{}) 2781 if err != nil { 2782 return 0, err 2783 } 2784 reply, err := p.backend.PluginRead(nil, dcrdata.PluginID, 2785 dcrdata.CmdBestBlock, string(payload)) 2786 if err != nil { 2787 return 0, fmt.Errorf("PluginRead %v %v: %v", 2788 dcrdata.PluginID, dcrdata.CmdBestBlock, err) 2789 } 2790 2791 // Handle response 2792 var bbr dcrdata.BestBlockReply 2793 err = json.Unmarshal([]byte(reply), &bbr) 2794 if err != nil { 2795 return 0, err 2796 } 2797 if bbr.Height == 0 { 2798 return 0, fmt.Errorf("invalid best block height 0") 2799 } 2800 2801 return bbr.Height, nil 2802 } 2803 2804 // voteHasEnded returns whether the vote has ended. 2805 func voteHasEnded(bestBlock, endHeight uint32) bool { 2806 return bestBlock >= endHeight 2807 } 2808 2809 // voteIsApproved returns whether the provided vote option results met the 2810 // provided quorum and pass percentage requirements. This function can only be 2811 // called on votes that use VoteOptionIDApprove and VoteOptionIDReject. Any 2812 // other vote option IDs will cause this function to panic. 2813 func voteIsApproved(vd ticketvote.VoteDetails, results []ticketvote.VoteOptionResult) bool { 2814 // Tally the total votes 2815 var total uint64 2816 for _, v := range results { 2817 total += v.Votes 2818 } 2819 2820 // Calculate required thresholds 2821 var ( 2822 eligible = float64(len(vd.EligibleTickets)) 2823 quorumPerc = float64(vd.Params.QuorumPercentage) 2824 passPerc = float64(vd.Params.PassPercentage) 2825 quorum = uint64(quorumPerc / 100 * eligible) 2826 pass = uint64(passPerc / 100 * float64(total)) 2827 2828 approvedVotes uint64 2829 ) 2830 2831 // Tally approve votes 2832 for _, v := range results { 2833 switch v.ID { 2834 case ticketvote.VoteOptionIDApprove: 2835 // Valid vote option 2836 approvedVotes = v.Votes 2837 case ticketvote.VoteOptionIDReject: 2838 // Valid vote option 2839 default: 2840 // Invalid vote option 2841 e := fmt.Sprintf("invalid vote option id found: %v", 2842 v.ID) 2843 panic(e) 2844 } 2845 } 2846 2847 // Check tally against thresholds 2848 var approved bool 2849 switch { 2850 case total < quorum: 2851 // Quorum not met 2852 approved = false 2853 2854 log.Debugf("Quorum not met on %v: votes cast %v, quorum %v", 2855 vd.Params.Token, total, quorum) 2856 2857 case approvedVotes < pass: 2858 // Pass percentage not met 2859 approved = false 2860 2861 log.Debugf("Pass threshold not met on %v: approved %v, "+ 2862 "required %v", vd.Params.Token, total, quorum) 2863 2864 default: 2865 // Vote was approved 2866 approved = true 2867 2868 log.Debugf("Vote %v approved: quorum %v, pass %v, total %v, "+ 2869 "approved %v", vd.Params.Token, quorum, pass, total, 2870 approvedVotes) 2871 } 2872 2873 return approved 2874 } 2875 2876 // tokenEncode encodes a token byte slice. 2877 func tokenEncode(tokenB []byte) string { 2878 return util.TokenEncode(tokenB) 2879 } 2880 2881 // tokenDecode decodes a record token and only accepts full length tokens. 2882 func tokenDecode(token string) ([]byte, error) { 2883 return util.TokenDecode(util.TokenTypeTstore, token) 2884 } 2885 2886 // tokenVerify verifies that a token that is part of a plugin command payload 2887 // is valid. This is applicable when a plugin command payload contains a 2888 // signature that includes the record token. The token included in payload must 2889 // be a valid, full length record token and it must match the token that was 2890 // passed into the politeiad API for this plugin command, i.e. the token for 2891 // the record that this plugin command is being executed on. 2892 func tokenVerify(cmdToken []byte, payloadToken string) error { 2893 pt, err := tokenDecode(payloadToken) 2894 if err != nil { 2895 return backend.PluginError{ 2896 PluginID: ticketvote.PluginID, 2897 ErrorCode: uint32(ticketvote.ErrorCodeTokenInvalid), 2898 ErrorContext: util.TokenRegexp(), 2899 } 2900 } 2901 if !bytes.Equal(cmdToken, pt) { 2902 return backend.PluginError{ 2903 PluginID: ticketvote.PluginID, 2904 ErrorCode: uint32(ticketvote.ErrorCodeTokenInvalid), 2905 ErrorContext: fmt.Sprintf("payload token does not "+ 2906 "match command token: got %x, want %x", pt, 2907 cmdToken), 2908 } 2909 } 2910 return nil 2911 } 2912 2913 // isRunoffParent returns whether a record is a runoff vote parent record. 2914 func isRunoffParent(v *ticketvote.VoteMetadata) bool { 2915 return v != nil && v.LinkBy > 0 2916 } 2917 2918 // isRunoffSub returns whether a record is a runoff vote submission. 2919 func isRunoffSub(v *ticketvote.VoteMetadata) bool { 2920 return v != nil && v.LinkTo != "" 2921 } 2922 2923 func convertSignatureError(err error) backend.PluginError { 2924 var e util.SignatureError 2925 var s ticketvote.ErrorCodeT 2926 if errors.As(err, &e) { 2927 switch e.ErrorCode { 2928 case util.ErrorStatusPublicKeyInvalid: 2929 s = ticketvote.ErrorCodePublicKeyInvalid 2930 case util.ErrorStatusSignatureInvalid: 2931 s = ticketvote.ErrorCodeSignatureInvalid 2932 } 2933 } 2934 return backend.PluginError{ 2935 PluginID: ticketvote.PluginID, 2936 ErrorCode: uint32(s), 2937 ErrorContext: e.ErrorContext, 2938 } 2939 } 2940 2941 func convertAuthDetailsFromBlobEntry(be store.BlobEntry) (*ticketvote.AuthDetails, error) { 2942 // Decode and validate data hint 2943 b, err := base64.StdEncoding.DecodeString(be.DataHint) 2944 if err != nil { 2945 return nil, fmt.Errorf("decode DataHint: %v", err) 2946 } 2947 var dd store.DataDescriptor 2948 err = json.Unmarshal(b, &dd) 2949 if err != nil { 2950 return nil, fmt.Errorf("unmarshal DataHint: %v", err) 2951 } 2952 if dd.Descriptor != dataDescriptorAuthDetails { 2953 return nil, fmt.Errorf("unexpected data descriptor: got %v, "+ 2954 "want %v", dd.Descriptor, dataDescriptorAuthDetails) 2955 } 2956 2957 // Decode data 2958 b, err = base64.StdEncoding.DecodeString(be.Data) 2959 if err != nil { 2960 return nil, fmt.Errorf("decode Data: %v", err) 2961 } 2962 digest, err := hex.DecodeString(be.Digest) 2963 if err != nil { 2964 return nil, fmt.Errorf("decode digest: %v", err) 2965 } 2966 if !bytes.Equal(util.Digest(b), digest) { 2967 return nil, fmt.Errorf("data is not coherent; got %x, want %x", 2968 util.Digest(b), digest) 2969 } 2970 var ad ticketvote.AuthDetails 2971 err = json.Unmarshal(b, &ad) 2972 if err != nil { 2973 return nil, fmt.Errorf("unmarshal AuthDetails: %v", err) 2974 } 2975 2976 return &ad, nil 2977 } 2978 2979 func convertVoteDetailsFromBlobEntry(be store.BlobEntry) (*ticketvote.VoteDetails, error) { 2980 // Decode and validate data hint 2981 b, err := base64.StdEncoding.DecodeString(be.DataHint) 2982 if err != nil { 2983 return nil, fmt.Errorf("decode DataHint: %v", err) 2984 } 2985 var dd store.DataDescriptor 2986 err = json.Unmarshal(b, &dd) 2987 if err != nil { 2988 return nil, fmt.Errorf("unmarshal DataHint: %v", err) 2989 } 2990 if dd.Descriptor != dataDescriptorVoteDetails { 2991 return nil, fmt.Errorf("unexpected data descriptor: got %v, "+ 2992 "want %v", dd.Descriptor, dataDescriptorVoteDetails) 2993 } 2994 2995 // Decode data 2996 b, err = base64.StdEncoding.DecodeString(be.Data) 2997 if err != nil { 2998 return nil, fmt.Errorf("decode Data: %v", err) 2999 } 3000 digest, err := hex.DecodeString(be.Digest) 3001 if err != nil { 3002 return nil, fmt.Errorf("decode digest: %v", err) 3003 } 3004 if !bytes.Equal(util.Digest(b), digest) { 3005 return nil, fmt.Errorf("data is not coherent; got %x, want %x", 3006 util.Digest(b), digest) 3007 } 3008 var vd ticketvote.VoteDetails 3009 err = json.Unmarshal(b, &vd) 3010 if err != nil { 3011 return nil, fmt.Errorf("unmarshal VoteDetails: %v", err) 3012 } 3013 3014 return &vd, nil 3015 } 3016 3017 func convertCastVoteDetailsFromBlobEntry(be store.BlobEntry) (*ticketvote.CastVoteDetails, error) { 3018 // Decode and validate data hint 3019 b, err := base64.StdEncoding.DecodeString(be.DataHint) 3020 if err != nil { 3021 return nil, fmt.Errorf("decode DataHint: %v", err) 3022 } 3023 var dd store.DataDescriptor 3024 err = json.Unmarshal(b, &dd) 3025 if err != nil { 3026 return nil, fmt.Errorf("unmarshal DataHint: %v", err) 3027 } 3028 if dd.Descriptor != dataDescriptorCastVoteDetails { 3029 return nil, fmt.Errorf("unexpected data descriptor: got %v, "+ 3030 "want %v", dd.Descriptor, dataDescriptorCastVoteDetails) 3031 } 3032 3033 // Decode data 3034 b, err = base64.StdEncoding.DecodeString(be.Data) 3035 if err != nil { 3036 return nil, fmt.Errorf("decode Data: %v", err) 3037 } 3038 digest, err := hex.DecodeString(be.Digest) 3039 if err != nil { 3040 return nil, fmt.Errorf("decode digest: %v", err) 3041 } 3042 if !bytes.Equal(util.Digest(b), digest) { 3043 return nil, fmt.Errorf("data is not coherent; got %x, want %x", 3044 util.Digest(b), digest) 3045 } 3046 var cv ticketvote.CastVoteDetails 3047 err = json.Unmarshal(b, &cv) 3048 if err != nil { 3049 return nil, fmt.Errorf("unmarshal CastVoteDetails: %v", err) 3050 } 3051 3052 return &cv, nil 3053 } 3054 3055 func convertVoteColliderFromBlobEntry(be store.BlobEntry) (*voteCollider, error) { 3056 // Decode and validate data hint 3057 b, err := base64.StdEncoding.DecodeString(be.DataHint) 3058 if err != nil { 3059 return nil, fmt.Errorf("decode DataHint: %v", err) 3060 } 3061 var dd store.DataDescriptor 3062 err = json.Unmarshal(b, &dd) 3063 if err != nil { 3064 return nil, fmt.Errorf("unmarshal DataHint: %v", err) 3065 } 3066 if dd.Descriptor != dataDescriptorVoteCollider { 3067 return nil, fmt.Errorf("unexpected data descriptor: got %v, "+ 3068 "want %v", dd.Descriptor, dataDescriptorVoteCollider) 3069 } 3070 3071 // Decode data 3072 b, err = base64.StdEncoding.DecodeString(be.Data) 3073 if err != nil { 3074 return nil, fmt.Errorf("decode Data: %v", err) 3075 } 3076 digest, err := hex.DecodeString(be.Digest) 3077 if err != nil { 3078 return nil, fmt.Errorf("decode digest: %v", err) 3079 } 3080 if !bytes.Equal(util.Digest(b), digest) { 3081 return nil, fmt.Errorf("data is not coherent; got %x, want %x", 3082 util.Digest(b), digest) 3083 } 3084 var vc voteCollider 3085 err = json.Unmarshal(b, &vc) 3086 if err != nil { 3087 return nil, fmt.Errorf("unmarshal vote collider: %v", err) 3088 } 3089 3090 return &vc, nil 3091 } 3092 3093 func convertStartRunoffFromBlobEntry(be store.BlobEntry) (*startRunoffRecord, error) { 3094 // Decode and validate data hint 3095 b, err := base64.StdEncoding.DecodeString(be.DataHint) 3096 if err != nil { 3097 return nil, fmt.Errorf("decode DataHint: %v", err) 3098 } 3099 var dd store.DataDescriptor 3100 err = json.Unmarshal(b, &dd) 3101 if err != nil { 3102 return nil, fmt.Errorf("unmarshal DataHint: %v", err) 3103 } 3104 if dd.Descriptor != dataDescriptorStartRunoff { 3105 return nil, fmt.Errorf("unexpected data descriptor: got %v, "+ 3106 "want %v", dd.Descriptor, dataDescriptorStartRunoff) 3107 } 3108 3109 // Decode data 3110 b, err = base64.StdEncoding.DecodeString(be.Data) 3111 if err != nil { 3112 return nil, fmt.Errorf("decode Data: %v", err) 3113 } 3114 digest, err := hex.DecodeString(be.Digest) 3115 if err != nil { 3116 return nil, fmt.Errorf("decode digest: %v", err) 3117 } 3118 if !bytes.Equal(util.Digest(b), digest) { 3119 return nil, fmt.Errorf("data is not coherent; got %x, want %x", 3120 util.Digest(b), digest) 3121 } 3122 var srr startRunoffRecord 3123 err = json.Unmarshal(b, &srr) 3124 if err != nil { 3125 return nil, fmt.Errorf("unmarshal StartRunoffRecord: %v", err) 3126 } 3127 3128 return &srr, nil 3129 } 3130 3131 func convertBlobEntryFromAuthDetails(ad ticketvote.AuthDetails) (*store.BlobEntry, error) { 3132 data, err := json.Marshal(ad) 3133 if err != nil { 3134 return nil, err 3135 } 3136 hint, err := json.Marshal( 3137 store.DataDescriptor{ 3138 Type: store.DataTypeStructure, 3139 Descriptor: dataDescriptorAuthDetails, 3140 }) 3141 if err != nil { 3142 return nil, err 3143 } 3144 be := store.NewBlobEntry(hint, data) 3145 return &be, nil 3146 } 3147 3148 func convertBlobEntryFromVoteDetails(vd ticketvote.VoteDetails) (*store.BlobEntry, error) { 3149 data, err := json.Marshal(vd) 3150 if err != nil { 3151 return nil, err 3152 } 3153 hint, err := json.Marshal( 3154 store.DataDescriptor{ 3155 Type: store.DataTypeStructure, 3156 Descriptor: dataDescriptorVoteDetails, 3157 }) 3158 if err != nil { 3159 return nil, err 3160 } 3161 be := store.NewBlobEntry(hint, data) 3162 return &be, nil 3163 } 3164 3165 func convertBlobEntryFromCastVoteDetails(cv ticketvote.CastVoteDetails) (*store.BlobEntry, error) { 3166 data, err := json.Marshal(cv) 3167 if err != nil { 3168 return nil, err 3169 } 3170 hint, err := json.Marshal( 3171 store.DataDescriptor{ 3172 Type: store.DataTypeStructure, 3173 Descriptor: dataDescriptorCastVoteDetails, 3174 }) 3175 if err != nil { 3176 return nil, err 3177 } 3178 be := store.NewBlobEntry(hint, data) 3179 return &be, nil 3180 } 3181 3182 func convertBlobEntryFromVoteCollider(vc voteCollider) (*store.BlobEntry, error) { 3183 data, err := json.Marshal(vc) 3184 if err != nil { 3185 return nil, err 3186 } 3187 hint, err := json.Marshal( 3188 store.DataDescriptor{ 3189 Type: store.DataTypeStructure, 3190 Descriptor: dataDescriptorVoteCollider, 3191 }) 3192 if err != nil { 3193 return nil, err 3194 } 3195 be := store.NewBlobEntry(hint, data) 3196 return &be, nil 3197 } 3198 3199 func convertBlobEntryFromStartRunoff(srr startRunoffRecord) (*store.BlobEntry, error) { 3200 data, err := json.Marshal(srr) 3201 if err != nil { 3202 return nil, err 3203 } 3204 hint, err := json.Marshal( 3205 store.DataDescriptor{ 3206 Type: store.DataTypeStructure, 3207 Descriptor: dataDescriptorStartRunoff, 3208 }) 3209 if err != nil { 3210 return nil, err 3211 } 3212 be := store.NewBlobEntry(hint, data) 3213 return &be, nil 3214 }