github.com/decred/politeia@v1.4.0/politeiawww/cmd/politeiavoter/politeiavoter.go (about)

     1  // Copyright (c) 2018-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 main
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	crand "crypto/rand"
    11  	"crypto/tls"
    12  	"crypto/x509"
    13  	"encoding/hex"
    14  	"encoding/json"
    15  	"errors"
    16  	"fmt"
    17  	"io"
    18  	"math/big"
    19  	"math/rand"
    20  	"net/http"
    21  	"net/http/cookiejar"
    22  	"net/url"
    23  	"os"
    24  	"path/filepath"
    25  	"strconv"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	pb "decred.org/dcrwallet/rpc/walletrpc"
    31  	"github.com/decred/dcrd/blockchain/stake/v3"
    32  	"github.com/decred/dcrd/chaincfg/chainhash"
    33  	"github.com/decred/dcrd/wire"
    34  	"github.com/decred/politeia/politeiad/api/v1/identity"
    35  	piv1 "github.com/decred/politeia/politeiawww/api/pi/v1"
    36  	rcv1 "github.com/decred/politeia/politeiawww/api/records/v1"
    37  	tkv1 "github.com/decred/politeia/politeiawww/api/ticketvote/v1"
    38  	v1 "github.com/decred/politeia/politeiawww/api/www/v1"
    39  	"github.com/decred/politeia/politeiawww/client"
    40  	"github.com/decred/politeia/util"
    41  	"github.com/gorilla/schema"
    42  	"golang.org/x/crypto/ssh/terminal"
    43  	"golang.org/x/net/publicsuffix"
    44  	"google.golang.org/grpc"
    45  	"google.golang.org/grpc/credentials"
    46  )
    47  
    48  const (
    49  	cmdInventory = "inventory"
    50  	cmdVote      = "vote"
    51  	cmdTally     = "tally"
    52  	cmdVerify    = "verify"
    53  	cmdHelp      = "help"
    54  )
    55  
    56  const (
    57  	failedJournal  = "failed.json"
    58  	successJournal = "success.json"
    59  	workJournal    = "work.json"
    60  )
    61  
    62  func generateSeed() (int64, error) {
    63  	var seedBytes [8]byte
    64  	_, err := crand.Read(seedBytes[:])
    65  	if err != nil {
    66  		return 0, err
    67  	}
    68  	return new(big.Int).SetBytes(seedBytes[:]).Int64(), nil
    69  }
    70  
    71  // walletPassphrase returns the wallet passphrase from the config if one was
    72  // provided or prompts the user for their wallet passphrase if one was not
    73  // provided.
    74  func (p *piv) walletPassphrase() ([]byte, error) {
    75  	if p.cfg.WalletPassphrase != "" {
    76  		return []byte(p.cfg.WalletPassphrase), nil
    77  	}
    78  
    79  	prompt := "Enter the private passphrase of your wallet: "
    80  	for {
    81  		fmt.Print(prompt)
    82  		pass, err := terminal.ReadPassword(int(os.Stdin.Fd()))
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  		fmt.Print("\n")
    87  		pass = bytes.TrimSpace(pass)
    88  		if len(pass) == 0 {
    89  			continue
    90  		}
    91  
    92  		return pass, nil
    93  	}
    94  }
    95  
    96  // piv is the client context.
    97  type piv struct {
    98  	sync.RWMutex                       // retryQ lock
    99  	ballotResults []tkv1.CastVoteReply // results of voting
   100  
   101  	run time.Time // when this run started
   102  
   103  	cfg *config // application config
   104  
   105  	// https
   106  	client    *http.Client
   107  	id        *identity.PublicIdentity
   108  	userAgent string
   109  
   110  	// wallet grpc
   111  	ctx    context.Context
   112  	cancel context.CancelFunc
   113  	creds  credentials.TransportCredentials
   114  	conn   *grpc.ClientConn
   115  	wallet pb.WalletServiceClient
   116  }
   117  
   118  func newPiVoter(shutdownCtx context.Context, cfg *config) (*piv, error) {
   119  	tlsConfig := &tls.Config{
   120  		InsecureSkipVerify: cfg.SkipVerify,
   121  	}
   122  	tr := &http.Transport{
   123  		TLSClientConfig: tlsConfig,
   124  		Dial:            cfg.dial,
   125  	}
   126  	if cfg.Proxy != "" {
   127  		tr.MaxConnsPerHost = 1
   128  		tr.DisableKeepAlives = true
   129  	}
   130  	jar, err := cookiejar.New(&cookiejar.Options{
   131  		PublicSuffixList: publicsuffix.List,
   132  	})
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  
   137  	// Wallet GRPC
   138  	serverCAs := x509.NewCertPool()
   139  	serverCert, err := os.ReadFile(cfg.WalletCert)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	if !serverCAs.AppendCertsFromPEM(serverCert) {
   144  		return nil, fmt.Errorf("no certificates found in %s",
   145  			cfg.WalletCert)
   146  	}
   147  	keypair, err := tls.LoadX509KeyPair(cfg.ClientCert, cfg.ClientKey)
   148  	if err != nil {
   149  		return nil, fmt.Errorf("read client keypair: %v", err)
   150  	}
   151  	creds := credentials.NewTLS(&tls.Config{
   152  		Certificates: []tls.Certificate{keypair},
   153  		RootCAs:      serverCAs,
   154  	})
   155  
   156  	conn, err := grpc.Dial(cfg.WalletHost,
   157  		grpc.WithTransportCredentials(creds))
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	wallet := pb.NewWalletServiceClient(conn)
   162  
   163  	// return context
   164  	return &piv{
   165  		run:    time.Now(),
   166  		ctx:    shutdownCtx,
   167  		creds:  creds,
   168  		conn:   conn,
   169  		wallet: wallet,
   170  		cfg:    cfg,
   171  		client: &http.Client{
   172  			Transport: tr,
   173  			Jar:       jar,
   174  		},
   175  		userAgent: fmt.Sprintf("politeiavoter/%s", cfg.Version),
   176  	}, nil
   177  }
   178  
   179  type JSONTime struct {
   180  	Time string `json:"time"`
   181  }
   182  
   183  func (p *piv) jsonLog(filename, token string, work ...interface{}) error {
   184  	dir := filepath.Join(p.cfg.voteDir, token)
   185  	os.MkdirAll(dir, 0700)
   186  
   187  	p.Lock()
   188  	defer p.Unlock()
   189  
   190  	f := filepath.Join(dir, fmt.Sprintf("%v.%v", filename, p.run.Unix()))
   191  	fh, err := os.OpenFile(f, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
   192  	if err != nil {
   193  		return err
   194  	}
   195  	defer fh.Close()
   196  
   197  	e := json.NewEncoder(fh)
   198  	e.SetIndent("", "  ")
   199  	err = e.Encode(JSONTime{
   200  		Time: time.Now().Format(time.StampNano),
   201  	})
   202  	if err != nil {
   203  		return err
   204  	}
   205  	for _, v := range work {
   206  		err = e.Encode(v)
   207  		if err != nil {
   208  			return err
   209  		}
   210  	}
   211  
   212  	return nil
   213  }
   214  
   215  func convertTicketHashes(h []string) ([][]byte, error) {
   216  	hashes := make([][]byte, 0, len(h))
   217  	for _, v := range h {
   218  		hh, err := chainhash.NewHashFromStr(v)
   219  		if err != nil {
   220  			return nil, err
   221  		}
   222  		hashes = append(hashes, hh[:])
   223  	}
   224  	return hashes, nil
   225  }
   226  
   227  func (p *piv) testMaybeFail(b interface{}) ([]byte, error) {
   228  	switch p.cfg.testingMode {
   229  	case testFailUnrecoverable:
   230  		return nil, fmt.Errorf("%v, %v %v", http.StatusBadRequest,
   231  			255, "fake")
   232  	default:
   233  	}
   234  	// Fail every 3rd vote
   235  	p.Lock()
   236  	p.cfg.testingCounter++
   237  	if p.cfg.testingCounter%3 == 0 {
   238  		p.Unlock()
   239  		return nil, ErrRetry{
   240  			At:   "FAKE r.StatusCode != http.StatusOK",
   241  			Err:  fmt.Errorf("fake error"),
   242  			Body: []byte{},
   243  			Code: http.StatusRequestTimeout,
   244  		}
   245  	}
   246  	p.Unlock()
   247  
   248  	// Fake out CastBallotReply. We cast b to CastBallot but this
   249  	// may have to change in the future if we add additional
   250  	// functionality here.
   251  	cbr := tkv1.CastBallotReply{
   252  		Receipts: []tkv1.CastVoteReply{
   253  			{
   254  				Ticket:  b.(*tkv1.CastBallot).Votes[0].Ticket,
   255  				Receipt: "receipt",
   256  				//ErrorCode:    tkv1.VoteErrorInternalError,
   257  				//ErrorContext: "testing",
   258  			},
   259  		},
   260  	}
   261  	jcbr, err := json.Marshal(cbr)
   262  	if err != nil {
   263  		return nil, fmt.Errorf("TEST FAILED: %v", err)
   264  	}
   265  	return jcbr, nil
   266  }
   267  
   268  func (p *piv) makeRequest(method, api, route string, b interface{}) ([]byte, error) {
   269  	var requestBody []byte
   270  	var queryParams string
   271  	if b != nil {
   272  		if method == http.MethodGet {
   273  			// GET requests don't have a request body; instead we will populate
   274  			// the query params.
   275  			form := url.Values{}
   276  			err := schema.NewEncoder().Encode(b, form)
   277  			if err != nil {
   278  				return nil, err
   279  			}
   280  
   281  			queryParams = "?" + form.Encode()
   282  		} else {
   283  			var err error
   284  			requestBody, err = json.Marshal(b)
   285  			if err != nil {
   286  				return nil, err
   287  			}
   288  		}
   289  	}
   290  
   291  	fullRoute := p.cfg.PoliteiaWWW + api + route + queryParams
   292  	log.Debugf("Request: %v %v", method, fullRoute)
   293  	if len(requestBody) != 0 {
   294  		log.Tracef("%v  ", string(requestBody))
   295  	}
   296  
   297  	// This is a hack to test this code.
   298  	if p.cfg.testing {
   299  		return p.testMaybeFail(b)
   300  	}
   301  	req, err := http.NewRequestWithContext(p.ctx, method, fullRoute,
   302  		bytes.NewReader(requestBody))
   303  	if err != nil {
   304  		return nil, err
   305  	}
   306  
   307  	req.Header.Set("User-Agent", p.userAgent)
   308  	r, err := p.client.Do(req)
   309  	if err != nil {
   310  		return nil, ErrRetry{
   311  			At:  "p.client.Do(req)",
   312  			Err: err,
   313  		}
   314  	}
   315  	defer func() {
   316  		r.Body.Close()
   317  	}()
   318  
   319  	responseBody := util.ConvertBodyToByteArray(r.Body, false)
   320  	log.Tracef("Response: %v %v", r.StatusCode, string(responseBody))
   321  
   322  	switch r.StatusCode {
   323  	case http.StatusOK:
   324  		// Nothing to do. Continue.
   325  	case http.StatusBadRequest:
   326  		// The error was caused by the client. These will result in
   327  		// the same error every time so should not be retried.
   328  		var ue tkv1.UserErrorReply
   329  		err = json.Unmarshal(responseBody, &ue)
   330  		if err == nil && ue.ErrorCode != 0 {
   331  			return nil, fmt.Errorf("%v, %v %v", r.StatusCode,
   332  				tkv1.ErrorCodes[ue.ErrorCode], ue.ErrorContext)
   333  		}
   334  	default:
   335  		// Retry all other errors
   336  		return nil, ErrRetry{
   337  			At:   "r.StatusCode != http.StatusOK",
   338  			Err:  err,
   339  			Body: responseBody,
   340  			Code: r.StatusCode,
   341  		}
   342  	}
   343  
   344  	return responseBody, nil
   345  }
   346  
   347  // getVersion retursn the server side version structure.
   348  func (p *piv) getVersion() (*v1.VersionReply, error) {
   349  	responseBody, err := p.makeRequest(http.MethodGet,
   350  		v1.PoliteiaWWWAPIRoute, v1.RouteVersion, nil)
   351  	if err != nil {
   352  		return nil, err
   353  	}
   354  
   355  	var v v1.VersionReply
   356  	err = json.Unmarshal(responseBody, &v)
   357  	if err != nil {
   358  		return nil, fmt.Errorf("Could not unmarshal version: %v", err)
   359  	}
   360  
   361  	return &v, nil
   362  }
   363  
   364  // firstContact connect to the wallet and it obtains the version structure from
   365  // the politeia server.
   366  func firstContact(shutdownCtx context.Context, cfg *config) (*piv, error) {
   367  	// Always hit / first for to obtain the server identity and api version
   368  	p, err := newPiVoter(shutdownCtx, cfg)
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  	version, err := p.getVersion()
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	log.Debugf("Version: %v", version.Version)
   377  	log.Debugf("Route  : %v", version.Route)
   378  	log.Debugf("Pubkey : %v", version.PubKey)
   379  
   380  	p.id, err = identity.PublicIdentityFromString(version.PubKey)
   381  	if err != nil {
   382  		return nil, err
   383  	}
   384  
   385  	return p, nil
   386  }
   387  
   388  // eligibleVotes takes a vote result reply that contains the full list of the
   389  // votes already cast along with a committed tickets response from wallet which
   390  // consists of a list of tickets the wallet is aware of and returns a list of
   391  // tickets that the wallet is actually able to sign and vote with.
   392  //
   393  // When a ticket has already voted, the signature is also checked to ensure it
   394  // is valid.  In the case it is invalid, and the wallet can sign it, the ticket
   395  // is included so it may be resubmitted.  This could be caused by bad data on
   396  // the server or if the server is lying to the client.
   397  func (p *piv) eligibleVotes(rr *tkv1.ResultsReply, ctres *pb.CommittedTicketsResponse) ([]*pb.CommittedTicketsResponse_TicketAddress, error) {
   398  	// Put cast votes into a map to filter in linear time
   399  	castVotes := make(map[string]tkv1.CastVoteDetails)
   400  	for _, v := range rr.Votes {
   401  		castVotes[v.Ticket] = v
   402  	}
   403  
   404  	// Filter out tickets that have already voted. If a ticket has
   405  	// voted but the signature is invalid, resubmit the vote. This
   406  	// could be caused by bad data on the server or if the server is
   407  	// lying to the client.
   408  	eligible := make([]*pb.CommittedTicketsResponse_TicketAddress, 0,
   409  		len(ctres.TicketAddresses))
   410  	for _, t := range ctres.TicketAddresses {
   411  		h, err := chainhash.NewHash(t.Ticket)
   412  		if err != nil {
   413  			return nil, err
   414  		}
   415  
   416  		// Filter out tickets tracked by imported xpub accounts.
   417  		r, err := p.wallet.GetTransaction(context.TODO(), &pb.GetTransactionRequest{
   418  			TransactionHash: h[:],
   419  		})
   420  		if err != nil {
   421  			log.Error(err)
   422  			continue
   423  		}
   424  		tx := new(wire.MsgTx)
   425  		err = tx.Deserialize(bytes.NewReader(r.Transaction.Transaction))
   426  		if err != nil {
   427  			log.Error(err)
   428  			continue
   429  		}
   430  		addr, err := stake.AddrFromSStxPkScrCommitment(tx.TxOut[1].PkScript, activeNetParams.Params)
   431  		if err != nil {
   432  			log.Error(err)
   433  			continue
   434  		}
   435  		vr, err := p.wallet.ValidateAddress(context.TODO(), &pb.ValidateAddressRequest{
   436  			Address: addr.String(),
   437  		})
   438  		if err != nil {
   439  			log.Error(err)
   440  			continue
   441  		}
   442  		if vr.AccountNumber >= 1<<31-1 { // imported xpub account
   443  			// do not append to filtered.
   444  			continue
   445  		}
   446  
   447  		_, ok := castVotes[h.String()]
   448  		if !ok {
   449  			eligible = append(eligible, t)
   450  		}
   451  	}
   452  
   453  	return eligible, nil
   454  }
   455  
   456  func (p *piv) _inventory(i tkv1.Inventory) (*tkv1.InventoryReply, error) {
   457  	responseBody, err := p.makeRequest(http.MethodPost,
   458  		tkv1.APIRoute, tkv1.RouteInventory, i)
   459  	if err != nil {
   460  		return nil, err
   461  	}
   462  
   463  	var ar tkv1.InventoryReply
   464  	err = json.Unmarshal(responseBody, &ar)
   465  	if err != nil {
   466  		return nil, fmt.Errorf("Could not unmarshal InventoryReply: %v",
   467  			err)
   468  	}
   469  
   470  	return &ar, nil
   471  }
   472  
   473  // voteDetails sends a ticketvote API Details request, then verifies and
   474  // returns the reply.
   475  func (p *piv) voteDetails(token, serverPubKey string) (*tkv1.DetailsReply, error) {
   476  	d := tkv1.Details{
   477  		Token: token,
   478  	}
   479  	responseBody, err := p.makeRequest(http.MethodPost,
   480  		tkv1.APIRoute, tkv1.RouteDetails, d)
   481  	if err != nil {
   482  		return nil, err
   483  	}
   484  
   485  	var dr tkv1.DetailsReply
   486  	err = json.Unmarshal(responseBody, &dr)
   487  	if err != nil {
   488  		return nil, fmt.Errorf("Could not unmarshal DetailsReply: %v",
   489  			err)
   490  	}
   491  
   492  	// Verify VoteDetails.
   493  	err = client.VoteDetailsVerify(*dr.Vote, serverPubKey)
   494  	if err != nil {
   495  		return nil, err
   496  	}
   497  
   498  	return &dr, nil
   499  }
   500  
   501  func (p *piv) voteResults(token, serverPubKey string) (*tkv1.ResultsReply, error) {
   502  	r := tkv1.Results{
   503  		Token: token,
   504  	}
   505  	responseBody, err := p.makeRequest(http.MethodPost,
   506  		tkv1.APIRoute, tkv1.RouteResults, r)
   507  	if err != nil {
   508  		return nil, err
   509  	}
   510  
   511  	var rr tkv1.ResultsReply
   512  	err = json.Unmarshal(responseBody, &rr)
   513  	if err != nil {
   514  		return nil, fmt.Errorf("Could not unmarshal ResultsReply: %v", err)
   515  	}
   516  
   517  	// Verify CastVoteDetails.
   518  	for _, cvd := range rr.Votes {
   519  		err = client.CastVoteDetailsVerify(cvd, serverPubKey)
   520  		if err != nil {
   521  			return nil, err
   522  		}
   523  	}
   524  
   525  	return &rr, nil
   526  }
   527  
   528  // records sends a records API Records request and returns the reply.
   529  func (p *piv) records(tokens []string, serverPubKey string) (*rcv1.RecordsReply, error) {
   530  	// Prepare request
   531  	reqs := make([]rcv1.RecordRequest, 0, len(tokens))
   532  	for _, t := range tokens {
   533  		reqs = append(reqs, rcv1.RecordRequest{
   534  			Token: t,
   535  			Filenames: []string{
   536  				piv1.FileNameProposalMetadata,
   537  			},
   538  		})
   539  	}
   540  
   541  	// Send request
   542  	responseBody, err := p.makeRequest(http.MethodPost, rcv1.APIRoute,
   543  		rcv1.RouteRecords, rcv1.Records{
   544  			Requests: reqs,
   545  		})
   546  	if err != nil {
   547  		return nil, err
   548  	}
   549  
   550  	var rsr rcv1.RecordsReply
   551  	err = json.Unmarshal(responseBody, &rsr)
   552  	if err != nil {
   553  		return nil, fmt.Errorf("Could not unmarshal RecordsReply: %v",
   554  			err)
   555  	}
   556  
   557  	return &rsr, nil
   558  }
   559  
   560  // votePolicy sends a ticketvote API Policy request and returns the reply.
   561  func (p *piv) votePolicy() (*tkv1.PolicyReply, error) {
   562  	// Send request
   563  	responseBody, err := p.makeRequest(http.MethodPost, tkv1.APIRoute,
   564  		tkv1.RoutePolicy, tkv1.Policy{})
   565  	if err != nil {
   566  		return nil, err
   567  	}
   568  
   569  	var pr tkv1.PolicyReply
   570  	err = json.Unmarshal(responseBody, &pr)
   571  	if err != nil {
   572  		return nil, fmt.Errorf("Could not unmarshal RecordsReply: %v",
   573  			err)
   574  	}
   575  
   576  	return &pr, nil
   577  }
   578  
   579  func (p *piv) inventory() error {
   580  	// Get server public key to verify replies.
   581  	version, err := p.getVersion()
   582  	if err != nil {
   583  		return err
   584  	}
   585  	serverPubKey := version.PubKey
   586  
   587  	// Inventory route is paginated, therefore we keep fetching
   588  	// until we receive a patch with number of records smaller than the
   589  	// ticketvote's declared page size. The page size is retrieved from
   590  	// the ticketvote API Policy route.
   591  	vp, err := p.votePolicy()
   592  	if err != nil {
   593  		return err
   594  	}
   595  	pageSize := vp.InventoryPageSize
   596  	page := uint32(1)
   597  	var tokens []string
   598  	for {
   599  		ir, err := p._inventory(tkv1.Inventory{
   600  			Page:   page,
   601  			Status: tkv1.VoteStatusStarted,
   602  		})
   603  		if err != nil {
   604  			return err
   605  		}
   606  		pageTokens := ir.Vetted[tkv1.VoteStatuses[tkv1.VoteStatusStarted]]
   607  		tokens = append(tokens, pageTokens...)
   608  		if uint32(len(pageTokens)) < pageSize {
   609  			break
   610  		}
   611  		page++
   612  	}
   613  
   614  	// Print empty message in case no active votes found.
   615  	if len(tokens) == 0 {
   616  		fmt.Printf("No active votes found.\n")
   617  		return nil
   618  	}
   619  
   620  	// Retrieve the proposals metadata and store proposal names in a
   621  	// map[token] => name.
   622  	names := make(map[string]string, len(tokens))
   623  	remainingTokens := tokens
   624  	// As the records API Records route is paged, we need to fetch the proposals
   625  	// metadata page by page.
   626  	for len(remainingTokens) != 0 {
   627  		var page []string
   628  		if len(remainingTokens) > rcv1.RecordsPageSize {
   629  			// If the number of remaining tokens to fetch exceeds the page size, we
   630  			// get the next page and keep the rest for the next iteration.
   631  			page = remainingTokens[:rcv1.RecordsPageSize]
   632  			remainingTokens = remainingTokens[rcv1.RecordsPageSize:]
   633  		} else {
   634  			// If the number of remaining tokens to fetch is equal or smaller than
   635  			// the page size then that's the last page.
   636  			page = remainingTokens
   637  			remainingTokens = []string{}
   638  		}
   639  
   640  		// Fetch page of records
   641  		reply, err := p.records(page, serverPubKey)
   642  		if err != nil {
   643  			return err
   644  		}
   645  
   646  		// Get proposal metadata and store proposal name in map.
   647  		for token, record := range reply.Records {
   648  			md, err := client.ProposalMetadataDecode(record.Files)
   649  			if err != nil {
   650  				return nil
   651  			}
   652  			names[token] = md.Name
   653  		}
   654  	}
   655  
   656  	for _, t := range tokens {
   657  		// Get vote details.
   658  		dr, err := p.voteDetails(t, serverPubKey)
   659  		if err != nil {
   660  			return err
   661  		}
   662  
   663  		// Ensure eligibility
   664  		tix, err := convertTicketHashes(dr.Vote.EligibleTickets)
   665  		if err != nil {
   666  			fmt.Printf("Ticket pool corrupt: %v %v\n",
   667  				dr.Vote.Params.Token, err)
   668  			continue
   669  		}
   670  		ctres, err := p.wallet.CommittedTickets(p.ctx,
   671  			&pb.CommittedTicketsRequest{
   672  				Tickets: tix,
   673  			})
   674  		if err != nil {
   675  			fmt.Printf("Ticket pool verification: %v %v\n",
   676  				dr.Vote.Params.Token, err)
   677  			continue
   678  		}
   679  
   680  		// Bail if there are no eligible tickets
   681  		if len(ctres.TicketAddresses) == 0 {
   682  			fmt.Printf("No eligible tickets: %v\n", dr.Vote.Params.Token)
   683  		}
   684  
   685  		// voteResults provides a list of the votes that have already been cast.
   686  		// Use these to filter out the tickets that have already voted.
   687  		rr, err := p.voteResults(dr.Vote.Params.Token, serverPubKey)
   688  		if err != nil {
   689  			fmt.Printf("Failed to obtain vote results for %v: %v\n",
   690  				dr.Vote.Params.Token, err)
   691  			continue
   692  		}
   693  
   694  		// Filter out tickets that have already voted or are otherwise
   695  		// ineligible for the wallet to sign.  Note that tickets that have
   696  		// already voted, but have an invalid signature are included so they
   697  		// may be resubmitted.
   698  		eligible, err := p.eligibleVotes(rr, ctres)
   699  		if err != nil {
   700  			fmt.Printf("Eligible vote filtering error: %v %v\n",
   701  				dr.Vote.Params, err)
   702  			continue
   703  		}
   704  
   705  		// Display vote bits
   706  		fmt.Printf("Vote: %v\n", dr.Vote.Params.Token)
   707  		fmt.Printf("  Proposal        : %v\n", names[t])
   708  		fmt.Printf("  Start block     : %v\n", dr.Vote.StartBlockHeight)
   709  		fmt.Printf("  End block       : %v\n", dr.Vote.EndBlockHeight)
   710  		fmt.Printf("  Mask            : %v\n", dr.Vote.Params.Mask)
   711  		fmt.Printf("  Eligible tickets: %v\n", len(ctres.TicketAddresses))
   712  		fmt.Printf("  Eligible votes  : %v\n", len(eligible))
   713  		for _, vo := range dr.Vote.Params.Options {
   714  			fmt.Printf("  Vote Option:\n")
   715  			fmt.Printf("    Id                   : %v\n", vo.ID)
   716  			fmt.Printf("    Description          : %v\n",
   717  				vo.Description)
   718  			fmt.Printf("    Bit                  : %v\n", vo.Bit)
   719  			fmt.Printf("    To choose this option: "+
   720  				"politeiavoter vote %v %v\n", dr.Vote.Params.Token,
   721  				vo.ID)
   722  		}
   723  	}
   724  
   725  	return nil
   726  }
   727  
   728  type ErrRetry struct {
   729  	At   string      `json:"at"`   // where in the code
   730  	Body []byte      `json:"body"` // http body if we have one
   731  	Code int         `json:"code"` // http code
   732  	Err  interface{} `json:"err"`  // underlying error
   733  }
   734  
   735  func (e ErrRetry) Error() string {
   736  	return fmt.Sprintf("retry error: %v (%v) %v", e.Code, e.At, e.Err)
   737  }
   738  
   739  // sendVoteFail isa test function that will fail a Ballot call with a retryable
   740  // error.
   741  func (p *piv) sendVoteFail(ballot *tkv1.CastBallot) (*tkv1.CastVoteReply, error) {
   742  	return nil, ErrRetry{
   743  		At: "sendVoteFail",
   744  	}
   745  }
   746  
   747  func (p *piv) sendVote(ballot *tkv1.CastBallot) (*tkv1.CastVoteReply, error) {
   748  	if len(ballot.Votes) != 1 {
   749  		return nil, fmt.Errorf("sendVote: only one vote allowed")
   750  	}
   751  
   752  	responseBody, err := p.makeRequest(http.MethodPost,
   753  		tkv1.APIRoute, tkv1.RouteCastBallot, ballot)
   754  	if err != nil {
   755  		return nil, err
   756  	}
   757  
   758  	var vr tkv1.CastBallotReply
   759  	err = json.Unmarshal(responseBody, &vr)
   760  	if err != nil {
   761  		return nil, fmt.Errorf("Could not unmarshal "+
   762  			"CastVoteReply: %v", err)
   763  	}
   764  	if len(vr.Receipts) != 1 {
   765  		// Should be impossible
   766  		return nil, fmt.Errorf("sendVote: invalid receipt count %v",
   767  			len(vr.Receipts))
   768  	}
   769  
   770  	return &vr.Receipts[0], nil
   771  }
   772  
   773  // dumpComplete dumps the completed votes in this run.
   774  func (p *piv) dumpComplete() {
   775  	p.RLock()
   776  	defer p.RUnlock()
   777  
   778  	fmt.Printf("Completed votes (%v):\n", len(p.ballotResults))
   779  	for _, v := range p.ballotResults {
   780  		fmt.Printf("  %v %v\n", v.Ticket, v.ErrorCode)
   781  	}
   782  }
   783  
   784  func (p *piv) dumpQueue() {
   785  	p.RLock()
   786  	defer p.RUnlock()
   787  
   788  	panic("dumpQueue")
   789  }
   790  
   791  // dumpTogo dumps the votes that have not been casrt yet.
   792  func (p *piv) dumpTogo() {
   793  	p.RLock()
   794  	defer p.RUnlock()
   795  
   796  	panic("dumpTogo")
   797  }
   798  
   799  func (p *piv) _vote(token, voteID string) error {
   800  	passphrase, err := p.walletPassphrase()
   801  	if err != nil {
   802  		return err
   803  	}
   804  	// This assumes the account is an HD account.
   805  	_, err = p.wallet.GetAccountExtendedPrivKey(p.ctx,
   806  		&pb.GetAccountExtendedPrivKeyRequest{
   807  			AccountNumber: 0, // TODO: make a config flag
   808  			Passphrase:    passphrase,
   809  		})
   810  	if err != nil {
   811  		return err
   812  	}
   813  
   814  	seed, err := generateSeed()
   815  	if err != nil {
   816  		return err
   817  	}
   818  
   819  	// Verify vote is still active
   820  	sr, err := p._summary(token)
   821  	if err != nil {
   822  		return err
   823  	}
   824  	vs, ok := sr.Summaries[token]
   825  	if !ok {
   826  		return fmt.Errorf("proposal does not exist: %v", token)
   827  	}
   828  	if vs.Status != tkv1.VoteStatusStarted {
   829  		return fmt.Errorf("proposal vote is not active: %v", vs.Status)
   830  	}
   831  	bestBlock := vs.BestBlock
   832  
   833  	// Get server public key by calling version request.
   834  	v, err := p.getVersion()
   835  	if err != nil {
   836  		return err
   837  	}
   838  
   839  	// Get vote details.
   840  	dr, err := p.voteDetails(token, v.PubKey)
   841  	if err != nil {
   842  		return err
   843  	}
   844  
   845  	// Validate voteId
   846  	var (
   847  		voteBit string
   848  		found   bool
   849  	)
   850  	for _, vv := range dr.Vote.Params.Options {
   851  		if vv.ID == voteID {
   852  			found = true
   853  			voteBit = strconv.FormatUint(vv.Bit, 16)
   854  			break
   855  		}
   856  	}
   857  	if !found {
   858  		return fmt.Errorf("vote id not found: %v", voteID)
   859  	}
   860  
   861  	// Find eligble tickets
   862  	tix, err := convertTicketHashes(dr.Vote.EligibleTickets)
   863  	if err != nil {
   864  		return fmt.Errorf("ticket pool corrupt: %v %v",
   865  			token, err)
   866  	}
   867  	ctres, err := p.wallet.CommittedTickets(p.ctx,
   868  		&pb.CommittedTicketsRequest{
   869  			Tickets: tix,
   870  		})
   871  	if err != nil {
   872  		return fmt.Errorf("ticket pool verification: %v %v",
   873  			token, err)
   874  	}
   875  	if len(ctres.TicketAddresses) == 0 {
   876  		return fmt.Errorf("no eligible tickets found")
   877  	}
   878  
   879  	// voteResults a list of the votes that have already been cast. We use these
   880  	// to filter out the tickets that have already voted.
   881  	rr, err := p.voteResults(token, v.PubKey)
   882  	if err != nil {
   883  		return err
   884  	}
   885  
   886  	// Filter out tickets that have already voted or are otherwise ineligible
   887  	// for the wallet to sign.  Note that tickets that have already voted, but
   888  	// have an invalid signature are included so they may be resubmitted.
   889  	eligible, err := p.eligibleVotes(rr, ctres)
   890  	if err != nil {
   891  		return err
   892  	}
   893  
   894  	eligibleLen := len(eligible)
   895  	if eligibleLen == 0 {
   896  		return fmt.Errorf("no eligible tickets found")
   897  	}
   898  	r := rand.New(rand.NewSource(seed))
   899  	// Fisher-Yates shuffle the ticket addresses.
   900  	for i := 0; i < eligibleLen; i++ {
   901  		// Pick a number between current index and the end.
   902  		j := r.Intn(eligibleLen-i) + i
   903  		eligible[i], eligible[j] = eligible[j], eligible[i]
   904  	}
   905  	ctres.TicketAddresses = eligible
   906  
   907  	// Sign all tickets
   908  	sm := &pb.SignMessagesRequest{
   909  		Passphrase: passphrase,
   910  		Messages: make([]*pb.SignMessagesRequest_Message, 0,
   911  			len(ctres.TicketAddresses)),
   912  	}
   913  	for _, v := range ctres.TicketAddresses {
   914  		h, err := chainhash.NewHash(v.Ticket)
   915  		if err != nil {
   916  			return err
   917  		}
   918  		msg := token + h.String() + voteBit
   919  		sm.Messages = append(sm.Messages, &pb.SignMessagesRequest_Message{
   920  			Address: v.Address,
   921  			Message: msg,
   922  		})
   923  	}
   924  	smr, err := p.wallet.SignMessages(p.ctx, sm)
   925  	if err != nil {
   926  		return err
   927  	}
   928  
   929  	// Make sure all signatures worked
   930  	for k, v := range smr.Replies {
   931  		if v.Error == "" {
   932  			continue
   933  		}
   934  		return fmt.Errorf("signature failed index %v: %v", k, v.Error)
   935  	}
   936  
   937  	// Trickle in the votes if specified
   938  	if p.cfg.Trickle {
   939  		// Setup the trickler vote duration
   940  		var (
   941  			blocksLeft     = int64(vs.EndBlockHeight) - int64(bestBlock)
   942  			blockTime      = activeNetParams.TargetTimePerBlock
   943  			timeLeftInVote = time.Duration(blocksLeft) * blockTime
   944  		)
   945  		err = p.setupVoteDuration(timeLeftInVote)
   946  		if err != nil {
   947  			return err
   948  		}
   949  
   950  		// Trickle votes
   951  		return p.alarmTrickler(token, voteBit, ctres, smr)
   952  	}
   953  
   954  	// Vote everything at once.
   955  
   956  	// Note that ctres, sm and smr use the same index.
   957  	cv := tkv1.CastBallot{
   958  		Votes: make([]tkv1.CastVote, 0, len(ctres.TicketAddresses)),
   959  	}
   960  	p.ballotResults = make([]tkv1.CastVoteReply, 0, len(ctres.TicketAddresses))
   961  	for k, v := range ctres.TicketAddresses {
   962  		h, err := chainhash.NewHash(v.Ticket)
   963  		if err != nil {
   964  			return err
   965  		}
   966  		signature := hex.EncodeToString(smr.Replies[k].Signature)
   967  		cv.Votes = append(cv.Votes, tkv1.CastVote{
   968  			Token:     token,
   969  			Ticket:    h.String(),
   970  			VoteBit:   voteBit,
   971  			Signature: signature,
   972  		})
   973  	}
   974  
   975  	// Vote on the supplied proposal
   976  	responseBody, err := p.makeRequest(http.MethodPost,
   977  		tkv1.APIRoute, tkv1.RouteCastBallot, &cv)
   978  	if err != nil {
   979  		return err
   980  	}
   981  
   982  	var br tkv1.CastBallotReply
   983  	err = json.Unmarshal(responseBody, &br)
   984  	if err != nil {
   985  		return fmt.Errorf("Could not unmarshal CastVoteReply: %v",
   986  			err)
   987  	}
   988  	p.ballotResults = br.Receipts
   989  
   990  	return nil
   991  }
   992  
   993  // setupVoteDuration sets up the duration that will be used for trickling
   994  // votes. The user can either set a duration manually using the --voteduration
   995  // setting or this function will calculate a duration. The calculated duration
   996  // is the remaining time left in the vote minus the --hoursprior setting.
   997  func (p *piv) setupVoteDuration(timeLeftInVote time.Duration) error {
   998  	switch {
   999  	case p.cfg.voteDuration.Seconds() > 0:
  1000  		// A vote duration was provided
  1001  		if p.cfg.voteDuration > timeLeftInVote {
  1002  			return fmt.Errorf("the provided --voteduration of %v is "+
  1003  				"greater than the remaining time in the vote of %v",
  1004  				p.cfg.voteDuration, timeLeftInVote)
  1005  		}
  1006  
  1007  	case p.cfg.voteDuration.Seconds() == 0:
  1008  		// A vote duration was not provided. The vote duration is set to
  1009  		// the remaining time in the vote minus the hours prior setting.
  1010  		p.cfg.voteDuration = timeLeftInVote - p.cfg.hoursPrior
  1011  
  1012  		// Force the user to manually set the vote duration when the
  1013  		// calculated duration is under 24h.
  1014  		if p.cfg.voteDuration < (24 * time.Hour) {
  1015  			return fmt.Errorf("there is only %v left in the vote; when "+
  1016  				"the remaining time is this low you must use --voteduration "+
  1017  				"to manually set the duration that will be used to trickle "+
  1018  				"in your votes, example --voteduration=6h", timeLeftInVote)
  1019  		}
  1020  
  1021  	default:
  1022  		// Should not be possible
  1023  		return fmt.Errorf("invalid vote duration %v", p.cfg.voteDuration)
  1024  	}
  1025  
  1026  	return nil
  1027  }
  1028  
  1029  func (p *piv) vote(args []string) error {
  1030  	if len(args) != 2 {
  1031  		return fmt.Errorf("vote: not enough arguments %v", args)
  1032  	}
  1033  
  1034  	err := p._vote(args[0], args[1])
  1035  	// we return err after printing details
  1036  
  1037  	// Verify vote replies. Already voted errors are not
  1038  	// considered to be failures because they occur when
  1039  	// a network error or dropped client connection causes
  1040  	// politeiavoter to incorrectly think that the first
  1041  	// attempt to cast the vote failed. politeiavoter will
  1042  	// attempt to retry the vote that it has already
  1043  	// successfully cast, resulting in the already voted
  1044  	// error.
  1045  	var alreadyVoted int
  1046  	failedReceipts := make([]tkv1.CastVoteReply, 0,
  1047  		len(p.ballotResults))
  1048  	for _, v := range p.ballotResults {
  1049  		if v.ErrorCode == nil {
  1050  			continue
  1051  		}
  1052  		if *v.ErrorCode == tkv1.VoteErrorTicketAlreadyVoted {
  1053  			alreadyVoted++
  1054  			continue
  1055  		}
  1056  		failedReceipts = append(failedReceipts, v)
  1057  	}
  1058  
  1059  	log.Debugf("%v already voted errors found; these are "+
  1060  		"counted as being successful", alreadyVoted)
  1061  
  1062  	fmt.Printf("Votes succeeded: %v\n", len(p.ballotResults)-
  1063  		len(failedReceipts))
  1064  	fmt.Printf("Votes failed   : %v\n", len(failedReceipts))
  1065  	notCast := cap(p.ballotResults) - len(p.ballotResults)
  1066  	if notCast > 0 {
  1067  		fmt.Printf("Votes not cast : %v\n", notCast)
  1068  	}
  1069  	for _, v := range failedReceipts {
  1070  		fmt.Printf("Failed vote    : %v %v\n",
  1071  			v.Ticket, v.ErrorContext)
  1072  	}
  1073  
  1074  	return err
  1075  }
  1076  
  1077  func (p *piv) _summary(token string) (*tkv1.SummariesReply, error) {
  1078  	responseBody, err := p.makeRequest(http.MethodPost,
  1079  		tkv1.APIRoute, tkv1.RouteSummaries,
  1080  		tkv1.Summaries{Tokens: []string{token}})
  1081  	if err != nil {
  1082  		return nil, err
  1083  	}
  1084  
  1085  	var sr tkv1.SummariesReply
  1086  	err = json.Unmarshal(responseBody, &sr)
  1087  	if err != nil {
  1088  		return nil, fmt.Errorf("Could not unmarshal SummariesReply: %v", err)
  1089  	}
  1090  
  1091  	return &sr, nil
  1092  }
  1093  
  1094  func (p *piv) tally(args []string) error {
  1095  	if len(args) != 1 {
  1096  		return fmt.Errorf("tally: not enough arguments %v", args)
  1097  	}
  1098  
  1099  	// Get server public key by calling version.
  1100  	v, err := p.getVersion()
  1101  	if err != nil {
  1102  		return err
  1103  	}
  1104  
  1105  	token := args[0]
  1106  	t, err := p.voteResults(token, v.PubKey)
  1107  	if err != nil {
  1108  		return err
  1109  	}
  1110  
  1111  	// tally votes
  1112  	count := make(map[uint64]uint)
  1113  	var total uint
  1114  	for _, v := range t.Votes {
  1115  		bits, err := strconv.ParseUint(v.VoteBit, 10, 64)
  1116  		if err != nil {
  1117  			return err
  1118  		}
  1119  		count[bits]++
  1120  		total++
  1121  	}
  1122  
  1123  	if total == 0 {
  1124  		return fmt.Errorf("no votes recorded")
  1125  	}
  1126  
  1127  	// Get vote details to dump vote options.
  1128  	dr, err := p.voteDetails(token, v.PubKey)
  1129  	if err != nil {
  1130  		return err
  1131  	}
  1132  
  1133  	// Dump
  1134  	for _, vo := range dr.Vote.Params.Options {
  1135  		fmt.Printf("Vote Option:\n")
  1136  		fmt.Printf("  Id                   : %v\n", vo.ID)
  1137  		fmt.Printf("  Description          : %v\n",
  1138  			vo.Description)
  1139  		fmt.Printf("  Bit                  : %v\n", vo.Bit)
  1140  		vr := count[vo.Bit]
  1141  		fmt.Printf("  Votes received       : %v\n", vr)
  1142  		if total == 0 {
  1143  			continue
  1144  		}
  1145  		fmt.Printf("  Percentage           : %v%%\n",
  1146  			(float64(vr))/float64(total)*100)
  1147  	}
  1148  
  1149  	return nil
  1150  }
  1151  
  1152  type failedTuple struct {
  1153  	Time  JSONTime
  1154  	Votes tkv1.CastBallot `json:"votes"`
  1155  	Error ErrRetry
  1156  }
  1157  
  1158  func decodeFailed(filename string, failed map[string][]failedTuple) error {
  1159  	f, err := os.Open(filename)
  1160  	if err != nil {
  1161  		return err
  1162  	}
  1163  	defer f.Close()
  1164  	d := json.NewDecoder(f)
  1165  
  1166  	var (
  1167  		ft     *failedTuple
  1168  		ticket string
  1169  	)
  1170  	state := 0
  1171  	for {
  1172  		switch state {
  1173  		case 0:
  1174  			ft = &failedTuple{}
  1175  			err = d.Decode(&ft.Time)
  1176  			if err != nil {
  1177  				// Only expect EOF in state 0
  1178  				if err == io.EOF {
  1179  					goto exit
  1180  				}
  1181  				return fmt.Errorf("decode time (%v): %v",
  1182  					d.InputOffset(), err)
  1183  			}
  1184  			state = 1
  1185  
  1186  		case 1:
  1187  			err = d.Decode(&ft.Votes)
  1188  			if err != nil {
  1189  				return fmt.Errorf("decode cast votes (%v): %v",
  1190  					d.InputOffset(), err)
  1191  			}
  1192  
  1193  			// Save ticket
  1194  			if len(ft.Votes.Votes) != 1 {
  1195  				// Should not happen
  1196  				return fmt.Errorf("decode invalid length %v",
  1197  					len(ft.Votes.Votes))
  1198  			}
  1199  			ticket = ft.Votes.Votes[0].Ticket
  1200  
  1201  			state = 2
  1202  
  1203  		case 2:
  1204  			err = d.Decode(&ft.Error)
  1205  			if err != nil {
  1206  				return fmt.Errorf("decode error retry (%v): %v",
  1207  					d.InputOffset(), err)
  1208  			}
  1209  
  1210  			// Add to map
  1211  			if ticket == "" {
  1212  				return fmt.Errorf("decode no ticket found")
  1213  			}
  1214  			//fmt.Printf("failed ticket %v\n", ticket)
  1215  			failed[ticket] = append(failed[ticket], *ft)
  1216  
  1217  			// Reset statemachine
  1218  			ft = &failedTuple{}
  1219  			ticket = ""
  1220  			state = 0
  1221  		}
  1222  	}
  1223  
  1224  exit:
  1225  	return nil
  1226  }
  1227  
  1228  type successTuple struct {
  1229  	Time   JSONTime
  1230  	Result tkv1.CastVoteReply
  1231  }
  1232  
  1233  func decodeSuccess(filename string, success map[string][]successTuple) error {
  1234  	f, err := os.Open(filename)
  1235  	if err != nil {
  1236  		return err
  1237  	}
  1238  	defer f.Close()
  1239  	d := json.NewDecoder(f)
  1240  
  1241  	var st *successTuple
  1242  	state := 0
  1243  	for {
  1244  		switch state {
  1245  		case 0:
  1246  			st = &successTuple{}
  1247  			err = d.Decode(&st.Time)
  1248  			if err != nil {
  1249  				// Only expect EOF in state 0
  1250  				if err == io.EOF {
  1251  					goto exit
  1252  				}
  1253  				return fmt.Errorf("decode time (%v): %v",
  1254  					d.InputOffset(), err)
  1255  			}
  1256  			state = 1
  1257  
  1258  		case 1:
  1259  			err = d.Decode(&st.Result)
  1260  			if err != nil {
  1261  				return fmt.Errorf("decode cast votes (%v): %v",
  1262  					d.InputOffset(), err)
  1263  			}
  1264  
  1265  			// Add to map
  1266  			ticket := st.Result.Ticket
  1267  			if ticket == "" {
  1268  				return fmt.Errorf("decode no ticket found")
  1269  			}
  1270  
  1271  			//fmt.Printf("success ticket %v\n", ticket)
  1272  			success[ticket] = append(success[ticket], *st)
  1273  
  1274  			// Reset statemachine
  1275  			st = &successTuple{}
  1276  			state = 0
  1277  		}
  1278  	}
  1279  
  1280  exit:
  1281  	return nil
  1282  }
  1283  
  1284  type workTuple struct {
  1285  	Time  JSONTime
  1286  	Votes []voteAlarm
  1287  }
  1288  
  1289  func decodeWork(filename string, work map[string][]workTuple) error {
  1290  	f, err := os.Open(filename)
  1291  	if err != nil {
  1292  		return err
  1293  	}
  1294  	defer f.Close()
  1295  	d := json.NewDecoder(f)
  1296  
  1297  	var (
  1298  		wt *workTuple
  1299  		t  string
  1300  	)
  1301  	state := 0
  1302  	for {
  1303  		switch state {
  1304  		case 0:
  1305  			wt = &workTuple{}
  1306  			err = d.Decode(&wt.Time)
  1307  			if err != nil {
  1308  				// Only expect EOF in state 0
  1309  				if err == io.EOF {
  1310  					goto exit
  1311  				}
  1312  				return fmt.Errorf("decode time (%v): %v",
  1313  					d.InputOffset(), err)
  1314  			}
  1315  			t = wt.Time.Time
  1316  			state = 1
  1317  
  1318  		case 1:
  1319  			err = d.Decode(&wt.Votes)
  1320  			if err != nil {
  1321  				return fmt.Errorf("decode votes (%v): %v",
  1322  					d.InputOffset(), err)
  1323  			}
  1324  
  1325  			// Add to map
  1326  			if t == "" {
  1327  				return fmt.Errorf("decode no time found")
  1328  			}
  1329  
  1330  			work[t] = append(work[t], *wt)
  1331  
  1332  			// Reset statemachine
  1333  			wt = &workTuple{}
  1334  			t = ""
  1335  			state = 0
  1336  		}
  1337  	}
  1338  
  1339  exit:
  1340  	return nil
  1341  }
  1342  
  1343  func (p *piv) verifyVote(vote string) error {
  1344  	// Vote directory
  1345  	dir := filepath.Join(p.cfg.voteDir, vote)
  1346  
  1347  	// See if vote is ongoing
  1348  	vsr, err := p._summary(vote)
  1349  	if err != nil {
  1350  		return fmt.Errorf("could not obtain proposal status: %v",
  1351  			err)
  1352  	}
  1353  	vs, ok := vsr.Summaries[vote]
  1354  	if !ok {
  1355  		return fmt.Errorf("proposal does not exist: %v", vote)
  1356  	}
  1357  	if vs.Status != tkv1.VoteStatusFinished &&
  1358  		vs.Status != tkv1.VoteStatusRejected &&
  1359  		vs.Status != tkv1.VoteStatusApproved {
  1360  		return fmt.Errorf("proposal vote not finished: %v",
  1361  			tkv1.VoteStatuses[vs.Status])
  1362  	}
  1363  
  1364  	// Get server public key.
  1365  	v, err := p.getVersion()
  1366  	if err != nil {
  1367  		return err
  1368  	}
  1369  
  1370  	// Get and cache vote results.
  1371  	voteResultsFilename := filepath.Join(dir, ".voteresults")
  1372  	if !util.FileExists(voteResultsFilename) {
  1373  		rr, err := p.voteResults(vote, v.PubKey)
  1374  		if err != nil {
  1375  			return fmt.Errorf("failed to obtain vote results "+
  1376  				"for %v: %v\n", vote, err)
  1377  		}
  1378  		f, err := os.Create(voteResultsFilename)
  1379  		if err != nil {
  1380  			return fmt.Errorf("create cache: %v", err)
  1381  		}
  1382  		e := json.NewEncoder(f)
  1383  		err = e.Encode(rr)
  1384  		if err != nil {
  1385  			f.Close()
  1386  			_ = os.Remove(voteResultsFilename)
  1387  			return fmt.Errorf("encode cache: %v", err)
  1388  		}
  1389  		f.Close()
  1390  	}
  1391  
  1392  	// Open cached vote results.
  1393  	f, err := os.Open(voteResultsFilename)
  1394  	if err != nil {
  1395  		return fmt.Errorf("open cache: %v", err)
  1396  	}
  1397  	d := json.NewDecoder(f)
  1398  	var rr tkv1.ResultsReply
  1399  	err = d.Decode(&rr)
  1400  	if err != nil {
  1401  		f.Close()
  1402  		return fmt.Errorf("decode cache: %v", err)
  1403  	}
  1404  	f.Close()
  1405  
  1406  	// Get vote details.
  1407  	dr, err := p.voteDetails(vote, v.PubKey)
  1408  	if err != nil {
  1409  		return fmt.Errorf("failed to obtain vote details "+
  1410  			"for %v: %v\n", vote, err)
  1411  	}
  1412  
  1413  	// Index vote results for more vroom vroom
  1414  	eligible := make(map[string]string,
  1415  		len(dr.Vote.EligibleTickets))
  1416  	for _, v := range dr.Vote.EligibleTickets {
  1417  		eligible[v] = "" // XXX
  1418  	}
  1419  	cast := make(map[string]string, len(rr.Votes))
  1420  	for _, v := range rr.Votes {
  1421  		cast[v.Ticket] = "" // XXX
  1422  	}
  1423  
  1424  	// Create local work caches
  1425  	fa, err := os.ReadDir(dir)
  1426  	if err != nil {
  1427  		return err
  1428  	}
  1429  
  1430  	failed := make(map[string][]failedTuple, 128)   // [ticket]result
  1431  	success := make(map[string][]successTuple, 128) // [ticket]result
  1432  	work := make(map[string][]workTuple, 128)       // [time]work
  1433  
  1434  	fmt.Printf("== Checking vote %v\n", vote)
  1435  	for k := range fa {
  1436  		name := fa[k].Name()
  1437  
  1438  		filename := filepath.Join(dir, name)
  1439  		switch {
  1440  		case strings.HasPrefix(name, failedJournal):
  1441  			err = decodeFailed(filename, failed)
  1442  			if err != nil {
  1443  				fmt.Printf("decodeFailed %v: %v\n", filename,
  1444  					err)
  1445  			}
  1446  
  1447  		case strings.HasPrefix(name, successJournal):
  1448  			err = decodeSuccess(filename, success)
  1449  			if err != nil {
  1450  				fmt.Printf("decodeSuccess %v: %v\n", filename,
  1451  					err)
  1452  			}
  1453  
  1454  		case strings.HasPrefix(name, workJournal):
  1455  			err = decodeWork(filename, work)
  1456  			if err != nil {
  1457  				fmt.Printf("decodeWork %v: %v\n", filename,
  1458  					err)
  1459  			}
  1460  
  1461  		case name == ".voteresults":
  1462  			// Cache file, skip
  1463  
  1464  		default:
  1465  			fmt.Printf("unknown journal: %v\n", name)
  1466  		}
  1467  	}
  1468  
  1469  	// Count vote statistics
  1470  	type voteStat struct {
  1471  		ticket  string
  1472  		retries int
  1473  		failed  int
  1474  		success int
  1475  	}
  1476  
  1477  	verbose := false
  1478  	failedVotes := make(map[string]voteStat)
  1479  	tickets := make(map[string]string, 128) // [time]
  1480  	for k := range work {
  1481  		wts := work[k]
  1482  
  1483  		for kk := range wts {
  1484  			wt := wts[kk]
  1485  
  1486  			for kkk := range wt.Votes {
  1487  				vi := wt.Votes[kkk]
  1488  
  1489  				if kkk == 0 && verbose {
  1490  					fmt.Printf("Vote %v started: %v\n",
  1491  						vi.Vote.Token, wt.Time.Time)
  1492  				}
  1493  
  1494  				ticket := vi.Vote.Ticket
  1495  				tickets[ticket] = "" // XXX
  1496  				vs := voteStat{
  1497  					ticket: ticket,
  1498  				}
  1499  				if f, ok := failed[ticket]; ok {
  1500  					vs.retries = len(f)
  1501  				}
  1502  				if s, ok := success[ticket]; ok {
  1503  					vs.success = len(s)
  1504  					if len(s) != 1 {
  1505  						fmt.Printf("multiple success:"+
  1506  							" %v %v\n", len(s),
  1507  							ticket)
  1508  					}
  1509  				} else {
  1510  					vs.failed = 1
  1511  					failedVotes[ticket] = vs
  1512  				}
  1513  
  1514  				if verbose {
  1515  					fmt.Printf("  ticket: %v retries %v "+
  1516  						"success %v failed %v\n",
  1517  						vs.ticket, vs.retries,
  1518  						vs.success, vs.failed)
  1519  				}
  1520  			}
  1521  		}
  1522  	}
  1523  
  1524  	noVote := 0
  1525  	failedVote := 0
  1526  	completedNotRecorded := 0
  1527  	for _, v := range failedVotes {
  1528  		reason := "Error"
  1529  		if v.retries == 0 {
  1530  			if _, ok := cast[v.ticket]; ok {
  1531  				completedNotRecorded++
  1532  				continue
  1533  			}
  1534  			reason = "Not attempted"
  1535  			noVote++
  1536  		}
  1537  		if v.failed != 0 {
  1538  			fmt.Printf("  FAILED: %v - %v\n", v.ticket, reason)
  1539  			failedVote++
  1540  			continue
  1541  		}
  1542  	}
  1543  	if noVote != 0 {
  1544  		fmt.Printf("  votes that were not attempted: %v\n", noVote)
  1545  	}
  1546  	if failedVote != 0 {
  1547  		fmt.Printf("  votes that failed: %v\n", failedVote)
  1548  	}
  1549  	if completedNotRecorded != 0 {
  1550  		fmt.Printf("  votes that completed but were not recorded: %v\n",
  1551  			completedNotRecorded)
  1552  	}
  1553  
  1554  	// Cross check results
  1555  	eligibleNotFound := 0
  1556  	for ticket := range tickets {
  1557  		// Did politea see ticket
  1558  		if _, ok := eligible[ticket]; !ok {
  1559  			fmt.Printf("work ticket not eligble: %v\n", ticket)
  1560  			eligibleNotFound++
  1561  		}
  1562  
  1563  		// Did politea complete vote
  1564  		_, successFound := success[ticket]
  1565  		_, failedFound := failedVotes[ticket]
  1566  		switch {
  1567  		case successFound && failedFound:
  1568  			fmt.Printf("  pi vote succeeded and failed, " +
  1569  				"impossible condition\n")
  1570  		case !successFound && failedFound:
  1571  			if _, ok := cast[ticket]; !ok {
  1572  				fmt.Printf("  pi vote failed: %v\n", ticket)
  1573  			}
  1574  		case successFound && !failedFound:
  1575  			// Vote succeeded on the first try
  1576  		case !successFound && !failedFound:
  1577  			fmt.Printf("  pi vote not seen: %v\n", ticket)
  1578  		}
  1579  	}
  1580  
  1581  	if eligibleNotFound != 0 {
  1582  		fmt.Printf("  ineligible tickets: %v\n", eligibleNotFound)
  1583  	}
  1584  
  1585  	// Print overall status
  1586  	fmt.Printf("  Total votes       : %v\n", len(tickets))
  1587  	fmt.Printf("  Successful votes  : %v\n", len(success)+
  1588  		completedNotRecorded)
  1589  	fmt.Printf("  Unsuccessful votes: %v\n", failedVote)
  1590  	if failedVote != 0 {
  1591  		fmt.Printf("== Failed votes on proposal %v\n", vote)
  1592  	} else {
  1593  		fmt.Printf("== NO failed votes on proposal %v\n", vote)
  1594  	}
  1595  
  1596  	return nil
  1597  }
  1598  
  1599  func (p *piv) verify(args []string) error {
  1600  	// Override 0 to list all possible votes.
  1601  	if len(args) == 0 {
  1602  		fa, err := os.ReadDir(p.cfg.voteDir)
  1603  		if err != nil {
  1604  			return err
  1605  		}
  1606  		fmt.Printf("Votes:\n")
  1607  		for k := range fa {
  1608  			_, err := hex.DecodeString(fa[k].Name())
  1609  			if err != nil {
  1610  				continue
  1611  			}
  1612  			fmt.Printf("  %v\n", fa[k].Name())
  1613  		}
  1614  	}
  1615  
  1616  	if len(args) == 1 && args[0] == "ALL" {
  1617  		fa, err := os.ReadDir(p.cfg.voteDir)
  1618  		if err != nil {
  1619  			return err
  1620  		}
  1621  		for k := range fa {
  1622  			_, err := hex.DecodeString(fa[k].Name())
  1623  			if err != nil {
  1624  				continue
  1625  			}
  1626  
  1627  			err = p.verifyVote(fa[k].Name())
  1628  			if err != nil {
  1629  				fmt.Printf("verifyVote: %v\n", err)
  1630  			}
  1631  		}
  1632  
  1633  		return nil
  1634  	}
  1635  
  1636  	for k := range args {
  1637  		_, err := hex.DecodeString(args[k])
  1638  		if err != nil {
  1639  			fmt.Printf("invalid vote: %v\n", args[k])
  1640  			continue
  1641  		}
  1642  
  1643  		err = p.verifyVote(args[k])
  1644  		if err != nil {
  1645  			fmt.Printf("verifyVote: %v\n", err)
  1646  		}
  1647  	}
  1648  
  1649  	return nil
  1650  }
  1651  
  1652  func (p *piv) help(command string) {
  1653  	switch command {
  1654  	case cmdInventory:
  1655  		fmt.Fprintf(os.Stdout, "%s\n", inventoryHelpMsg)
  1656  	case cmdVote:
  1657  		fmt.Fprintf(os.Stdout, "%s\n", voteHelpMsg)
  1658  	case cmdTally:
  1659  		fmt.Fprintf(os.Stdout, "%s\n", tallyHelpMsg)
  1660  	case cmdVerify:
  1661  		fmt.Fprintf(os.Stdout, "%s\n", verifyHelpMsg)
  1662  	}
  1663  }
  1664  
  1665  func _main() error {
  1666  	appName := filepath.Base(os.Args[0])
  1667  	appName = strings.TrimSuffix(appName, filepath.Ext(appName))
  1668  	cfg, args, err := loadConfig(appName)
  1669  	if err != nil {
  1670  		usageMessage := fmt.Sprintf("Use %s -h to show usage", appName)
  1671  		fmt.Fprintln(os.Stderr, err)
  1672  		var e errSuppressUsage
  1673  		if !errors.As(err, &e) {
  1674  			fmt.Fprintln(os.Stderr, usageMessage)
  1675  		}
  1676  		return err
  1677  	}
  1678  	defer func() {
  1679  		if logRotator != nil {
  1680  			logRotator.Close()
  1681  		}
  1682  	}()
  1683  	if len(args) == 0 {
  1684  		err := fmt.Errorf("No command specified\n%s", listCmdMessage)
  1685  		fmt.Fprintln(os.Stderr, err)
  1686  		return err
  1687  	}
  1688  	action := args[0]
  1689  
  1690  	// Get a context that will be canceled when a shutdown signal has been
  1691  	// triggered either from an OS signal such as SIGINT (Ctrl+C) or from
  1692  	// another subsystem such as the RPC server.
  1693  	shutdownCtx := shutdownListener()
  1694  
  1695  	// Contact WWW
  1696  	c, err := firstContact(shutdownCtx, cfg)
  1697  	if err != nil {
  1698  		return err
  1699  	}
  1700  	// Close GRPC
  1701  	defer c.conn.Close()
  1702  
  1703  	// Validate command
  1704  	switch action {
  1705  	case cmdInventory, cmdTally, cmdVote:
  1706  		// These commands require a connection to a dcrwallet instance. Get
  1707  		// block height to validate GPRC creds.
  1708  		ar, err := c.wallet.Accounts(c.ctx, &pb.AccountsRequest{})
  1709  		if err != nil {
  1710  			return err
  1711  		}
  1712  		log.Debugf("Current wallet height: %v", ar.CurrentBlockHeight)
  1713  
  1714  	case cmdVerify, cmdHelp:
  1715  		// valid command, continue
  1716  
  1717  	default:
  1718  		err := fmt.Errorf("Unrecognized command %q\n%s", action, listCmdMessage)
  1719  		fmt.Fprintln(os.Stderr, err)
  1720  		return err
  1721  	}
  1722  
  1723  	// Run command
  1724  	switch action {
  1725  	case cmdInventory:
  1726  		err = c.inventory()
  1727  	case cmdVote:
  1728  		err = c.vote(args[1:])
  1729  	case cmdTally:
  1730  		err = c.tally(args[1:])
  1731  	case cmdVerify:
  1732  		err = c.verify(args[1:])
  1733  	case cmdHelp:
  1734  		if len(args) < 2 {
  1735  			err := fmt.Errorf("No help command specified\n%s", listCmdMessage)
  1736  			fmt.Fprintln(os.Stderr, err)
  1737  			return err
  1738  		}
  1739  		c.help(args[1])
  1740  	}
  1741  
  1742  	if err != nil {
  1743  		log.Error(err)
  1744  	}
  1745  	return err
  1746  }
  1747  
  1748  func main() {
  1749  	if err := _main(); err != nil {
  1750  		os.Exit(1)
  1751  	}
  1752  }