github.com/decred/politeia@v1.4.0/politeiad/backend/gitbe/cms.go (about) 1 // Copyright (c) 2020 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 gitbe 6 7 import ( 8 "bytes" 9 "encoding/hex" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io" 14 "os" 15 "path/filepath" 16 "strconv" 17 "strings" 18 "time" 19 20 "github.com/decred/politeia/politeiad/api/v1/identity" 21 "github.com/decred/politeia/politeiad/backend" 22 "github.com/decred/politeia/politeiad/backend/gitbe/cmsplugin" 23 "github.com/decred/politeia/util" 24 ) 25 26 const ( 27 cmsPluginIdentity = "cmsfullidentity" 28 cmsPluginJournals = "cmssjournals" 29 cmsPluginEnableCache = "enablecache" 30 31 defaultCMSBallotFilename = "cms.ballot.journal" 32 defaultCMSBallotFlushed = "cms.ballot.flushed" 33 ) 34 35 type CastDCCVoteJournal struct { 36 CastVote cmsplugin.CastVote `json:"castvote"` // Client side vote 37 Receipt string `json:"receipt"` // Signature of CastVote.Signature 38 } 39 40 func encodeCastDCCVoteJournal(cvj CastDCCVoteJournal) ([]byte, error) { 41 b, err := json.Marshal(cvj) 42 if err != nil { 43 return nil, err 44 } 45 46 return b, nil 47 } 48 49 var ( 50 cmsPluginSettings map[string]string // [key]setting 51 cmsPluginHooks map[string]func(string) error // [key]func(token) error 52 53 // Cached values, requires lock. These caches are lazy loaded. 54 cmsPluginVoteCache = make(map[string]cmsplugin.StartVote) // [token]startvote 55 cmsPluginVoteSnapshotCache = make(map[string]cmsplugin.StartVoteReply) // [token]StartVoteReply 56 57 // Cached values, requires lock. These caches are built on startup. 58 cmsPluginVotesCache = make(map[string]map[string]struct{}) // [token][ticket]struct{} 59 60 // errIneligibleUserID is emitted when a vote is cast using an 61 // ineligible userid. 62 errIneligibleUserID = errors.New("ineligible userid") 63 ) 64 65 func getCMSPlugin(testnet bool) backend.Plugin { 66 cmsPlugin := backend.Plugin{ 67 ID: cmsplugin.ID, 68 Version: cmsplugin.Version, 69 Settings: []backend.PluginSetting{}, 70 } 71 72 cmsPlugin.Settings = append(cmsPlugin.Settings, 73 backend.PluginSetting{ 74 Key: cmsPluginEnableCache, 75 Value: "", 76 }) 77 78 // Initialize hooks 79 cmsPluginHooks = make(map[string]func(string) error) 80 81 // Initialize settings map 82 cmsPluginSettings = make(map[string]string) 83 for _, v := range cmsPlugin.Settings { 84 cmsPluginSettings[v.Key] = v.Value 85 } 86 return cmsPlugin 87 } 88 89 // initDecredPluginJournals is called externally to run initial procedures 90 // such as replaying journals 91 func (g *gitBackEnd) initCMSPluginJournals() error { 92 log.Infof("initCMSPluginJournals") 93 94 // check if backend journal is initialized 95 if g.journal == nil { 96 return fmt.Errorf("initCMSPlugin backend journal isn't initialized") 97 } 98 99 err := g.replayAllJournals() 100 if err != nil { 101 log.Infof("initCMSPlugin replay all journals %v", err) 102 } 103 return nil 104 } 105 106 // SetCMSPluginSetting removes a setting if the value is "" and adds a setting otherwise. 107 func setCMSPluginSetting(key, value string) { 108 if value == "" { 109 delete(cmsPluginSettings, key) 110 return 111 } 112 113 cmsPluginSettings[key] = value 114 } 115 116 // flushDCCVotes flushes votes journal to cms plugin directory in git. It 117 // returns the filename that was coppied into git repo. 118 // 119 // Must be called WITH the mutex held. 120 func (g *gitBackEnd) flushDCCVotes(token string) (string, error) { 121 if !g.vettedPropExists(token) { 122 return "", fmt.Errorf("unknown dcc: %v", token) 123 } 124 125 // Setup source filenames and verify they actually exist 126 srcDir := pijoin(g.journals, token) 127 srcVotes := pijoin(srcDir, defaultCMSBallotFilename) 128 if !util.FileExists(srcVotes) { 129 return "", nil 130 } 131 132 // Setup destination filenames 133 version, err := getLatest(pijoin(g.unvetted, token)) 134 if err != nil { 135 return "", err 136 } 137 dir := pijoin(g.unvetted, token, version, pluginDataDir) 138 votes := pijoin(dir, defaultCMSBallotFilename) 139 140 // Create the destination container dir 141 _ = os.MkdirAll(dir, 0764) 142 143 // Move journal into place 144 err = g.journal.Copy(srcVotes, votes) 145 if err != nil { 146 return "", err 147 } 148 149 // Return filename that is relative to git dir. 150 return pijoin(token, version, pluginDataDir, defaultCMSBallotFilename), nil 151 } 152 153 // _flushDCCVotesJournals walks all votes journal directories and copies 154 // modified journals into the unvetted repo. It returns an array of filenames 155 // that need to be added to the git repo and subsequently rebased into the 156 // vetted repo . 157 // 158 // Must be called WITH the mutex held. 159 func (g *gitBackEnd) _flushDCCVotesJournals() ([]string, error) { 160 dirs, err := os.ReadDir(g.journals) 161 if err != nil { 162 return nil, err 163 } 164 165 files := make([]string, 0, len(dirs)) 166 for _, v := range dirs { 167 filename := pijoin(g.journals, v.Name(), 168 defaultCMSBallotFlushed) 169 log.Tracef("Checking: %v", v.Name()) 170 if util.FileExists(filename) { 171 continue 172 } 173 174 log.Infof("Flushing votes: %v", v.Name()) 175 176 // We simply copy the journal into git 177 destination, err := g.flushDCCVotes(v.Name()) 178 if err != nil { 179 log.Errorf("Could not flush %v: %v", v.Name(), err) 180 continue 181 } 182 183 // Create flush record 184 err = createFlushFile(filename) 185 if err != nil { 186 log.Errorf("Could not mark flushed %v: %v", v.Name(), 187 err) 188 continue 189 } 190 191 // Add filename to work 192 files = append(files, destination) 193 } 194 195 return files, nil 196 } 197 198 // flushDCCVoteJournals wraps _flushDCCVoteJournals in git magic to revert 199 // flush in case of errors. 200 // 201 // Must be called WITHOUT the mutex held. 202 func (g *gitBackEnd) flushDCCVoteJournals() error { 203 log.Tracef("flushDCCVoteJournals") 204 205 // We may have to make this more granular 206 g.Lock() 207 defer g.Unlock() 208 209 // git checkout master 210 err := g.gitCheckout(g.unvetted, "master") 211 if err != nil { 212 return err 213 } 214 215 // git pull --ff-only --rebase 216 err = g.gitPull(g.unvetted, true) 217 if err != nil { 218 return err 219 } 220 221 // git checkout -b timestamp_flushvotes 222 branch := strconv.FormatInt(time.Now().Unix(), 10) + "_flushvotes" 223 _ = g.gitBranchDelete(g.unvetted, branch) // Just in case 224 err = g.gitNewBranch(g.unvetted, branch) 225 if err != nil { 226 return err 227 } 228 229 // closure to handle unwind if needed 230 var errUnwind error 231 defer func() { 232 if errUnwind == nil { 233 return 234 } 235 err := g.flushJournalsUnwind(branch) 236 if err != nil { 237 log.Errorf("flushJournalsUnwind: %v", err) 238 } 239 }() 240 241 // Flush journals 242 files, err := g._flushDCCVotesJournals() 243 if err != nil { 244 errUnwind = err 245 return err 246 } 247 248 if len(files) == 0 { 249 log.Info("flushVotesJournals: nothing to do") 250 err = g.flushJournalsUnwind(branch) 251 if err != nil { 252 log.Errorf("flushJournalsUnwind: %v", err) 253 } 254 return nil 255 } 256 257 // git add journals 258 commitMessage := "Flush vote journals.\n\n" 259 for _, v := range files { 260 err = g.gitAdd(g.unvetted, v) 261 if err != nil { 262 errUnwind = err 263 return err 264 } 265 266 s := strings.Split(v, string(os.PathSeparator)) 267 if len(s) == 0 { 268 commitMessage += "ERROR: " + v + "\n" 269 } else { 270 commitMessage += s[0] + "\n" 271 } 272 } 273 274 // git commit 275 err = g.gitCommit(g.unvetted, commitMessage) 276 if err != nil { 277 errUnwind = err 278 return err 279 } 280 281 // git rebase master 282 err = g.rebasePR(branch) 283 if err != nil { 284 errUnwind = err 285 return err 286 } 287 288 return nil 289 } 290 func (g *gitBackEnd) cmsPluginJournalFlusher() { 291 // XXX make this a single PR instead of 2 to save some git time 292 err := g.flushDCCVoteJournals() 293 if err != nil { 294 log.Errorf("cmsPluginVoteFlusher: %v", err) 295 } 296 } 297 298 func (g *gitBackEnd) pluginStartDCCVote(payload string) (string, error) { 299 vote, err := cmsplugin.DecodeStartVote([]byte(payload)) 300 if err != nil { 301 return "", fmt.Errorf("DecodeStartVote %v", err) 302 } 303 304 // Verify vote bits are somewhat sane 305 for _, v := range vote.Vote.Options { 306 err = _validateCMSVoteBit(vote.Vote.Options, vote.Vote.Mask, v.Bits) 307 if err != nil { 308 return "", fmt.Errorf("invalid vote bits: %v", err) 309 } 310 } 311 312 // Verify dcc exists 313 tokenB, err := util.ConvertStringToken(vote.Vote.Token) 314 if err != nil { 315 return "", fmt.Errorf("ConvertStringToken %v", err) 316 } 317 token := vote.Vote.Token 318 319 if !g.vettedPropExists(token) { 320 return "", fmt.Errorf("unknown proposal: %v", token) 321 } 322 323 // Make sure vote duration is within min/max range 324 // XXX calculate this value for testnet instead of using hard coded values. 325 if vote.Vote.Duration < cmsplugin.VoteDurationMin || 326 vote.Vote.Duration > cmsplugin.VoteDurationMax { 327 // XXX return a user error instead of an internal error 328 return "", fmt.Errorf("invalid duration: %v (%v - %v)", 329 vote.Vote.Duration, cmsplugin.VoteDurationMin, 330 cmsplugin.VoteDurationMax) 331 } 332 333 // 1. Get best block 334 bb, err := bestBlock() 335 if err != nil { 336 return "", fmt.Errorf("bestBlock %v", err) 337 } 338 if bb.Height < uint32(g.activeNetParams.TicketMaturity) { 339 return "", fmt.Errorf("invalid height") 340 } 341 // 2. Subtract TicketMaturity from block height to get into 342 // unforkable teritory 343 startVoteBlock, err := block(bb.Height) 344 if err != nil { 345 return "", fmt.Errorf("bestBlock %v", err) 346 } 347 348 svr := cmsplugin.StartVoteReply{ 349 Version: cmsplugin.VersionStartVoteReply, 350 StartBlockHeight: startVoteBlock.Height, 351 StartBlockHash: startVoteBlock.Hash, 352 EndHeight: startVoteBlock.Height + vote.Vote.Duration, 353 } 354 svrb, err := cmsplugin.EncodeStartVoteReply(svr) 355 if err != nil { 356 return "", fmt.Errorf("EncodeStartVoteReply: %v", err) 357 } 358 359 // Add version to on disk structure 360 vote.Version = cmsplugin.VersionStartVote 361 voteb, err := cmsplugin.EncodeStartVote(vote) 362 if err != nil { 363 return "", fmt.Errorf("EncodeStartVote: %v", err) 364 } 365 366 // Verify proposal state 367 g.Lock() 368 defer g.Unlock() 369 if g.shutdown { 370 // Make sure we are not shutting down 371 return "", backend.ErrShutdown 372 } 373 374 // Verify DCC vote state 375 vbExists := g.vettedMetadataStreamExists(tokenB, 376 cmsplugin.MDStreamVoteBits) 377 vsExists := g.vettedMetadataStreamExists(tokenB, 378 cmsplugin.MDStreamVoteSnapshot) 379 380 switch { 381 case vbExists && vsExists: 382 // Vote has started 383 return "", fmt.Errorf("dcc vote already started: %v", token) 384 case !vbExists && !vsExists: 385 // Vote has not started; continue 386 default: 387 // We're in trouble! 388 return "", fmt.Errorf("dcc vote is unknown vote state: %v", 389 token) 390 } 391 392 // Store snapshot in metadata 393 err = g._updateVettedMetadata(tokenB, nil, []backend.MetadataStream{ 394 { 395 ID: cmsplugin.MDStreamVoteBits, 396 Payload: string(voteb), 397 }, 398 { 399 ID: cmsplugin.MDStreamVoteSnapshot, 400 Payload: string(svrb), 401 }}) 402 if err != nil { 403 return "", fmt.Errorf("_updateVettedMetadata: %v", err) 404 } 405 406 // Add vote snapshot to in-memory cache 407 cmsPluginVoteSnapshotCache[token] = svr 408 409 log.Infof("Vote started for: %v snapshot %v start %v end %v", 410 token, svr.StartBlockHash, svr.StartBlockHeight, 411 svr.EndHeight) 412 413 // return success and encoded answer 414 return string(svrb), nil 415 } 416 417 // validateCMSVoteBits ensures that the passed in bit is a valid vote option. 418 // This function is expensive due to it's filesystem touches and therefore is 419 // lazily cached. This could stand a rewrite. 420 func (g *gitBackEnd) validateCMSVoteBit(token, bit string) error { 421 b, err := strconv.ParseUint(bit, 16, 64) 422 if err != nil { 423 return err 424 } 425 426 g.Lock() 427 defer g.Unlock() 428 if g.shutdown { 429 return backend.ErrShutdown 430 } 431 432 sv, ok := cmsPluginVoteCache[token] 433 if !ok { 434 // StartVote is not in the cache. Load it from disk. 435 436 // git checkout master 437 err = g.gitCheckout(g.unvetted, "master") 438 if err != nil { 439 return err 440 } 441 442 // git pull --ff-only --rebase 443 err = g.gitPull(g.unvetted, true) 444 if err != nil { 445 return err 446 } 447 // Load md stream 448 svb, err := os.ReadFile(mdFilename(g.vetted, token, 449 cmsplugin.MDStreamVoteBits)) 450 if err != nil { 451 return err 452 } 453 svp, err := cmsplugin.DecodeStartVote(svb) 454 if err != nil { 455 return err 456 } 457 sv = svp 458 459 // Update cache 460 cmsPluginVoteCache[token] = sv 461 } 462 463 // Handle StartVote versioning 464 var ( 465 mask uint64 466 options []cmsplugin.VoteOption 467 ) 468 switch sv.Version { 469 case cmsplugin.VersionStartVote: 470 mask = sv.Vote.Mask 471 options = sv.Vote.Options 472 default: 473 return fmt.Errorf("invalid start vote version %v %v", 474 sv.Version, sv.Token) 475 } 476 477 return _validateCMSVoteBit(options, mask, b) 478 } 479 480 type invalidVoteBitError struct { 481 err error 482 } 483 484 func (i invalidVoteBitError) Error() string { 485 return i.err.Error() 486 } 487 488 // _validateVoteBit iterates over all vote bits and ensure the sent in vote bit 489 // exists. 490 func _validateCMSVoteBit(options []cmsplugin.VoteOption, mask uint64, bit uint64) error { 491 if len(options) == 0 { 492 return fmt.Errorf("_validateVoteBit vote corrupt") 493 } 494 if bit == 0 { 495 return invalidVoteBitError{ 496 err: fmt.Errorf("invalid bit 0x%x", bit), 497 } 498 } 499 if mask&bit != bit { 500 return invalidVoteBitError{ 501 err: fmt.Errorf("invalid mask 0x%x bit 0x%x", 502 mask, bit), 503 } 504 } 505 for _, v := range options { 506 if v.Bits == bit { 507 return nil 508 } 509 if v.Id != cmsplugin.DCCApprovalString && 510 v.Id != cmsplugin.DCCDisapprovalString { 511 return invalidVoteBitError{ 512 err: fmt.Errorf("bit option not valid found: %s", v.Id), 513 } 514 } 515 } 516 return invalidVoteBitError{ 517 err: fmt.Errorf("bit not found 0x%x", bit), 518 } 519 } 520 521 // replayDCCBallot replays voting journal for given dcc. 522 // 523 // Functions must be called WITH the lock held. 524 func (g *gitBackEnd) replayDCCBallot(token string) error { 525 // Verify proposal exists, we can run this lockless 526 if !g.vettedPropExists(token) { 527 return nil 528 } 529 530 // Do some cheap things before expensive calls 531 bfilename := pijoin(g.journals, token, 532 defaultCMSBallotFilename) 533 534 // Replay journal 535 err := g.journal.Open(bfilename) 536 if err != nil { 537 if !os.IsNotExist(err) { 538 return fmt.Errorf("journal.Open: %v", err) 539 } 540 return nil 541 } 542 defer func() { 543 err = g.journal.Close(bfilename) 544 if err != nil { 545 log.Errorf("journal.Close: %v", err) 546 } 547 }() 548 549 for { 550 err = g.journal.Replay(bfilename, func(s string) error { 551 ss := bytes.NewReader([]byte(s)) 552 d := json.NewDecoder(ss) 553 554 // Decode action 555 var action JournalAction 556 err = d.Decode(&action) 557 if err != nil { 558 return fmt.Errorf("journal action: %v", err) 559 } 560 561 switch action.Action { 562 case journalActionAdd: 563 var cvj CastDCCVoteJournal 564 err = d.Decode(&cvj) 565 if err != nil { 566 return fmt.Errorf("journal add: %v", 567 err) 568 } 569 570 token := cvj.CastVote.Token 571 userid := cvj.CastVote.UserID 572 // See if the prop already exists 573 if _, ok := cmsPluginVotesCache[token]; !ok { 574 // Create map to track tickets 575 cmsPluginVotesCache[token] = make(map[string]struct{}) 576 } 577 // See if we have a duplicate vote 578 if _, ok := cmsPluginVotesCache[token][userid]; ok { 579 log.Errorf("duplicate cms cast vote %v %v", 580 token, userid) 581 } 582 // All good, record vote in cache 583 cmsPluginVotesCache[token][userid] = struct{}{} 584 585 default: 586 return fmt.Errorf("invalid action: %v", 587 action.Action) 588 } 589 return nil 590 }) 591 if errors.Is(err, io.EOF) { 592 break 593 } else if err != nil { 594 return err 595 } 596 } 597 598 return nil 599 } 600 601 // loadDCCVoteCache loads the cmsplugin.StartVote from disk for the provided 602 // token and adds it to the cmsPluginVoteCache. 603 // 604 // This function must be called WITH the lock held. 605 func (g *gitBackEnd) loadDCCVoteCache(token string) (*cmsplugin.StartVote, error) { 606 // git checkout master 607 err := g.gitCheckout(g.unvetted, "master") 608 if err != nil { 609 return nil, err 610 } 611 612 // git pull --ff-only --rebase 613 err = g.gitPull(g.unvetted, true) 614 if err != nil { 615 return nil, err 616 } 617 618 // Load the vote snapshot from disk 619 f, err := os.Open(mdFilename(g.vetted, token, 620 cmsplugin.MDStreamVoteBits)) 621 if err != nil { 622 return nil, err 623 } 624 defer f.Close() 625 626 var sv cmsplugin.StartVote 627 d := json.NewDecoder(f) 628 err = d.Decode(&sv) 629 if err != nil { 630 return nil, err 631 } 632 633 cmsPluginVoteCache[token] = sv 634 635 return &sv, nil 636 } 637 638 // loadDCCVoteSnapshotCache loads the cmsplugin.StartVoteReply from disk for the provided 639 // token and adds it to the cmsPluginVoteSnapshotCache. 640 // 641 // This function must be called WITH the lock held. 642 func (g *gitBackEnd) loadDCCVoteSnapshotCache(token string) (*cmsplugin.StartVoteReply, error) { 643 // git checkout master 644 err := g.gitCheckout(g.unvetted, "master") 645 if err != nil { 646 return nil, err 647 } 648 649 // git pull --ff-only --rebase 650 err = g.gitPull(g.unvetted, true) 651 if err != nil { 652 return nil, err 653 } 654 655 // Load the vote snapshot from disk 656 f, err := os.Open(mdFilename(g.vetted, token, 657 cmsplugin.MDStreamVoteSnapshot)) 658 if err != nil { 659 return nil, err 660 } 661 defer f.Close() 662 663 var svr cmsplugin.StartVoteReply 664 d := json.NewDecoder(f) 665 err = d.Decode(&svr) 666 if err != nil { 667 return nil, err 668 } 669 670 cmsPluginVoteSnapshotCache[token] = svr 671 672 return &svr, nil 673 } 674 675 // dccVoteEndHeight returns the voting period end height for the provided token. 676 func (g *gitBackEnd) dccVoteEndHeight(token string) (uint32, error) { 677 g.Lock() 678 defer g.Unlock() 679 if g.shutdown { 680 return 0, backend.ErrShutdown 681 } 682 683 svr, ok := cmsPluginVoteSnapshotCache[token] 684 if !ok { 685 s, err := g.loadDCCVoteSnapshotCache(token) 686 if err != nil { 687 return 0, err 688 } 689 svr = *s 690 } 691 692 return svr.EndHeight, nil 693 } 694 695 // writeDCCVote writes the provided vote to the provided journal file path, if the 696 // vote does not already exist. Once successfully written to the journal, the 697 // vote is added to the cast vote memory cache. 698 // 699 // This function must be called WITHOUT the lock held. 700 func (g *gitBackEnd) writeDCCVote(v cmsplugin.CastVote, receipt, journalPath string) error { 701 g.Lock() 702 defer g.Unlock() 703 704 // Ensure ticket is eligible to vote. 705 // This cache should have already been loaded when the 706 // vote end height was validated, but lets be sure. 707 sv, ok := cmsPluginVoteCache[v.Token] 708 if !ok { 709 s, err := g.loadDCCVoteCache(v.Token) 710 if err != nil { 711 return fmt.Errorf("loadDCCVoteCache: %v", 712 err) 713 } 714 sv = *s 715 } 716 var found bool 717 for _, t := range sv.UserWeights { 718 if t.UserID == v.UserID { 719 found = true 720 break 721 } 722 } 723 if !found { 724 return errIneligibleUserID 725 } 726 727 // Ensure vote is not a duplicate 728 _, ok = cmsPluginVotesCache[v.Token] 729 if !ok { 730 cmsPluginVotesCache[v.Token] = make(map[string]struct{}) 731 } 732 733 _, ok = cmsPluginVotesCache[v.Token][v.UserID] 734 if ok { 735 return errDuplicateVote 736 } 737 738 // Create journal entry 739 cvj := CastDCCVoteJournal{ 740 CastVote: v, 741 Receipt: receipt, 742 } 743 blob, err := encodeCastDCCVoteJournal(cvj) 744 if err != nil { 745 return fmt.Errorf("encodeCastVoteJournal: %v", 746 err) 747 } 748 749 // Write vote to journal 750 err = g.journal.Journal(journalPath, string(journalAdd)+ 751 string(blob)) 752 if err != nil { 753 return fmt.Errorf("could not journal vote %v: %v %v", 754 v.Token, v.UserID, err) 755 } 756 757 // Add vote to memory cache 758 cmsPluginVotesCache[v.Token][v.UserID] = struct{}{} 759 760 return nil 761 } 762 763 func (g *gitBackEnd) pluginCastVote(payload string) (string, error) { 764 log.Tracef("pluginCastVote") 765 766 // Check if journals were replayed 767 if !journalsReplayed { 768 return "", backend.ErrJournalsNotReplayed 769 } 770 771 // Decode ballot 772 vote, err := cmsplugin.DecodeCastVote([]byte(payload)) 773 if err != nil { 774 return "", fmt.Errorf("DecodeBallot: %v", err) 775 } 776 777 // XXX this should become part of some sort of context 778 fiJSON, ok := cmsPluginSettings[cmsPluginIdentity] 779 if !ok { 780 return "", fmt.Errorf("full identity not set") 781 } 782 fi, err := identity.UnmarshalFullIdentity([]byte(fiJSON)) 783 if err != nil { 784 return "", err 785 } 786 787 // Get best block 788 bb, err := bestBlock() 789 if err != nil { 790 return "", fmt.Errorf("bestBlock %v", err) 791 } 792 793 br := cmsplugin.CastVoteReply{} 794 // Verify proposal exists, we can run this lockless 795 if !g.vettedPropExists(vote.Token) { 796 log.Errorf("pluginCastVote: proposal not found: %v", 797 vote.Token) 798 e := cmsplugin.ErrorStatusDCCNotFound 799 err := fmt.Sprintf("%v: %v", 800 cmsplugin.ErrorStatus[e], vote.Token) 801 return "", fmt.Errorf("write vote: %v", err) 802 } 803 804 // Ensure that the votebits are correct 805 err = g.validateCMSVoteBit(vote.Token, vote.VoteBit) 806 if err != nil { 807 var ierr invalidVoteBitError 808 if errors.As(err, &ierr) { 809 es := cmsplugin.ErrorStatusInvalidVoteBit 810 err := fmt.Sprintf("%v: %v", 811 cmsplugin.ErrorStatus[es], ierr.err.Error()) 812 return "", fmt.Errorf("validateCMSVoteBit: %v", err) 813 814 } 815 t := time.Now().Unix() 816 log.Errorf("pluginCastVote: validateCMSVoteBit %v %v %v %v", 817 vote.UserID, vote.Token, t, err) 818 e := cmsplugin.ErrorStatusInternalError 819 err := fmt.Sprintf("%v: %v", 820 cmsplugin.ErrorStatus[e], t) 821 return "", fmt.Errorf("write vote: %v", err) 822 823 } 824 825 // Verify voting period has not ended 826 endHeight, err := g.dccVoteEndHeight(vote.Token) 827 if err != nil { 828 t := time.Now().Unix() 829 log.Errorf("pluginCastVote: dccVoteEndHeight %v %v %v %v", 830 vote.UserID, vote.Token, t, err) 831 e := cmsplugin.ErrorStatusInternalError 832 err := fmt.Sprintf("%v: %v", 833 cmsplugin.ErrorStatus[e], t) 834 return "", fmt.Errorf("write vote: %v", err) 835 836 } 837 if bb.Height >= endHeight { 838 e := cmsplugin.ErrorStatusVoteHasEnded 839 br.ErrorStatus = e 840 err := fmt.Sprintf("%v: %v", 841 cmsplugin.ErrorStatus[e], vote.Token) 842 return "", fmt.Errorf("write vote: %v", err) 843 844 } 845 846 // Ensure journal directory exists 847 dir := pijoin(g.journals, vote.Token) 848 bfilename := pijoin(dir, defaultCMSBallotFilename) 849 err = os.MkdirAll(dir, 0774) 850 if err != nil { 851 // Should not fail, so return failure to alert people 852 return "", fmt.Errorf("make journal dir: %v", err) 853 } 854 855 // Sign signature 856 r := fi.SignMessage([]byte(vote.Signature)) 857 receipt := hex.EncodeToString(r[:]) 858 859 // Write vote to journal 860 err = g.writeDCCVote(*vote, receipt, bfilename) 861 if err != nil { 862 switch err { 863 case errDuplicateVote: 864 e := cmsplugin.ErrorStatusDuplicateVote 865 err := fmt.Sprintf("%v: %v", 866 cmsplugin.ErrorStatus[e], vote.Token) 867 return "", fmt.Errorf("write vote: %v", err) 868 case errIneligibleUserID: 869 e := cmsplugin.ErrorStatusIneligibleUserID 870 err := fmt.Sprintf("%v: %v", 871 cmsplugin.ErrorStatus[e], vote.Token) 872 return "", fmt.Errorf("write vote: %v", err) 873 default: 874 // Should not fail, so return failure to alert people 875 return "", fmt.Errorf("write vote: %v", err) 876 } 877 } 878 879 // Update reply 880 br.ClientSignature = vote.Signature 881 br.Signature = receipt 882 883 // Mark comment journal dirty 884 flushFilename := pijoin(g.journals, vote.Token, 885 defaultCMSBallotFlushed) 886 _ = os.Remove(flushFilename) 887 888 // Encode reply 889 brb, err := cmsplugin.EncodeCastVoteReply(br) 890 if err != nil { 891 return "", fmt.Errorf("EncodeCastVoteReply: %v", err) 892 } 893 894 // return success and encoded answer 895 return string(brb), nil 896 } 897 898 // tallyDCCVotes replays the ballot journal for a proposal and tallies the votes. 899 // 900 // Function must be called WITH the lock held. 901 func (g *gitBackEnd) tallyDCCVotes(token string) ([]cmsplugin.CastVote, error) { 902 // Do some cheap things before expensive calls 903 bfilename := pijoin(g.journals, token, defaultCMSBallotFilename) 904 905 // Replay journal 906 err := g.journal.Open(bfilename) 907 if err != nil { 908 if !os.IsNotExist(err) { 909 return nil, fmt.Errorf("journal.Open: %v", err) 910 } 911 return []cmsplugin.CastVote{}, nil 912 } 913 defer func() { 914 err = g.journal.Close(bfilename) 915 if err != nil { 916 log.Errorf("journal.Close: %v", err) 917 } 918 }() 919 920 cv := make([]cmsplugin.CastVote, 0, 41000) 921 for { 922 err = g.journal.Replay(bfilename, func(s string) error { 923 ss := bytes.NewReader([]byte(s)) 924 d := json.NewDecoder(ss) 925 926 // Decode action 927 var action JournalAction 928 err = d.Decode(&action) 929 if err != nil { 930 return fmt.Errorf("journal action: %v", err) 931 } 932 933 switch action.Action { 934 case journalActionAdd: 935 var cvj CastDCCVoteJournal 936 err = d.Decode(&cvj) 937 if err != nil { 938 return fmt.Errorf("journal add: %v", 939 err) 940 } 941 cv = append(cv, cvj.CastVote) 942 943 default: 944 return fmt.Errorf("invalid action: %v", 945 action.Action) 946 } 947 return nil 948 }) 949 if errors.Is(err, io.EOF) { 950 break 951 } else if err != nil { 952 return nil, err 953 } 954 } 955 956 return cv, nil 957 } 958 959 // pluginDCCVoteDetails returns the VoteDetails of a requested DCC vote. 960 // It uses the caches that should be populated with the StartVotes and 961 // StartVoteReplies. 962 func (g *gitBackEnd) pluginDCCVoteDetails(payload string) (string, error) { 963 log.Tracef("pluginDCCVoteDetails: %v", payload) 964 965 vd, err := cmsplugin.DecodeVoteDetails([]byte(payload)) 966 if err != nil { 967 return "", fmt.Errorf("DecodeVoteResults %v", err) 968 } 969 970 // Verify dcc exists, we can run this lockless 971 if !g.vettedPropExists(vd.Token) { 972 return "", fmt.Errorf("dcc not found: %v", vd.Token) 973 } 974 975 token, err := hex.DecodeString(vd.Token) 976 if err != nil { 977 return "", err 978 } 979 // Find the most recent vesion number for this record 980 r, err := g.GetVetted(token, "") 981 if err != nil { 982 return "", fmt.Errorf("GetVetted %v version 0: %v", token, err) 983 } 984 985 var vdr cmsplugin.VoteDetailsReply 986 // Prepare reply 987 for _, v := range r.Metadata { 988 switch v.ID { 989 case cmsplugin.MDStreamVoteBits: 990 // Start vote 991 sv, err := cmsplugin.DecodeStartVote([]byte(v.Payload)) 992 if err != nil { 993 return "", err 994 } 995 vdr.StartVote = sv 996 case cmsplugin.MDStreamVoteSnapshot: 997 svr, err := cmsplugin.DecodeStartVoteReply([]byte(v.Payload)) 998 if err != nil { 999 return "", err 1000 } 1001 vdr.StartVoteReply = svr 1002 } 1003 } 1004 1005 reply, err := cmsplugin.EncodeVoteDetailsReply(vdr) 1006 if err != nil { 1007 return "", fmt.Errorf("Could not encode VoteResultsReply: %v", 1008 err) 1009 } 1010 return string(reply), nil 1011 } 1012 1013 // pluginDCCVoteSummary 1014 func (g *gitBackEnd) pluginDCCVoteSummary(payload string) (string, error) { 1015 log.Tracef("pluginDCCVoteSummary: %v", payload) 1016 1017 vs, err := cmsplugin.DecodeVoteSummary([]byte(payload)) 1018 if err != nil { 1019 return "", fmt.Errorf("DecodeVoteResults %v", err) 1020 } 1021 1022 // Verify dcc exists, we can run this lockless 1023 if !g.vettedPropExists(vs.Token) { 1024 return "", fmt.Errorf("dcc not found: %v", vs.Token) 1025 } 1026 1027 token, err := hex.DecodeString(vs.Token) 1028 if err != nil { 1029 return "", err 1030 } 1031 // Find the most recent vesion number for this record 1032 r, err := g.GetVetted(token, "") 1033 if err != nil { 1034 return "", fmt.Errorf("GetVetted %v version 0: %v", token, err) 1035 } 1036 1037 // Prepare reply 1038 var vrr cmsplugin.VoteResultsReply 1039 var vsr cmsplugin.VoteSummaryReply 1040 var svr cmsplugin.StartVoteReply 1041 vors := make([]cmsplugin.VoteOptionResult, 0, 1042 len(vrr.StartVote.Vote.Options)) 1043 1044 // Fill out cast votes 1045 vrr.CastVotes, err = g.tallyDCCVotes(vs.Token) 1046 if err != nil { 1047 return "", fmt.Errorf("Could not tally votes: %v", err) 1048 } 1049 1050 for _, v := range r.Metadata { 1051 switch v.ID { 1052 case cmsplugin.MDStreamVoteBits: 1053 // Start vote 1054 sv, err := cmsplugin.DecodeStartVote([]byte(v.Payload)) 1055 if err != nil { 1056 return "", err 1057 } 1058 vrr.StartVote = sv 1059 case cmsplugin.MDStreamVoteSnapshot: 1060 svr, err = cmsplugin.DecodeStartVoteReply([]byte(v.Payload)) 1061 if err != nil { 1062 return "", err 1063 } 1064 } 1065 } 1066 1067 vsr.EndHeight = svr.EndHeight 1068 vsr.Duration = vrr.StartVote.Vote.Duration 1069 vsr.PassPercentage = vrr.StartVote.Vote.PassPercentage 1070 1071 for _, voteOption := range vrr.StartVote.Vote.Options { 1072 vors = append(vors, cmsplugin.VoteOptionResult{ 1073 ID: voteOption.Id, 1074 Description: voteOption.Description, 1075 Bits: voteOption.Bits, 1076 }) 1077 } 1078 1079 for _, vote := range vrr.CastVotes { 1080 b, err := strconv.ParseUint(vote.VoteBit, 16, 64) 1081 if err != nil { 1082 log.Errorf("unable to parse vote bits for vote %v %v", 1083 vote.Signature, err) 1084 continue 1085 } 1086 for i, option := range vors { 1087 if b == option.Bits { 1088 vors[i].Votes++ 1089 } 1090 } 1091 } 1092 vsr.Results = vors 1093 1094 reply, err := cmsplugin.EncodeVoteSummaryReply(vsr) 1095 if err != nil { 1096 return "", fmt.Errorf("Could not encode VoteResultsReply: %v", 1097 err) 1098 } 1099 1100 return string(reply), nil 1101 } 1102 1103 // pluginDCCVoteResults tallies all votes for a dcc. We can run the tally 1104 // unlocked and just replay the journal. If the replay becomes an issue we 1105 // could cache it. The Vote that is returned does have to be locked. 1106 func (g *gitBackEnd) pluginDCCVoteResults(payload string) (string, error) { 1107 log.Tracef("pluginDCCVoteResults: %v", payload) 1108 1109 vote, err := cmsplugin.DecodeVoteResults([]byte(payload)) 1110 if err != nil { 1111 return "", fmt.Errorf("DecodeVoteResults %v", err) 1112 } 1113 1114 // Verify dcc exists, we can run this lockless 1115 if !g.vettedPropExists(vote.Token) { 1116 return "", fmt.Errorf("dcc not found: %v", vote.Token) 1117 } 1118 1119 // Prepare reply 1120 var vrr cmsplugin.VoteResultsReply 1121 1122 token, err := hex.DecodeString(vote.Token) 1123 if err != nil { 1124 return "", err 1125 } 1126 1127 // Find the most recent vesion number for this record 1128 r, err := g.GetVetted(token, "") 1129 if err != nil { 1130 return "", fmt.Errorf("GetVetted %v version 0: %v", token, err) 1131 } 1132 1133 for _, v := range r.Metadata { 1134 switch v.ID { 1135 case cmsplugin.MDStreamVoteBits: 1136 // Start vote 1137 sv, err := cmsplugin.DecodeStartVote([]byte(v.Payload)) 1138 if err != nil { 1139 return "", err 1140 } 1141 vrr.StartVote = sv 1142 } 1143 } 1144 1145 // Fill out cast votes 1146 vrr.CastVotes, err = g.tallyDCCVotes(vote.Token) 1147 if err != nil { 1148 return "", fmt.Errorf("Could not tally votes: %v", err) 1149 } 1150 1151 reply, err := cmsplugin.EncodeVoteResultsReply(vrr) 1152 if err != nil { 1153 return "", fmt.Errorf("Could not encode VoteResultsReply: %v", 1154 err) 1155 } 1156 1157 return string(reply), nil 1158 } 1159 1160 // pluginCMSInventory returns the cms plugin inventory for all dccs. The 1161 // inventory consists vote details, and cast votes. 1162 func (g *gitBackEnd) pluginCMSInventory() (string, error) { 1163 log.Tracef("pluginInventory") 1164 1165 g.Lock() 1166 defer g.Unlock() 1167 1168 // Ensure journal has been replayed 1169 if !journalsReplayed { 1170 return "", backend.ErrJournalsNotReplayed 1171 } 1172 1173 // Walk vetted repo and compile all file paths 1174 paths := make([]string, 0, 2048) // PNOOMA 1175 err := filepath.Walk(g.vetted, 1176 func(path string, info os.FileInfo, err error) error { 1177 if err != nil { 1178 return err 1179 } 1180 paths = append(paths, path) 1181 return nil 1182 }) 1183 if err != nil { 1184 return "", fmt.Errorf("walk vetted: %v", err) 1185 } 1186 1187 // Filter out the file paths for authorize vote metadata and 1188 // start vote metadata 1189 svPaths := make([]string, 0, len(paths)) 1190 svFile := fmt.Sprintf("%02v%v", cmsplugin.MDStreamVoteBits, 1191 defaultMDFilenameSuffix) 1192 for _, v := range paths { 1193 switch filepath.Base(v) { 1194 case svFile: 1195 svPaths = append(svPaths, v) 1196 } 1197 } 1198 1199 // Compile the start vote tuples. The in-memory caches that 1200 // contain the vote bits and the vote snapshots are lazy 1201 // loaded so we have to read vote metadata directly from disk. 1202 svt := make([]cmsplugin.StartVoteTuple, 0, len(cmsPluginVoteCache)) 1203 for _, v := range svPaths { 1204 // Read vote bits file into memory 1205 b, err := os.ReadFile(v) 1206 if err != nil { 1207 return "", fmt.Errorf("ReadFile %v: %v", v, err) 1208 } 1209 1210 // Decode vote bits 1211 sv, err := cmsplugin.DecodeStartVote(b) 1212 if err != nil { 1213 return "", fmt.Errorf("DecodeStartVote: %v", err) 1214 } 1215 1216 // Read vote snapshot file into memory 1217 dir := filepath.Dir(v) 1218 filename := fmt.Sprintf("%02v%v", cmsplugin.MDStreamVoteSnapshot, 1219 defaultMDFilenameSuffix) 1220 path := filepath.Join(dir, filename) 1221 b, err = os.ReadFile(path) 1222 if err != nil { 1223 return "", fmt.Errorf("ReadFile %v: %v", path, err) 1224 } 1225 1226 // Decode vote snapshot 1227 svr, err := cmsplugin.DecodeStartVoteReply(b) 1228 if err != nil { 1229 return "", fmt.Errorf("DecodeStartVoteReply: %v", err) 1230 } 1231 1232 // Create start vote tuple 1233 svt = append(svt, cmsplugin.StartVoteTuple{ 1234 StartVote: sv, 1235 StartVoteReply: svr, 1236 }) 1237 } 1238 1239 // Compile cast votes. The in-memory votes cache does not 1240 // store the full cast vote struct so we need to replay the 1241 // vote journals. 1242 1243 // Walk journals directory and tally votes for all ballot 1244 // journals that are found. 1245 cv := make([][]cmsplugin.CastVote, 0, len(svt)) 1246 err = filepath.Walk(g.journals, 1247 func(path string, info os.FileInfo, err error) error { 1248 if err != nil { 1249 return err 1250 } 1251 1252 if info.Name() == defaultBallotFilename { 1253 token := filepath.Base(filepath.Dir(path)) 1254 votes, err := g.tallyDCCVotes(token) 1255 if err != nil { 1256 return fmt.Errorf("tallyDCCVotes %v: %v", token, err) 1257 } 1258 1259 cv = append(cv, votes) 1260 } 1261 1262 return nil 1263 }) 1264 if err != nil { 1265 return "", fmt.Errorf("walk journals: %v", err) 1266 } 1267 1268 var count = 0 1269 for _, v := range cv { 1270 count += len(v) 1271 } 1272 votes := make([]cmsplugin.CastVote, 0, count) 1273 for _, v := range cv { 1274 votes = append(votes, v...) 1275 } 1276 1277 // Prepare reply 1278 ir := cmsplugin.InventoryReply{ 1279 StartVoteTuples: svt, 1280 CastVotes: votes, 1281 } 1282 1283 payload, err := cmsplugin.EncodeInventoryReply(ir) 1284 if err != nil { 1285 return "", fmt.Errorf("EncodeInventoryReply: %v", err) 1286 } 1287 1288 return string(payload), nil 1289 }