github.com/decred/politeia@v1.4.0/politeiawww/legacy/dcc.go (about) 1 // Copyright (c) 2019-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 legacy 6 7 import ( 8 "bytes" 9 "context" 10 "encoding/base64" 11 "encoding/hex" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "net/http" 16 "regexp" 17 "strconv" 18 "strings" 19 "time" 20 21 pd "github.com/decred/politeia/politeiad/api/v1" 22 "github.com/decred/politeia/politeiad/api/v1/identity" 23 "github.com/decred/politeia/politeiad/backend/gitbe/cmsplugin" 24 "github.com/decred/politeia/politeiad/backend/gitbe/decredplugin" 25 cms "github.com/decred/politeia/politeiawww/api/cms/v1" 26 www "github.com/decred/politeia/politeiawww/api/www/v1" 27 "github.com/decred/politeia/politeiawww/legacy/cmsdatabase" 28 "github.com/decred/politeia/politeiawww/legacy/mdstream" 29 "github.com/decred/politeia/politeiawww/legacy/user" 30 "github.com/decred/politeia/util" 31 ) 32 33 const ( 34 // dccFile contains the file name of the dcc file 35 dccFile = "dcc.json" 36 37 supportString = "aye" 38 opposeString = "nay" 39 40 // DCC All User Vote Configuration 41 userWeightMonthLookback = 6 // Lookback 6 months to deteremine user voting weight 42 ) 43 44 var ( 45 validSponsorStatement = regexp.MustCompile(createSponsorStatementRegex()) 46 47 // The valid contractor 48 invalidDCCContractorType = map[cms.ContractorTypeT]bool{ 49 cms.ContractorTypeNominee: true, 50 cms.ContractorTypeInvalid: true, 51 } 52 53 // This covers the possible valid status transitions for any dcc. 54 validDCCStatusTransitions = map[cms.DCCStatusT][]cms.DCCStatusT{ 55 // Active DCC's may only be approved or rejected. 56 cms.DCCStatusActive: { 57 cms.DCCStatusApproved, 58 cms.DCCStatusRejected, 59 }, 60 } 61 ) 62 63 // createSponsorStatementRegex generates a regex based on the policy supplied for 64 // valid characters sponsor statement. 65 func createSponsorStatementRegex() string { 66 var buf bytes.Buffer 67 buf.WriteString("^[") 68 69 for _, supportedChar := range cms.PolicySponsorStatementSupportedChars { 70 if len(supportedChar) > 1 { 71 buf.WriteString(supportedChar) 72 } else { 73 buf.WriteString(`\` + supportedChar) 74 } 75 } 76 buf.WriteString("]*$") 77 78 return buf.String() 79 } 80 81 func convertRecordToDatabaseDCC(p pd.Record) (*cmsdatabase.DCC, error) { 82 dbDCC := cmsdatabase.DCC{ 83 Files: convertWWWFilesFromPD(p.Files), 84 Token: p.CensorshipRecord.Token, 85 ServerSignature: p.CensorshipRecord.Signature, 86 } 87 88 // Decode invoice file 89 for _, v := range p.Files { 90 if v.Name == dccFile { 91 b, err := base64.StdEncoding.DecodeString(v.Payload) 92 if err != nil { 93 return nil, err 94 } 95 96 var dcc cms.DCCInput 97 err = json.Unmarshal(b, &dcc) 98 if err != nil { 99 return nil, fmt.Errorf("could not decode DCC input data: token '%v': %v", 100 p.CensorshipRecord.Token, err) 101 } 102 dbDCC.Type = dcc.Type 103 dbDCC.NomineeUserID = dcc.NomineeUserID 104 dbDCC.SponsorStatement = dcc.SponsorStatement 105 dbDCC.Domain = dcc.Domain 106 dbDCC.ContractorType = dcc.ContractorType 107 } 108 } 109 110 for _, m := range p.Metadata { 111 switch m.ID { 112 case mdstream.IDRecordStatusChange: 113 // Ignore initial stream change since it's just the automatic change from 114 // unvetted to vetted 115 continue 116 case mdstream.IDDCCGeneral: 117 var mdGeneral mdstream.DCCGeneral 118 err := json.Unmarshal([]byte(m.Payload), &mdGeneral) 119 if err != nil { 120 return nil, fmt.Errorf("could not decode metadata '%v' token '%v': %v", 121 p.Metadata, p.CensorshipRecord.Token, err) 122 } 123 124 dbDCC.TimeSubmitted = mdGeneral.Timestamp 125 dbDCC.PublicKey = mdGeneral.PublicKey 126 dbDCC.UserSignature = mdGeneral.Signature 127 128 case mdstream.IDDCCStatusChange: 129 sc, err := mdstream.DecodeDCCStatusChange([]byte(m.Payload)) 130 if err != nil { 131 return nil, fmt.Errorf("could not decode metadata '%v' token '%v': %v", 132 m, p.CensorshipRecord.Token, err) 133 } 134 135 // We don't need all of the status changes. 136 // Just the most recent one. 137 for _, s := range sc { 138 dbDCC.Status = s.NewStatus 139 dbDCC.StatusChangeReason = s.Reason 140 switch s.NewStatus { 141 case cms.DCCStatusActive: 142 dbDCC.TimeSubmitted = s.Timestamp 143 case cms.DCCStatusApproved, cms.DCCStatusRejected: 144 dbDCC.TimeReviewed = s.Timestamp 145 } 146 } 147 case mdstream.IDDCCSupportOpposition: 148 // Support and Opposition 149 so, err := mdstream.DecodeDCCSupportOpposition([]byte(m.Payload)) 150 if err != nil { 151 log.Errorf("convertDCCFromRecord: decode md stream: "+ 152 "token:%v error:%v payload:%v", 153 p.CensorshipRecord.Token, err, m) 154 continue 155 } 156 supportPubkeys := make([]string, 0, len(so)) 157 opposePubkeys := make([]string, 0, len(so)) 158 // Tabulate all support and opposition 159 for _, s := range so { 160 if s.Vote == supportString { 161 supportPubkeys = append(supportPubkeys, s.PublicKey) 162 } else if s.Vote == opposeString { 163 opposePubkeys = append(opposePubkeys, s.PublicKey) 164 } 165 } 166 supports := "" 167 for i, support := range supportPubkeys { 168 if i == 0 { 169 supports += support 170 } else { 171 supports += "," + support 172 } 173 } 174 dbDCC.SupportUserIDs = supports 175 opposes := "" 176 for i, oppose := range opposePubkeys { 177 if i == 0 { 178 opposes += oppose 179 } else { 180 opposes += "," + oppose 181 } 182 } 183 dbDCC.OppositionUserIDs = opposes 184 case cmsplugin.MDStreamVoteBits: 185 case cmsplugin.MDStreamVoteSnapshot: 186 // Voting information available but not currently used in the database 187 default: 188 // Log error but proceed 189 log.Errorf("convertRecordToDCC: invalid "+ 190 "metadata stream ID %v token %v", 191 m.ID, p.CensorshipRecord.Token) 192 } 193 } 194 195 return &dbDCC, nil 196 } 197 198 func convertDCCDatabaseToRecord(dbDCC *cmsdatabase.DCC) cms.DCCRecord { 199 dccRecord := cms.DCCRecord{} 200 201 dccRecord.DCC.Type = dbDCC.Type 202 dccRecord.DCC.NomineeUserID = dbDCC.NomineeUserID 203 dccRecord.DCC.SponsorStatement = dbDCC.SponsorStatement 204 dccRecord.DCC.Domain = dbDCC.Domain 205 dccRecord.DCC.ContractorType = dbDCC.ContractorType 206 dccRecord.Status = dbDCC.Status 207 dccRecord.StatusChangeReason = dbDCC.StatusChangeReason 208 dccRecord.TimeSubmitted = dbDCC.TimeSubmitted 209 dccRecord.TimeReviewed = dbDCC.TimeReviewed 210 dccRecord.CensorshipRecord = www.CensorshipRecord{ 211 Token: dbDCC.Token, 212 } 213 dccRecord.PublicKey = dbDCC.PublicKey 214 dccRecord.Signature = dbDCC.ServerSignature 215 dccRecord.SponsorUserID = dbDCC.SponsorUserID 216 supportUserIDs := strings.Split(dbDCC.SupportUserIDs, ",") 217 cleanedSupport := make([]string, 0, len(supportUserIDs)) 218 for _, support := range supportUserIDs { 219 cleanedSupport = append(cleanedSupport, strings.TrimSpace(support)) 220 } 221 dccRecord.SupportUserIDs = cleanedSupport 222 oppositionUserIDs := strings.Split(dbDCC.OppositionUserIDs, ",") 223 cleanedOpposed := make([]string, 0, len(oppositionUserIDs)) 224 for _, oppose := range oppositionUserIDs { 225 cleanedOpposed = append(cleanedOpposed, strings.TrimSpace(oppose)) 226 } 227 dccRecord.OppositionUserIDs = cleanedOpposed 228 229 return dccRecord 230 } 231 232 func convertDCCDatabaseFromDCCRecord(dccRecord cms.DCCRecord) cmsdatabase.DCC { 233 dbDCC := cmsdatabase.DCC{} 234 235 dbDCC.Type = dccRecord.DCC.Type 236 dbDCC.NomineeUserID = dccRecord.DCC.NomineeUserID 237 dbDCC.SponsorStatement = dccRecord.DCC.SponsorStatement 238 dbDCC.Domain = dccRecord.DCC.Domain 239 dbDCC.ContractorType = dccRecord.DCC.ContractorType 240 dbDCC.Status = dccRecord.Status 241 dbDCC.StatusChangeReason = dccRecord.StatusChangeReason 242 dbDCC.TimeSubmitted = dccRecord.TimeSubmitted 243 dbDCC.TimeReviewed = dccRecord.TimeReviewed 244 dbDCC.Token = dccRecord.CensorshipRecord.Token 245 dbDCC.PublicKey = dccRecord.PublicKey 246 dbDCC.ServerSignature = dccRecord.Signature 247 dbDCC.SponsorUserID = dccRecord.SponsorUserID 248 dbDCC.Token = dccRecord.CensorshipRecord.Token 249 250 supportUserIDs := "" 251 for i, s := range dccRecord.SupportUserIDs { 252 if i == 0 { 253 supportUserIDs += s 254 } else { 255 supportUserIDs += "," + s 256 } 257 } 258 dbDCC.SupportUserIDs = supportUserIDs 259 260 oppositionUserIDs := "" 261 for i, s := range dccRecord.OppositionUserIDs { 262 if i == 0 { 263 oppositionUserIDs += s 264 } else { 265 oppositionUserIDs += "," + s 266 } 267 } 268 dbDCC.OppositionUserIDs = oppositionUserIDs 269 270 return dbDCC 271 } 272 273 func convertCastVoteFromCMS(b cms.CastVote) cmsplugin.CastVote { 274 return cmsplugin.CastVote{ 275 VoteBit: b.VoteBit, 276 Token: b.Token, 277 UserID: b.UserID, 278 Signature: b.Signature, 279 } 280 } 281 282 func convertCastVoteReplyToCMS(cv *cmsplugin.CastVoteReply) *cms.CastVoteReply { 283 return &cms.CastVoteReply{ 284 ClientSignature: cv.ClientSignature, 285 Signature: cv.Signature, 286 Error: cv.Error, 287 ErrorStatus: cv.ErrorStatus, 288 } 289 } 290 291 func convertUserWeightToCMS(uw []cmsplugin.UserWeight) []cms.DCCWeight { 292 dccWeight := make([]cms.DCCWeight, 0, len(uw)) 293 for _, w := range uw { 294 dccWeight = append(dccWeight, cms.DCCWeight{ 295 UserID: w.UserID, 296 Weight: w.Weight, 297 }) 298 } 299 return dccWeight 300 } 301 302 func convertVoteOptionResultsToCMS(vr []cmsplugin.VoteOptionResult) []cms.VoteOptionResult { 303 votes := make([]cms.VoteOptionResult, 0, len(vr)) 304 for _, w := range vr { 305 votes = append(votes, cms.VoteOptionResult{ 306 Option: cms.VoteOption{ 307 Id: w.ID, 308 Description: w.Description, 309 Bits: w.Bits, 310 }, 311 VotesReceived: w.Votes, 312 }) 313 } 314 return votes 315 } 316 func convertCMSStartVoteToCMSVoteDetailsReply(sv cmsplugin.StartVote, svr cmsplugin.StartVoteReply) (*cms.VoteDetailsReply, error) { 317 voteb, err := cmsplugin.EncodeVote(sv.Vote) 318 if err != nil { 319 return nil, err 320 } 321 userWeights := make([]string, 0, len(sv.UserWeights)) 322 for _, weights := range sv.UserWeights { 323 userWeight := weights.UserID + "-" + strconv.Itoa(int(weights.Weight)) 324 userWeights = append(userWeights, userWeight) 325 } 326 return &cms.VoteDetailsReply{ 327 Version: uint32(sv.Version), 328 Vote: string(voteb), 329 PublicKey: sv.PublicKey, 330 Signature: sv.Signature, 331 StartBlockHeight: svr.StartBlockHeight, 332 StartBlockHash: svr.StartBlockHash, 333 EndBlockHeight: svr.EndHeight, 334 UserWeights: userWeights, 335 }, nil 336 } 337 338 func convertCMSStartVoteToCMS(sv cmsplugin.StartVote) cms.StartVote { 339 vote := cms.Vote{ 340 Token: sv.Vote.Token, 341 Mask: sv.Vote.Mask, 342 Duration: sv.Vote.Duration, 343 QuorumPercentage: sv.Vote.QuorumPercentage, 344 PassPercentage: sv.Vote.PassPercentage, 345 } 346 347 voteOptions := make([]cms.VoteOption, 0, len(sv.Vote.Options)) 348 for _, option := range sv.Vote.Options { 349 voteOption := cms.VoteOption{ 350 Id: option.Id, 351 Description: option.Description, 352 Bits: option.Bits, 353 } 354 voteOptions = append(voteOptions, voteOption) 355 } 356 vote.Options = voteOptions 357 358 return cms.StartVote{ 359 Vote: vote, 360 PublicKey: sv.PublicKey, 361 Signature: sv.Signature, 362 } 363 } 364 365 func convertCMSStartVoteReplyToCMS(svr cmsplugin.StartVoteReply) cms.StartVoteReply { 366 return cms.StartVoteReply{ 367 StartBlockHeight: svr.StartBlockHeight, 368 StartBlockHash: svr.StartBlockHash, 369 EndBlockHeight: svr.EndHeight, 370 } 371 } 372 373 func convertStartVoteToCMS(sv cms.StartVote) cmsplugin.StartVote { 374 vote := cmsplugin.Vote{ 375 Token: sv.Vote.Token, 376 Mask: sv.Vote.Mask, 377 Duration: sv.Vote.Duration, 378 QuorumPercentage: sv.Vote.QuorumPercentage, 379 PassPercentage: sv.Vote.PassPercentage, 380 } 381 382 voteOptions := make([]cmsplugin.VoteOption, 0, len(sv.Vote.Options)) 383 for _, option := range sv.Vote.Options { 384 voteOption := cmsplugin.VoteOption{ 385 Id: option.Id, 386 Description: option.Description, 387 Bits: option.Bits, 388 } 389 voteOptions = append(voteOptions, voteOption) 390 } 391 vote.Options = voteOptions 392 393 return cmsplugin.StartVote{ 394 Token: sv.Vote.Token, 395 Vote: vote, 396 PublicKey: sv.PublicKey, 397 Signature: sv.Signature, 398 } 399 400 } 401 402 func (p *Politeiawww) processNewDCC(ctx context.Context, nd cms.NewDCC, u *user.User) (*cms.NewDCCReply, error) { 403 reply := &cms.NewDCCReply{} 404 405 err := p.validateDCC(nd, u) 406 if err != nil { 407 return nil, err 408 } 409 410 cmsUser, err := p.getCMSUserByID(u.ID.String()) 411 if err != nil { 412 return nil, err 413 } 414 415 // Ensure that the user is authorized to create DCCs 416 if _, ok := invalidDCCContractorType[cmsUser.ContractorType]; ok { 417 return nil, www.UserError{ 418 ErrorCode: cms.ErrorStatusInvalidUserDCC, 419 } 420 } 421 422 m := mdstream.DCCGeneral{ 423 Version: mdstream.VersionDCCGeneral, 424 Timestamp: time.Now().Unix(), 425 PublicKey: nd.PublicKey, 426 Signature: nd.Signature, 427 } 428 md, err := mdstream.EncodeDCCGeneral(m) 429 if err != nil { 430 return nil, err 431 } 432 433 sc := mdstream.DCCStatusChange{ 434 Version: mdstream.VersionDCCStatusChange, 435 Timestamp: time.Now().Unix(), 436 NewStatus: cms.DCCStatusActive, 437 Reason: "new dcc", 438 } 439 scb, err := mdstream.EncodeDCCStatusChange(sc) 440 if err != nil { 441 return nil, err 442 } 443 444 // Create expected []www.File from single dcc.json file 445 files := make([]www.File, 0, 1) 446 files = append(files, nd.File) 447 448 // Setup politeiad request 449 challenge, err := util.Random(pd.ChallengeSize) 450 if err != nil { 451 return nil, err 452 } 453 n := pd.NewRecord{ 454 Challenge: hex.EncodeToString(challenge), 455 Metadata: []pd.MetadataStream{ 456 { 457 ID: mdstream.IDDCCGeneral, 458 Payload: string(md), 459 }, 460 { 461 ID: mdstream.IDDCCStatusChange, 462 Payload: string(scb), 463 }, 464 }, 465 Files: convertPDFilesFromWWW(files), 466 } 467 468 // Send the newrecord politeiad request 469 responseBody, err := p.makeRequest(ctx, http.MethodPost, 470 pd.NewRecordRoute, n) 471 if err != nil { 472 return nil, err 473 } 474 475 log.Infof("Submitted issuance nomination: %v", u.Username) 476 for k, f := range n.Files { 477 log.Infof("%02v: %v %v", k, f.Name, f.Digest) 478 } 479 480 // Handle newRecord response 481 var pdReply pd.NewRecordReply 482 err = json.Unmarshal(responseBody, &pdReply) 483 if err != nil { 484 return nil, fmt.Errorf("Unmarshal NewDCCReply: %v", err) 485 } 486 487 // Verify NewRecord challenge 488 err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response) 489 if err != nil { 490 return nil, err 491 } 492 493 // Change politeiad record status to public. DCCs 494 // do not need to be reviewed before becoming public. 495 // An admin pubkey and signature are not included for 496 // this reason. 497 c := mdstream.RecordStatusChangeV2{ 498 Version: mdstream.VersionRecordStatusChange, 499 Timestamp: time.Now().Unix(), 500 NewStatus: pd.RecordStatusPublic, 501 } 502 blob, err := mdstream.EncodeRecordStatusChangeV2(c) 503 if err != nil { 504 return nil, err 505 } 506 507 challenge, err = util.Random(pd.ChallengeSize) 508 if err != nil { 509 return nil, err 510 } 511 512 sus := pd.SetUnvettedStatus{ 513 Token: pdReply.CensorshipRecord.Token, 514 Status: pd.RecordStatusPublic, 515 Challenge: hex.EncodeToString(challenge), 516 MDAppend: []pd.MetadataStream{ 517 { 518 ID: mdstream.IDRecordStatusChange, 519 Payload: string(blob), 520 }, 521 }, 522 } 523 524 // Send SetUnvettedStatus request to politeiad 525 responseBody, err = p.makeRequest(ctx, http.MethodPost, 526 pd.SetUnvettedStatusRoute, sus) 527 if err != nil { 528 return nil, err 529 } 530 531 var pdSetUnvettedStatusReply pd.SetUnvettedStatusReply 532 err = json.Unmarshal(responseBody, &pdSetUnvettedStatusReply) 533 if err != nil { 534 return nil, fmt.Errorf("Could not unmarshal SetUnvettedStatusReply: %v", 535 err) 536 } 537 538 // Verify the SetUnvettedStatus challenge. 539 err = util.VerifyChallenge(p.cfg.Identity, challenge, 540 pdSetUnvettedStatusReply.Response) 541 if err != nil { 542 return nil, err 543 } 544 545 r := pd.Record{ 546 Metadata: n.Metadata, 547 Files: n.Files, 548 CensorshipRecord: pdReply.CensorshipRecord, 549 } 550 551 // Submit issuance to cmsdb 552 dccRec, err := convertRecordToDatabaseDCC(r) 553 if err != nil { 554 return nil, err 555 } 556 557 err = p.cmsDB.NewDCC(dccRec) 558 if err != nil { 559 return nil, err 560 } 561 562 // Emit event notification for new DCC being submitted 563 p.events.Emit(eventDCCNew, 564 dataDCCNew{ 565 token: pdReply.CensorshipRecord.Token, 566 }) 567 568 cr := convertWWWCensorFromPD(pdReply.CensorshipRecord) 569 570 reply.CensorshipRecord = cr 571 return reply, nil 572 } 573 574 func (p *Politeiawww) validateDCC(nd cms.NewDCC, u *user.User) error { 575 // Obtain signature 576 sig, err := util.ConvertSignature(nd.Signature) 577 if err != nil { 578 return www.UserError{ 579 ErrorCode: www.ErrorStatusInvalidSignature, 580 } 581 } 582 583 // Verify public key 584 if u.PublicKey() != nd.PublicKey { 585 return www.UserError{ 586 ErrorCode: www.ErrorStatusInvalidSigningKey, 587 } 588 } 589 590 pk, err := identity.PublicIdentityFromBytes(u.ActiveIdentity().Key[:]) 591 if err != nil { 592 return err 593 } 594 595 // Check for at least 1 a non-empty payload. 596 if nd.File.Payload == "" { 597 return www.UserError{ 598 ErrorCode: www.ErrorStatusProposalMissingFiles, 599 } 600 } 601 602 v := nd.File 603 604 if v.Name != dccFile { 605 return www.UserError{ 606 ErrorCode: cms.ErrorStatusMalformedDCCFile, 607 } 608 } 609 610 data, err := base64.StdEncoding.DecodeString(v.Payload) 611 if err != nil { 612 return err 613 } 614 615 if len(data) > cms.PolicyMaxMDSize { 616 return www.UserError{ 617 ErrorCode: www.ErrorStatusMaxMDSizeExceededPolicy, 618 } 619 } 620 621 // Check to see if the data can be parsed properly into DCCInput 622 // struct. 623 var dcc cms.DCCInput 624 if err := json.Unmarshal(data, &dcc); err != nil { 625 return www.UserError{ 626 ErrorCode: cms.ErrorStatusMalformedDCCFile, 627 } 628 } 629 // Check UserID of Nominee 630 nomineeUser, err := p.getCMSUserByID(dcc.NomineeUserID) 631 if err != nil { 632 return www.UserError{ 633 ErrorCode: cms.ErrorStatusInvalidDCCNominee, 634 } 635 } 636 // All nominees, direct and subcontractors are allowed to be submitted 637 // for an issuance. 638 if (nomineeUser.ContractorType != cms.ContractorTypeNominee && 639 nomineeUser.ContractorType != cms.ContractorTypeDirect && 640 nomineeUser.ContractorType != cms.ContractorTypeSubContractor) && 641 dcc.Type == cms.DCCTypeIssuance { 642 return www.UserError{ 643 ErrorCode: cms.ErrorStatusInvalidDCCNominee, 644 } 645 } 646 647 // Ensure that nominated user doesn't already have a DCC 648 dccs, err := p.cmsDB.DCCsAll() 649 if err != nil { 650 return err 651 } 652 for _, existingDCC := range dccs { 653 if existingDCC.NomineeUserID == dcc.NomineeUserID { 654 return www.UserError{ 655 ErrorCode: cms.ErrorStatusInvalidDCCNominee} 656 } 657 } 658 659 sponsorUser, err := p.getCMSUserByID(u.ID.String()) 660 if err != nil { 661 return err 662 } 663 664 // Check that domains match 665 if sponsorUser.Domain != dcc.Domain { 666 return www.UserError{ 667 ErrorCode: cms.ErrorStatusInvalidNominatingDomain, 668 } 669 } 670 671 // Validate sponsor statement input 672 statement := formatSponsorStatement(dcc.SponsorStatement) 673 if !validateSponsorStatement(statement) { 674 return www.UserError{ 675 ErrorCode: cms.ErrorStatusMalformedSponsorStatement, 676 } 677 } 678 679 // Check to see that ContractorType is valid for any issuance 680 // DCC Proposal 681 if dcc.Type == cms.DCCTypeIssuance && 682 dcc.ContractorType != cms.ContractorTypeDirect && 683 dcc.ContractorType != cms.ContractorTypeSubContractor { 684 return www.UserError{ 685 ErrorCode: cms.ErrorStatusInvalidDCCContractorType, 686 } 687 } 688 689 // Check to see that if the issuance is for a subcontractor that the 690 // sponsor user is a supervisor. 691 if dcc.Type == cms.DCCTypeIssuance && 692 dcc.ContractorType == cms.ContractorTypeSubContractor && 693 sponsorUser.ContractorType != cms.ContractorTypeSupervisor { 694 return www.UserError{ 695 ErrorCode: cms.ErrorStatusInvalidDCCContractorType, 696 } 697 698 } 699 700 // Note that we need validate the string representation of the merkle 701 mr, err := merkleRoot([]www.File{nd.File}) 702 if err != nil { 703 return err 704 } 705 if !pk.VerifyMessage([]byte(mr), sig) { 706 return www.UserError{ 707 ErrorCode: www.ErrorStatusInvalidSignature, 708 } 709 } 710 return nil 711 } 712 713 // formatSponsorStatement normalizes a sponsor statement without leading and 714 // trailing spaces. 715 func formatSponsorStatement(statement string) string { 716 return strings.TrimSpace(statement) 717 } 718 719 // validateSponsorStatement verifies that a field filled out in invoice.json is 720 // valid 721 func validateSponsorStatement(statement string) bool { 722 if statement != formatSponsorStatement(statement) { 723 log.Tracef("validateSponsorStatement: not normalized: %s %s", 724 statement, formatSponsorStatement(statement)) 725 return false 726 } 727 if len(statement) > cms.PolicyMaxSponsorStatementLength || 728 len(statement) < cms.PolicyMinSponsorStatementLength { 729 log.Tracef("validateSponsorStatement: not within bounds: have %v expected > %v < %v", 730 len(statement), cms.PolicyMaxSponsorStatementLength, 731 cms.PolicyMinSponsorStatementLength) 732 return false 733 } 734 if !validSponsorStatement.MatchString(statement) { 735 log.Tracef("validateSponsorStatement: not valid: %s %s", 736 statement, validSponsorStatement.String()) 737 return false 738 } 739 return true 740 } 741 742 // getDCC gets the most recent verions of the given DCC from the cmsDB 743 // then fills in any missing user fields before returning the DCC record. 744 func (p *Politeiawww) getDCC(token string) (*cms.DCCRecord, error) { 745 // Get dcc from cmsdb 746 r, err := p.cmsDB.DCCByToken(token) 747 if err != nil { 748 return nil, err 749 } 750 i := convertDCCDatabaseToRecord(r) 751 752 // Check for possible malformed DCC 753 if i.PublicKey == "" { 754 return nil, www.UserError{ 755 ErrorCode: cms.ErrorStatusMalformedDCC, 756 } 757 } 758 759 // Get user IDs of support/oppose pubkeys 760 supportUserIDs := make([]string, 0, len(i.SupportUserIDs)) 761 opposeUserIDs := make([]string, 0, len(i.OppositionUserIDs)) 762 supportUsernames := make([]string, 0, len(i.SupportUserIDs)) 763 opposeUsernames := make([]string, 0, len(i.OppositionUserIDs)) 764 for _, v := range i.SupportUserIDs { 765 // Fill in userID and username fields 766 u, err := p.db.UserGetByPubKey(v) 767 if err != nil { 768 log.Errorf("getDCC: getUserByPubKey: token:%v "+ 769 "pubKey:%v err:%v", token, v, err) 770 } else { 771 supportUserIDs = append(supportUserIDs, u.ID.String()) 772 supportUsernames = append(supportUsernames, u.Username) 773 } 774 } 775 for _, v := range i.OppositionUserIDs { 776 // Fill in userID and username fields 777 u, err := p.db.UserGetByPubKey(v) 778 if err != nil { 779 log.Errorf("getDCC: getUserByPubKey: token:%v "+ 780 "pubKey:%v err:%v", token, v, err) 781 } else { 782 opposeUserIDs = append(opposeUserIDs, u.ID.String()) 783 opposeUsernames = append(opposeUsernames, u.Username) 784 } 785 } 786 i.SupportUserIDs = supportUserIDs 787 i.OppositionUserIDs = opposeUserIDs 788 i.SupportUsernames = supportUsernames 789 i.OppositionUsernames = opposeUsernames 790 791 // Fill in sponsoring userID and username fields 792 u, err := p.db.UserGetByPubKey(i.PublicKey) 793 if err != nil { 794 log.Errorf("getDCC: getUserByPubKey: token:%v "+ 795 "pubKey:%v err:%v", token, i.PublicKey, err) 796 } else { 797 i.SponsorUserID = u.ID.String() 798 i.SponsorUsername = u.Username 799 } 800 801 // Fill in nominee username 802 803 nomineeUser, err := p.getCMSUserByID(i.DCC.NomineeUserID) 804 if err != nil { 805 log.Errorf("getDCC: getCMSUserByID: token:%v "+ 806 "userid:%v err:%v", token, i.DCC.NomineeUserID, err) 807 } else { 808 i.NomineeUsername = nomineeUser.Username 809 } 810 811 return &i, nil 812 } 813 814 func (p *Politeiawww) processDCCDetails(ctx context.Context, gd cms.DCCDetails) (*cms.DCCDetailsReply, error) { 815 log.Tracef("processDCCDetails: %v", gd.Token) 816 vdr, err := p.cmsVoteDetails(ctx, gd.Token) 817 if err != nil { 818 return nil, err 819 } 820 821 vsr, err := p.cmsVoteSummary(ctx, gd.Token) 822 if err != nil { 823 return nil, err 824 } 825 826 dcc, err := p.getDCC(gd.Token) 827 if err != nil { 828 if errors.Is(err, cmsdatabase.ErrDCCNotFound) { 829 err = www.UserError{ 830 ErrorCode: cms.ErrorStatusDCCNotFound, 831 } 832 return nil, err 833 } 834 } 835 836 voteResults := convertVoteOptionResultsToCMS(vsr.Results) 837 838 voteSummary := cms.VoteSummary{ 839 UserWeights: convertUserWeightToCMS(vdr.StartVote.UserWeights), 840 EndHeight: vsr.EndHeight, 841 Results: voteResults, 842 Duration: vsr.Duration, 843 PassPercentage: vsr.PassPercentage, 844 } 845 reply := &cms.DCCDetailsReply{ 846 DCC: *dcc, 847 VoteSummary: voteSummary, 848 } 849 return reply, nil 850 } 851 852 func (p *Politeiawww) processGetDCCs(gds cms.GetDCCs) (*cms.GetDCCsReply, error) { 853 log.Tracef("processGetDCCs: %v", gds.Status) 854 855 var dbDCCs []*cmsdatabase.DCC 856 var err error 857 switch { 858 case gds.Status != 0: 859 dbDCCs, err = p.cmsDB.DCCsByStatus(int(gds.Status)) 860 if err != nil { 861 return nil, err 862 } 863 864 default: 865 dbDCCs, err = p.cmsDB.DCCsAll() 866 if err != nil { 867 return nil, err 868 } 869 } 870 dccs := make([]cms.DCCRecord, 0, len(dbDCCs)) 871 872 for _, v := range dbDCCs { 873 dcc, err := p.getDCC(v.Token) 874 if err != nil { 875 log.Errorf("getDCCs: getDCC %v %v", v.Token, err) 876 // Just skip to the next one but carry on with the rest. 877 continue 878 } 879 dccs = append(dccs, *dcc) 880 } 881 882 return &cms.GetDCCsReply{ 883 DCCs: dccs, 884 }, nil 885 } 886 887 func (p *Politeiawww) processSupportOpposeDCC(ctx context.Context, sd cms.SupportOpposeDCC, u *user.User) (*cms.SupportOpposeDCCReply, error) { 888 log.Tracef("processSupportOpposeDCC: %v %v", sd.Token, u.ID) 889 890 // The submitted Vote in the request must either be "aye" or "nay" 891 if sd.Vote != supportString && sd.Vote != opposeString { 892 return nil, www.UserError{ 893 ErrorCode: cms.ErrorStatusInvalidSupportOppose, 894 ErrorContext: []string{"support string not aye or nay"}, 895 } 896 } 897 898 // Validate signature 899 msg := fmt.Sprintf("%v%v", sd.Token, sd.Vote) 900 err := validateSignature(sd.PublicKey, sd.Signature, msg) 901 if err != nil { 902 return nil, err 903 } 904 905 dcc, err := p.getDCC(sd.Token) 906 if err != nil { 907 if errors.Is(err, cmsdatabase.ErrDCCNotFound) { 908 err = www.UserError{ 909 ErrorCode: cms.ErrorStatusDCCNotFound, 910 } 911 return nil, err 912 } 913 } 914 915 // Check to make sure the user has not SupportOpposeed or Opposed this DCC yet 916 if stringInSlice(dcc.SupportUserIDs, u.ID.String()) || 917 stringInSlice(dcc.OppositionUserIDs, u.ID.String()) { 918 return nil, www.UserError{ 919 ErrorCode: cms.ErrorStatusDuplicateSupportOppose, 920 } 921 } 922 923 // Check to make sure the user is not the author of the DCC. 924 if dcc.SponsorUserID == u.ID.String() { 925 return nil, www.UserError{ 926 ErrorCode: cms.ErrorStatusUserIsAuthor, 927 } 928 } 929 930 // Check to make sure that the DCC is still active 931 if dcc.Status != cms.DCCStatusActive { 932 return nil, www.UserError{ 933 ErrorCode: cms.ErrorStatusWrongDCCStatus, 934 ErrorContext: []string{"dcc status must be active"}, 935 } 936 } 937 938 cmsUser, err := p.getCMSUserByID(u.ID.String()) 939 if err != nil { 940 return nil, err 941 } 942 943 // Ensure that the user is authorized to support/oppose DCCs 944 if _, ok := invalidDCCContractorType[cmsUser.ContractorType]; ok { 945 return nil, www.UserError{ 946 ErrorCode: cms.ErrorStatusInvalidUserDCC, 947 } 948 } 949 950 // Create the support/opposition record. 951 c := mdstream.DCCSupportOpposition{ 952 Version: mdstream.VersionDCCSupposeOpposition, 953 PublicKey: sd.PublicKey, 954 Timestamp: time.Now().Unix(), 955 Vote: sd.Vote, 956 Signature: sd.Signature, 957 } 958 blob, err := mdstream.EncodeDCCSupportOpposition(c) 959 if err != nil { 960 return nil, err 961 } 962 963 challenge, err := util.Random(pd.ChallengeSize) 964 if err != nil { 965 return nil, err 966 } 967 968 pdCommand := pd.UpdateVettedMetadata{ 969 Challenge: hex.EncodeToString(challenge), 970 Token: sd.Token, 971 MDAppend: []pd.MetadataStream{ 972 { 973 ID: mdstream.IDDCCSupportOpposition, 974 Payload: string(blob), 975 }, 976 }, 977 } 978 979 responseBody, err := p.makeRequest(ctx, http.MethodPost, 980 pd.UpdateVettedMetadataRoute, pdCommand) 981 if err != nil { 982 return nil, err 983 } 984 985 var pdReply pd.UpdateVettedMetadataReply 986 err = json.Unmarshal(responseBody, &pdReply) 987 if err != nil { 988 return nil, fmt.Errorf("Could not unmarshal UpdateVettedMetadataReply: %v", 989 err) 990 } 991 992 // Verify the UpdateVettedMetadata challenge. 993 err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response) 994 if err != nil { 995 return nil, err 996 } 997 998 if sd.Vote == supportString { 999 dcc.SupportUserIDs = append(dcc.SupportUserIDs, sd.PublicKey) 1000 } else if sd.Vote == opposeString { 1001 dcc.OppositionUserIDs = append(dcc.OppositionUserIDs, sd.PublicKey) 1002 } 1003 dbDcc := convertDCCDatabaseFromDCCRecord(*dcc) 1004 if err != nil { 1005 return nil, err 1006 } 1007 err = p.cmsDB.UpdateDCC(&dbDcc) 1008 if err != nil { 1009 return nil, err 1010 } 1011 1012 // Emit event notification for a DCC being supported/opposed 1013 p.events.Emit(eventDCCSupportOppose, 1014 dataDCCSupportOppose{ 1015 token: sd.Token, 1016 }) 1017 1018 return &cms.SupportOpposeDCCReply{}, nil 1019 } 1020 1021 func stringInSlice(arr []string, str string) bool { 1022 for _, s := range arr { 1023 if str == s { 1024 return true 1025 } 1026 } 1027 1028 return false 1029 } 1030 1031 func validateNewComment(c www.NewComment) error { 1032 // Validate token 1033 _, err := util.ConvertStringToken(c.Token) 1034 if err != nil { 1035 return www.UserError{ 1036 ErrorCode: www.ErrorStatusInvalidCensorshipToken, 1037 } 1038 } 1039 // Validate max length 1040 if len(c.Comment) > www.PolicyMaxCommentLength { 1041 return www.UserError{ 1042 ErrorCode: www.ErrorStatusCommentLengthExceededPolicy, 1043 } 1044 } 1045 return nil 1046 } 1047 1048 // processNewCommentDCC sends a new comment decred plugin command to politeaid 1049 // then fetches the new comment from the cache and returns it. 1050 func (p *Politeiawww) processNewCommentDCC(ctx context.Context, nc www.NewComment, u *user.User) (*www.NewCommentReply, error) { 1051 log.Tracef("processNewCommentDCC: %v %v", nc.Token, u.ID) 1052 1053 // Validate comment 1054 err := validateNewComment(nc) 1055 if err != nil { 1056 return nil, err 1057 } 1058 1059 // Ensure the public key is the user's active key 1060 if nc.PublicKey != u.PublicKey() { 1061 return nil, www.UserError{ 1062 ErrorCode: www.ErrorStatusInvalidSigningKey, 1063 } 1064 } 1065 1066 // Validate signature 1067 msg := nc.Token + nc.ParentID + nc.Comment 1068 err = validateSignature(nc.PublicKey, nc.Signature, msg) 1069 if err != nil { 1070 return nil, err 1071 } 1072 1073 cmsUser, err := p.getCMSUserByID(u.ID.String()) 1074 if err != nil { 1075 return nil, err 1076 } 1077 1078 // Ensure that the user is authorized to comment on a DCCs 1079 if _, ok := invalidDCCContractorType[cmsUser.ContractorType]; ok { 1080 return nil, www.UserError{ 1081 ErrorCode: cms.ErrorStatusInvalidUserDCC, 1082 } 1083 } 1084 1085 dcc, err := p.getDCC(nc.Token) 1086 if err != nil { 1087 if errors.Is(err, cmsdatabase.ErrDCCNotFound) { 1088 err = www.UserError{ 1089 ErrorCode: cms.ErrorStatusDCCNotFound, 1090 } 1091 return nil, err 1092 } 1093 } 1094 1095 // Check to make sure that dcc isn't already approved. 1096 if dcc.Status != cms.DCCStatusActive { 1097 return nil, www.UserError{ 1098 ErrorCode: cms.ErrorStatusWrongDCCStatus, 1099 ErrorContext: []string{"dcc status must be active"}, 1100 } 1101 } 1102 1103 // Setup plugin command 1104 challenge, err := util.Random(pd.ChallengeSize) 1105 if err != nil { 1106 return nil, err 1107 } 1108 1109 dnc := convertNewCommentToDecredPlugin(nc) 1110 payload, err := decredplugin.EncodeNewComment(dnc) 1111 if err != nil { 1112 return nil, err 1113 } 1114 1115 pc := pd.PluginCommand{ 1116 Challenge: hex.EncodeToString(challenge), 1117 ID: decredplugin.ID, 1118 Command: decredplugin.CmdNewComment, 1119 CommandID: decredplugin.CmdNewComment, 1120 Payload: string(payload), 1121 } 1122 1123 // Send polieiad request 1124 responseBody, err := p.makeRequest(ctx, http.MethodPost, 1125 pd.PluginCommandRoute, pc) 1126 if err != nil { 1127 return nil, err 1128 } 1129 1130 // Handle response 1131 var reply pd.PluginCommandReply 1132 err = json.Unmarshal(responseBody, &reply) 1133 if err != nil { 1134 return nil, fmt.Errorf("could not unmarshal "+ 1135 "PluginCommandReply: %v", err) 1136 } 1137 1138 err = util.VerifyChallenge(p.cfg.Identity, challenge, reply.Response) 1139 if err != nil { 1140 return nil, err 1141 } 1142 1143 ncr, err := decredplugin.DecodeNewCommentReply([]byte(reply.Payload)) 1144 if err != nil { 1145 return nil, err 1146 } 1147 1148 // Get comment 1149 comments, err := p.getDCCComments(ctx, nc.Token) 1150 if err != nil { 1151 return nil, fmt.Errorf("getComments: %v", err) 1152 } 1153 var c www.Comment 1154 for _, v := range comments { 1155 if v.CommentID == ncr.CommentID { 1156 c = v 1157 break 1158 } 1159 } 1160 1161 return &www.NewCommentReply{ 1162 Comment: c, 1163 }, nil 1164 } 1165 1166 // processDCCComments returns all comments for a given dcc. If the user is 1167 // logged in the user's last access time for the given comments will also be 1168 // returned. 1169 func (p *Politeiawww) processDCCComments(ctx context.Context, token string, u *user.User) (*www.GetCommentsReply, error) { 1170 log.Tracef("processDCCComment: %v", token) 1171 1172 // Fetch dcc comments from cache 1173 c, err := p.getDCCComments(ctx, token) 1174 if err != nil { 1175 return nil, err 1176 } 1177 1178 // Get the last time the user accessed these comments. This is 1179 // a public route so a user may not exist. 1180 var accessTime int64 1181 if u != nil { 1182 if u.ProposalCommentsAccessTimes == nil { 1183 u.ProposalCommentsAccessTimes = make(map[string]int64) 1184 } 1185 accessTime = u.ProposalCommentsAccessTimes[token] 1186 u.ProposalCommentsAccessTimes[token] = time.Now().Unix() 1187 err = p.db.UserUpdate(*u) 1188 if err != nil { 1189 return nil, err 1190 } 1191 } 1192 1193 return &www.GetCommentsReply{ 1194 Comments: c, 1195 AccessTime: accessTime, 1196 }, nil 1197 } 1198 1199 func (p *Politeiawww) getDCCComments(ctx context.Context, token string) ([]www.Comment, error) { 1200 log.Tracef("getDCCComments: %v", token) 1201 1202 dc, err := p.decredGetComments(ctx, token) 1203 if err != nil { 1204 return nil, fmt.Errorf("decredGetComments: %v", err) 1205 } 1206 1207 // Convert comments and fill in author info. 1208 comments := make([]www.Comment, 0, len(dc)) 1209 for _, v := range dc { 1210 c := convertCommentFromDecred(v) 1211 u, err := p.db.UserGetByPubKey(c.PublicKey) 1212 if err != nil { 1213 log.Errorf("getDCCComments: UserGetByPubKey: "+ 1214 "token:%v commentID:%v pubKey:%v err:%v", 1215 token, c.CommentID, c.PublicKey, err) 1216 } else { 1217 c.UserID = u.ID.String() 1218 c.Username = u.Username 1219 } 1220 comments = append(comments, c) 1221 } 1222 1223 return comments, nil 1224 } 1225 1226 func (p *Politeiawww) processSetDCCStatus(ctx context.Context, sds cms.SetDCCStatus, u *user.User) (*cms.SetDCCStatusReply, error) { 1227 log.Tracef("processSetDCCStatus: %v", u.PublicKey()) 1228 1229 // Ensure the provided public key is the user's active key. 1230 if sds.PublicKey != u.PublicKey() { 1231 return nil, www.UserError{ 1232 ErrorCode: www.ErrorStatusInvalidSigningKey, 1233 } 1234 } 1235 1236 // Validate signature 1237 msg := fmt.Sprintf("%v%v%v", sds.Token, int(sds.Status), sds.Reason) 1238 err := validateSignature(sds.PublicKey, sds.Signature, msg) 1239 if err != nil { 1240 return nil, err 1241 } 1242 1243 dcc, err := p.getDCC(sds.Token) 1244 if err != nil { 1245 if errors.Is(err, cmsdatabase.ErrDCCNotFound) { 1246 err = www.UserError{ 1247 ErrorCode: cms.ErrorStatusDCCNotFound, 1248 } 1249 return nil, err 1250 } 1251 } 1252 1253 err = validateDCCStatusTransition(dcc.Status, sds.Status, sds.Reason) 1254 if err != nil { 1255 return nil, err 1256 } 1257 1258 // Validate vote status 1259 vsr, err := p.cmsVoteSummary(ctx, sds.Token) 1260 if err != nil { 1261 return nil, err 1262 } 1263 1264 // Only allow voting on All Vote DCC proposals 1265 // Get vote summary to check vote status 1266 bb, err := p.decredBestBlock(ctx) 1267 if err != nil { 1268 return nil, err 1269 } 1270 1271 voteStatus := dccVoteStatusFromVoteSummary(*vsr, bb) 1272 switch voteStatus { 1273 case cms.DCCVoteStatusStarted: 1274 return nil, www.UserError{ 1275 ErrorCode: www.ErrorStatusWrongVoteStatus, 1276 ErrorContext: []string{"vote has not finished"}, 1277 } 1278 case cms.DCCVoteStatusInvalid: 1279 return nil, www.UserError{ 1280 ErrorCode: www.ErrorStatusWrongVoteStatus, 1281 } 1282 } 1283 1284 // Create the change record. 1285 c := mdstream.DCCStatusChange{ 1286 Version: mdstream.VersionDCCStatusChange, 1287 AdminPublicKey: u.PublicKey(), 1288 Timestamp: time.Now().Unix(), 1289 NewStatus: sds.Status, 1290 Reason: sds.Reason, 1291 Signature: sds.Signature, 1292 } 1293 blob, err := mdstream.EncodeDCCStatusChange(c) 1294 if err != nil { 1295 return nil, err 1296 } 1297 1298 challenge, err := util.Random(pd.ChallengeSize) 1299 if err != nil { 1300 return nil, err 1301 } 1302 1303 pdCommand := pd.UpdateVettedMetadata{ 1304 Challenge: hex.EncodeToString(challenge), 1305 Token: sds.Token, 1306 MDAppend: []pd.MetadataStream{ 1307 { 1308 ID: mdstream.IDDCCStatusChange, 1309 Payload: string(blob), 1310 }, 1311 }, 1312 } 1313 1314 responseBody, err := p.makeRequest(ctx, http.MethodPost, 1315 pd.UpdateVettedMetadataRoute, pdCommand) 1316 if err != nil { 1317 return nil, err 1318 } 1319 1320 var pdReply pd.UpdateVettedMetadataReply 1321 err = json.Unmarshal(responseBody, &pdReply) 1322 if err != nil { 1323 return nil, fmt.Errorf("Could not unmarshal UpdateVettedMetadataReply: %v", 1324 err) 1325 } 1326 1327 // Verify the UpdateVettedMetadata challenge. 1328 err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response) 1329 if err != nil { 1330 return nil, err 1331 } 1332 1333 switch sds.Status { 1334 case cms.DCCStatusApproved: 1335 switch dcc.DCC.Type { 1336 case cms.DCCTypeIssuance: 1337 // Do DCC user Issuance processing 1338 err := p.issuanceDCCUser(dcc.DCC.NomineeUserID, dcc.SponsorUserID, 1339 int(dcc.DCC.Domain), int(dcc.DCC.ContractorType)) 1340 if err != nil { 1341 return nil, err 1342 } 1343 case cms.DCCTypeRevocation: 1344 // Do DCC user Revocation processing 1345 err = p.revokeDCCUser(dcc.DCC.NomineeUserID) 1346 if err != nil { 1347 return nil, err 1348 } 1349 } 1350 } 1351 1352 dbDCC, err := p.cmsDB.DCCByToken(sds.Token) 1353 if err != nil { 1354 return nil, err 1355 } 1356 dbDCC.Status = sds.Status 1357 dbDCC.StatusChangeReason = sds.Reason 1358 1359 // Update cmsdb 1360 err = p.cmsDB.UpdateDCC(dbDCC) 1361 if err != nil { 1362 return nil, err 1363 } 1364 1365 return &cms.SetDCCStatusReply{}, nil 1366 } 1367 1368 func validateDCCStatusTransition(oldStatus cms.DCCStatusT, newStatus cms.DCCStatusT, reason string) error { 1369 validStatuses, ok := validDCCStatusTransitions[oldStatus] 1370 if !ok { 1371 log.Debugf("status not supported: %v", oldStatus) 1372 return www.UserError{ 1373 ErrorCode: cms.ErrorStatusInvalidDCCStatusTransition, 1374 } 1375 } 1376 1377 if !dccStatusInSlice(validStatuses, newStatus) { 1378 return www.UserError{ 1379 ErrorCode: cms.ErrorStatusInvalidDCCStatusTransition, 1380 } 1381 } 1382 1383 if (newStatus == cms.DCCStatusApproved || 1384 newStatus == cms.DCCStatusRejected) && reason == "" { 1385 return www.UserError{ 1386 ErrorCode: cms.ErrorStatusReasonNotProvided, 1387 } 1388 } 1389 return nil 1390 } 1391 1392 func dccStatusInSlice(arr []cms.DCCStatusT, status cms.DCCStatusT) bool { 1393 for _, s := range arr { 1394 if status == s { 1395 return true 1396 } 1397 } 1398 1399 return false 1400 } 1401 1402 func (p *Politeiawww) processCastVoteDCC(ctx context.Context, cv cms.CastVote, u *user.User) (*cms.CastVoteReply, error) { 1403 log.Tracef("processCastVoteDCC: %v", u.PublicKey()) 1404 1405 vdr, err := p.cmsVoteDetails(ctx, cv.Token) 1406 if err != nil { 1407 return nil, err 1408 } 1409 1410 validVoteBit := false 1411 for _, option := range vdr.StartVote.Vote.Options { 1412 if cv.VoteBit == strconv.FormatUint(option.Bits, 16) { 1413 validVoteBit = true 1414 break 1415 } 1416 } 1417 1418 if !validVoteBit { 1419 return nil, www.UserError{ 1420 ErrorCode: cms.ErrorStatusInvalidSupportOppose, 1421 ErrorContext: []string{"votebits not valid for given dcc vote"}, 1422 } 1423 } 1424 1425 // Only allow voting on All Vote DCC proposals 1426 // Get vote summary to check vote status 1427 1428 bb, err := p.decredBestBlock(ctx) 1429 if err != nil { 1430 return nil, err 1431 } 1432 1433 // Check to make sure that the Vote hasn't ended yet. 1434 if vdr.StartVoteReply.EndHeight < bb { 1435 return nil, www.UserError{ 1436 ErrorCode: cms.ErrorStatusDCCVoteEnded, 1437 } 1438 } 1439 1440 challenge, err := util.Random(pd.ChallengeSize) 1441 if err != nil { 1442 return nil, err 1443 } 1444 1445 vote := convertCastVoteFromCMS(cv) 1446 payload, err := cmsplugin.EncodeCastVote(vote) 1447 if err != nil { 1448 return nil, err 1449 } 1450 pc := pd.PluginCommand{ 1451 Challenge: hex.EncodeToString(challenge), 1452 ID: cmsplugin.ID, 1453 Command: cmsplugin.CmdCastVote, 1454 CommandID: cmsplugin.CmdCastVote, 1455 Payload: string(payload), 1456 } 1457 1458 responseBody, err := p.makeRequest(ctx, http.MethodPost, 1459 pd.PluginCommandRoute, pc) 1460 if err != nil { 1461 return nil, err 1462 } 1463 1464 var reply pd.PluginCommandReply 1465 err = json.Unmarshal(responseBody, &reply) 1466 if err != nil { 1467 return nil, fmt.Errorf("Could not unmarshal "+ 1468 "PluginCommandReply: %v", err) 1469 } 1470 1471 // Verify the challenge. 1472 err = util.VerifyChallenge(p.cfg.Identity, challenge, reply.Response) 1473 if err != nil { 1474 return nil, err 1475 } 1476 1477 // Decode plugin reply 1478 pluginCastVoteReply, err := cmsplugin.DecodeCastVoteReply([]byte(reply.Payload)) 1479 if err != nil { 1480 return nil, err 1481 } 1482 1483 return convertCastVoteReplyToCMS(pluginCastVoteReply), nil 1484 } 1485 1486 func (p *Politeiawww) processVoteDetailsDCC(ctx context.Context, token string) (*cms.VoteDetailsReply, error) { 1487 log.Tracef("processVoteDetailsDCC: %v", token) 1488 1489 // Validate vote status 1490 dvdr, err := p.cmsVoteDetails(ctx, token) 1491 if err != nil { 1492 if errors.Is(err, cmsdatabase.ErrDCCNotFound) { 1493 err = www.UserError{ 1494 ErrorCode: cms.ErrorStatusDCCNotFound, 1495 } 1496 return nil, err 1497 } 1498 } 1499 if dvdr.StartVoteReply.StartBlockHash == "" { 1500 return nil, www.UserError{ 1501 ErrorCode: www.ErrorStatusWrongVoteStatus, 1502 ErrorContext: []string{"voting has not started yet"}, 1503 } 1504 } 1505 vdr, err := convertCMSStartVoteToCMSVoteDetailsReply(dvdr.StartVote, 1506 dvdr.StartVoteReply) 1507 if err != nil { 1508 return nil, err 1509 } 1510 1511 return vdr, nil 1512 } 1513 1514 // cmsVoteDetails sends the cms plugin votedetails command to the gitbe 1515 // and returns the vote details for the passed in proposal. 1516 func (p *Politeiawww) cmsVoteDetails(ctx context.Context, token string) (*cmsplugin.VoteDetailsReply, error) { 1517 // Setup plugin command 1518 vd := cmsplugin.VoteDetails{ 1519 Token: token, 1520 } 1521 payload, err := cmsplugin.EncodeVoteDetails(vd) 1522 if err != nil { 1523 return nil, err 1524 } 1525 challenge, err := util.Random(pd.ChallengeSize) 1526 if err != nil { 1527 return nil, err 1528 } 1529 pc := pd.PluginCommand{ 1530 Challenge: hex.EncodeToString(challenge), 1531 ID: cmsplugin.ID, 1532 Command: cmsplugin.CmdVoteDetails, 1533 Payload: string(payload), 1534 } 1535 responseBody, err := p.makeRequest(ctx, http.MethodPost, 1536 pd.PluginCommandRoute, pc) 1537 if err != nil { 1538 return nil, err 1539 } 1540 1541 // Handle reply 1542 var reply pd.PluginCommandReply 1543 err = json.Unmarshal(responseBody, &reply) 1544 if err != nil { 1545 return nil, fmt.Errorf("could not unmarshal "+ 1546 "PluginCommandReply: %v", err) 1547 } 1548 err = util.VerifyChallenge(p.cfg.Identity, challenge, reply.Response) 1549 if err != nil { 1550 return nil, err 1551 } 1552 vdr, err := cmsplugin.DecodeVoteDetailsReply([]byte(reply.Payload)) 1553 if err != nil { 1554 return nil, err 1555 } 1556 1557 return vdr, nil 1558 } 1559 1560 // cmsVoteSummary provides the current tally of a given DCC proposal based on 1561 // the provided token. 1562 func (p *Politeiawww) cmsVoteSummary(ctx context.Context, token string) (*cmsplugin.VoteSummaryReply, error) { 1563 // Setup plugin command 1564 vs := cmsplugin.VoteSummary{ 1565 Token: token, 1566 } 1567 payload, err := cmsplugin.EncodeVoteSummary(vs) 1568 if err != nil { 1569 return nil, err 1570 } 1571 challenge, err := util.Random(pd.ChallengeSize) 1572 if err != nil { 1573 return nil, err 1574 } 1575 pc := pd.PluginCommand{ 1576 Challenge: hex.EncodeToString(challenge), 1577 ID: cmsplugin.ID, 1578 Command: cmsplugin.CmdVoteSummary, 1579 Payload: string(payload), 1580 } 1581 responseBody, err := p.makeRequest(ctx, http.MethodPost, 1582 pd.PluginCommandRoute, pc) 1583 if err != nil { 1584 return nil, err 1585 } 1586 1587 // Handle reply 1588 var reply pd.PluginCommandReply 1589 err = json.Unmarshal(responseBody, &reply) 1590 if err != nil { 1591 return nil, fmt.Errorf("could not unmarshal "+ 1592 "PluginCommandReply: %v", err) 1593 } 1594 err = util.VerifyChallenge(p.cfg.Identity, challenge, reply.Response) 1595 if err != nil { 1596 return nil, err 1597 } 1598 vsr, err := cmsplugin.DecodeVoteSummaryReply([]byte(reply.Payload)) 1599 if err != nil { 1600 return nil, err 1601 } 1602 1603 return vsr, nil 1604 } 1605 1606 func (p *Politeiawww) processActiveVoteDCC(ctx context.Context) (*cms.ActiveVoteReply, error) { 1607 log.Tracef("processActiveVoteDCC") 1608 1609 // Request full record inventory from backend 1610 challenge, err := util.Random(pd.ChallengeSize) 1611 if err != nil { 1612 return nil, err 1613 } 1614 1615 pdCommand := pd.Inventory{ 1616 Challenge: hex.EncodeToString(challenge), 1617 IncludeFiles: true, 1618 AllVersions: true, 1619 } 1620 1621 responseBody, err := p.makeRequest(ctx, http.MethodPost, 1622 pd.InventoryRoute, pdCommand) 1623 if err != nil { 1624 return nil, err 1625 } 1626 1627 var pdReply pd.InventoryReply 1628 err = json.Unmarshal(responseBody, &pdReply) 1629 if err != nil { 1630 return nil, fmt.Errorf("Could not unmarshal InventoryReply: %v", 1631 err) 1632 } 1633 1634 // Verify the UpdateVettedMetadata challenge. 1635 err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response) 1636 if err != nil { 1637 return nil, err 1638 } 1639 vetted := pdReply.Vetted 1640 1641 bb, err := p.decredBestBlock(ctx) 1642 if err != nil { 1643 return nil, err 1644 } 1645 1646 active := make([]string, 0, len(vetted)) 1647 for _, r := range vetted { 1648 for _, m := range r.Metadata { 1649 switch m.ID { 1650 case mdstream.IDDCCGeneral: 1651 vs, err := p.cmsVoteSummary(ctx, r.CensorshipRecord.Token) 1652 if err != nil { 1653 log.Errorf("processActiveVotes: error pull cmsVoteSummary "+ 1654 "%v %v", r.CensorshipRecord.Token, err) 1655 continue 1656 } 1657 if vs.EndHeight > bb { 1658 active = append(active, r.CensorshipRecord.Token) 1659 } 1660 } 1661 } 1662 } 1663 1664 dccs, err := p.getDCCs(active) 1665 if err != nil { 1666 return nil, err 1667 } 1668 1669 // Compile dcc vote tuples 1670 vt := make([]cms.VoteTuple, 0, len(dccs)) 1671 for _, v := range dccs { 1672 // Get vote details from gitbe 1673 vdr, err := p.cmsVoteDetails(ctx, v.CensorshipRecord.Token) 1674 if err != nil { 1675 return nil, fmt.Errorf("decredVoteDetails %v: %v", 1676 v.CensorshipRecord.Token, err) 1677 } 1678 // Create vote tuple 1679 vt = append(vt, cms.VoteTuple{ 1680 DCC: v, 1681 StartVote: convertCMSStartVoteToCMS(vdr.StartVote), 1682 StartVoteReply: convertCMSStartVoteReplyToCMS(vdr.StartVoteReply), 1683 }) 1684 } 1685 1686 return &cms.ActiveVoteReply{ 1687 Votes: vt, 1688 }, nil 1689 } 1690 1691 // getDCCs returns a [token]cms.DCCRecord map for the provided list of 1692 // censorship tokens. If a proposal is not found, the map will not include an 1693 // entry for the corresponding censorship token. It is the responsibility of 1694 // the caller to ensure that results are returned for all of the provided 1695 // censorship tokens. 1696 func (p *Politeiawww) getDCCs(tokens []string) (map[string]cms.DCCRecord, error) { 1697 log.Tracef("getDCCs: %v", tokens) 1698 1699 // Use pointers for now so the props can be easily updated 1700 dccs := make(map[string]*cms.DCCRecord, len(tokens)) 1701 for _, token := range tokens { 1702 dcc, err := p.getDCC(token) 1703 if err != nil { 1704 log.Errorf("getDCCs: unable to getDCC for %v %v", token, err) 1705 } 1706 dccs[token] = dcc 1707 } 1708 1709 // Compile a list of unique proposal author pubkeys. These 1710 // are needed to lookup the proposal author info. 1711 pubKeys := make(map[string]struct{}) 1712 for _, pr := range dccs { 1713 if _, ok := pubKeys[pr.PublicKey]; !ok { 1714 pubKeys[pr.PublicKey] = struct{}{} 1715 } 1716 } 1717 1718 // Lookup proposal authors 1719 pk := make([]string, 0, len(pubKeys)) 1720 for k := range pubKeys { 1721 pk = append(pk, k) 1722 } 1723 users, err := p.db.UsersGetByPubKey(pk) 1724 if err != nil { 1725 return nil, err 1726 } 1727 if len(users) != len(pubKeys) { 1728 // A user is missing from the userdb for one 1729 // or more public keys. We're in trouble! 1730 notFound := make([]string, 0, len(pubKeys)) 1731 for v := range pubKeys { 1732 if _, ok := users[v]; !ok { 1733 notFound = append(notFound, v) 1734 } 1735 } 1736 e := fmt.Sprintf("users not found for pubkeys: %v", 1737 strings.Join(notFound, ", ")) 1738 panic(e) 1739 } 1740 1741 // Fill in proposal author info 1742 for i, pr := range dccs { 1743 dccs[i].SponsorUserID = users[pr.PublicKey].ID.String() 1744 dccs[i].SponsorUsername = users[pr.PublicKey].Username 1745 } 1746 1747 // Convert pointers to values 1748 tDCCs := make(map[string]cms.DCCRecord, len(dccs)) 1749 for token, dr := range dccs { 1750 tDCCs[token] = *dr 1751 } 1752 1753 return tDCCs, nil 1754 } 1755 1756 // processStartVoteV2 starts the voting period on a proposal using the provided 1757 // v2 StartVote. 1758 func (p *Politeiawww) processStartVoteDCC(ctx context.Context, sv cms.StartVote, u *user.User) (*cms.StartVoteReply, error) { 1759 log.Tracef("processStartVoteDCC %v", sv.Vote.Token) 1760 1761 // Sanity check 1762 if !u.Admin { 1763 return nil, fmt.Errorf("user is not an admin") 1764 } 1765 1766 // Validate vote bits 1767 for _, v := range sv.Vote.Options { 1768 err := validateVoteBitDCC(sv.Vote, v.Bits) 1769 if err != nil { 1770 log.Debugf("processStartVoteDCC: invalid vote bits: %v", err) 1771 return nil, www.UserError{ 1772 ErrorCode: www.ErrorStatusInvalidPropVoteBits, 1773 } 1774 } 1775 } 1776 1777 // Validate vote params 1778 switch { 1779 case sv.Vote.Duration < p.cfg.VoteDurationMin: 1780 e := fmt.Sprintf("vote duration must be > %v", p.cfg.VoteDurationMin) 1781 return nil, www.UserError{ 1782 ErrorCode: www.ErrorStatusInvalidPropVoteParams, 1783 ErrorContext: []string{e}, 1784 } 1785 case sv.Vote.Duration > p.cfg.VoteDurationMax: 1786 e := fmt.Sprintf("vote duration must be < %v", p.cfg.VoteDurationMax) 1787 return nil, www.UserError{ 1788 ErrorCode: www.ErrorStatusInvalidPropVoteParams, 1789 ErrorContext: []string{e}, 1790 } 1791 case sv.Vote.QuorumPercentage > 100: 1792 return nil, www.UserError{ 1793 ErrorCode: www.ErrorStatusInvalidPropVoteParams, 1794 ErrorContext: []string{"quorum percentage must be <= 100"}, 1795 } 1796 case sv.Vote.PassPercentage > 100: 1797 return nil, www.UserError{ 1798 ErrorCode: www.ErrorStatusInvalidPropVoteParams, 1799 ErrorContext: []string{"pass percentage must be <= 100"}, 1800 } 1801 } 1802 1803 // Ensure the public key is the user's active key 1804 if sv.PublicKey != u.PublicKey() { 1805 return nil, www.UserError{ 1806 ErrorCode: www.ErrorStatusInvalidSigningKey, 1807 } 1808 } 1809 1810 // Validate signature 1811 dsv := convertStartVoteToCMS(sv) 1812 1813 userWeights, err := p.getCMSUserWeights() 1814 if err != nil { 1815 return nil, err 1816 } 1817 1818 cmsUserWeights := make([]cmsplugin.UserWeight, 0, len(userWeights)) 1819 for id, weight := range userWeights { 1820 cmsuw := cmsplugin.UserWeight{ 1821 UserID: id, 1822 Weight: weight, 1823 } 1824 cmsUserWeights = append(cmsUserWeights, cmsuw) 1825 } 1826 dsv.UserWeights = cmsUserWeights 1827 1828 err = dsv.VerifySignature() 1829 if err != nil { 1830 log.Debugf("processStartVote: VerifySignature: %v", err) 1831 return nil, www.UserError{ 1832 ErrorCode: www.ErrorStatusInvalidSignature, 1833 } 1834 } 1835 1836 // Validate proposal version and status 1837 pr, err := p.getDCC(sv.Vote.Token) 1838 if err != nil { 1839 if errors.Is(err, cmsdatabase.ErrDCCNotFound) { 1840 err = www.UserError{ 1841 ErrorCode: www.ErrorStatusProposalNotFound, 1842 } 1843 return nil, err 1844 } 1845 } 1846 if pr.Status != cms.DCCStatusActive { 1847 return nil, www.UserError{ 1848 ErrorCode: www.ErrorStatusWrongStatus, 1849 ErrorContext: []string{"dcc is not active"}, 1850 } 1851 } 1852 1853 // Validate vote status 1854 vsr, err := p.cmsVoteSummary(ctx, sv.Vote.Token) 1855 if err != nil { 1856 return nil, err 1857 } 1858 1859 // Only allow voting on All Vote DCC proposals 1860 // Get vote summary to check vote status 1861 1862 bb, err := p.decredBestBlock(ctx) 1863 if err != nil { 1864 return nil, err 1865 } 1866 1867 voteStatus := dccVoteStatusFromVoteSummary(*vsr, bb) 1868 switch voteStatus { 1869 case cms.DCCVoteStatusStarted: 1870 return nil, www.UserError{ 1871 ErrorCode: www.ErrorStatusWrongVoteStatus, 1872 ErrorContext: []string{"vote already started"}, 1873 } 1874 case cms.DCCVoteStatusFinished: 1875 return nil, www.UserError{ 1876 ErrorCode: www.ErrorStatusWrongVoteStatus, 1877 ErrorContext: []string{"vote already finished"}, 1878 } 1879 case cms.DCCVoteStatusInvalid: 1880 return nil, www.UserError{ 1881 ErrorCode: www.ErrorStatusWrongVoteStatus, 1882 } 1883 } 1884 1885 // Tell decred plugin to start voting 1886 payload, err := cmsplugin.EncodeStartVote(dsv) 1887 if err != nil { 1888 return nil, err 1889 } 1890 challenge, err := util.Random(pd.ChallengeSize) 1891 if err != nil { 1892 return nil, err 1893 } 1894 pc := pd.PluginCommand{ 1895 Challenge: hex.EncodeToString(challenge), 1896 ID: cmsplugin.ID, 1897 Command: cmsplugin.CmdStartVote, 1898 CommandID: cmsplugin.CmdStartVote + " " + sv.Vote.Token, 1899 Payload: string(payload), 1900 } 1901 responseBody, err := p.makeRequest(ctx, http.MethodPost, 1902 pd.PluginCommandRoute, pc) 1903 if err != nil { 1904 return nil, err 1905 } 1906 1907 // Handle reply 1908 var reply pd.PluginCommandReply 1909 err = json.Unmarshal(responseBody, &reply) 1910 if err != nil { 1911 return nil, fmt.Errorf("could not unmarshal "+ 1912 "PluginCommandReply: %v", err) 1913 } 1914 err = util.VerifyChallenge(p.cfg.Identity, challenge, reply.Response) 1915 if err != nil { 1916 return nil, err 1917 } 1918 dsvr, err := cmsplugin.DecodeStartVoteReply([]byte(reply.Payload)) 1919 if err != nil { 1920 return nil, err 1921 } 1922 svr := convertCMSStartVoteReplyToCMS(dsvr) 1923 if err != nil { 1924 return nil, err 1925 } 1926 1927 /// XXX Do some sort of notification? 1928 1929 return &svr, nil 1930 } 1931 1932 // validateVoteBitDCC ensures that bit is a valid vote bit. 1933 func validateVoteBitDCC(vote cms.Vote, bit uint64) error { 1934 if len(vote.Options) == 0 { 1935 return fmt.Errorf("vote corrupt") 1936 } 1937 if bit == 0 { 1938 return fmt.Errorf("invalid bit 0x%x", bit) 1939 } 1940 if vote.Mask&bit != bit { 1941 return fmt.Errorf("invalid mask 0x%x bit 0x%x", 1942 vote.Mask, bit) 1943 } 1944 1945 for _, v := range vote.Options { 1946 if v.Bits == bit { 1947 return nil 1948 } 1949 } 1950 1951 return fmt.Errorf("bit not found 0x%x", bit) 1952 } 1953 1954 func dccVoteStatusFromVoteSummary(r cmsplugin.VoteSummaryReply, bestBlock uint32) cms.DCCVoteStatusT { 1955 switch { 1956 case r.EndHeight == 0: 1957 return cms.DCCVoteStatusNotStarted 1958 default: 1959 if bestBlock < r.EndHeight { 1960 return cms.DCCVoteStatusStarted 1961 } 1962 1963 return cms.DCCVoteStatusFinished 1964 } 1965 }