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  }