github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/ticketvote/cmds.go (about)

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