github.com/decred/politeia@v1.4.0/politeiawww/cmd/pictl/cmdcastballot.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 main
     6  
     7  import (
     8  	"context"
     9  	"encoding/hex"
    10  	"fmt"
    11  	"strconv"
    12  
    13  	"decred.org/dcrwallet/rpc/walletrpc"
    14  	"github.com/decred/dcrd/chaincfg/chainhash"
    15  	"github.com/decred/politeia/politeiad/api/v1/identity"
    16  	tkv1 "github.com/decred/politeia/politeiawww/api/ticketvote/v1"
    17  	pclient "github.com/decred/politeia/politeiawww/client"
    18  )
    19  
    20  // cmdCastBallot casts a ballot of votes.
    21  type cmdCastBallot struct {
    22  	Args struct {
    23  		Token  string `positional-arg-name:"token"`
    24  		VoteID string `positional-arg-name:"voteid"`
    25  	} `positional-args:"true" required:"true"`
    26  	Password string `long:"password" optional:"true"`
    27  }
    28  
    29  // Execute executes the cmdCastBallot command.
    30  //
    31  // This function satisfies the go-flags Commander interface.
    32  func (c *cmdCastBallot) Execute(args []string) error {
    33  	// Unpack args
    34  	var (
    35  		token  = c.Args.Token
    36  		voteID = c.Args.VoteID
    37  	)
    38  
    39  	// Setup politeiawww client
    40  	opts := pclient.Opts{
    41  		HTTPSCert: cfg.HTTPSCert,
    42  		Verbose:   cfg.Verbose,
    43  		RawJSON:   cfg.RawJSON,
    44  	}
    45  	pc, err := pclient.New(cfg.Host, opts)
    46  	if err != nil {
    47  		return err
    48  	}
    49  
    50  	// Setup dcrwallet client
    51  	ctx := context.Background()
    52  	wc, err := newDcrwalletClient(cfg.WalletHost, cfg.WalletCert,
    53  		cfg.ClientCert, cfg.ClientKey)
    54  	if err != nil {
    55  		return err
    56  	}
    57  	defer wc.conn.Close()
    58  
    59  	// Get vote details
    60  	d := tkv1.Details{
    61  		Token: token,
    62  	}
    63  	dr, err := pc.TicketVoteDetails(d)
    64  	if err != nil {
    65  		return err
    66  	}
    67  	if dr.Vote == nil {
    68  		return fmt.Errorf("vote not started")
    69  	}
    70  	voteDetails := dr.Vote
    71  
    72  	// Verify provided vote ID
    73  	var voteBit string
    74  	for _, option := range voteDetails.Params.Options {
    75  		if voteID == option.ID {
    76  			voteBit = strconv.FormatUint(option.Bit, 16)
    77  			break
    78  		}
    79  	}
    80  	if voteBit == "" {
    81  		return fmt.Errorf("vote id not found: %v", voteID)
    82  	}
    83  
    84  	// Get the user's tickets that are eligible to vote
    85  	ticketPool := make([][]byte, 0, len(voteDetails.EligibleTickets))
    86  	for _, v := range voteDetails.EligibleTickets {
    87  		h, err := chainhash.NewHashFromStr(v)
    88  		if err != nil {
    89  			return err
    90  		}
    91  		ticketPool = append(ticketPool, h[:])
    92  	}
    93  	ct := walletrpc.CommittedTicketsRequest{
    94  		Tickets: ticketPool,
    95  	}
    96  	ctr, err := wc.wallet.CommittedTickets(ctx, &ct)
    97  	if err != nil {
    98  		return fmt.Errorf("CommittedTickets: %v", err)
    99  	}
   100  	if len(ctr.TicketAddresses) == 0 {
   101  		return fmt.Errorf("user has no eligible tickets")
   102  	}
   103  
   104  	// Compile the ticket hashes of the user's eligible tickets
   105  	eligibleTickets := make([]string, 0, len(ctr.TicketAddresses))
   106  	for _, v := range ctr.TicketAddresses {
   107  		h, err := chainhash.NewHash(v.Ticket)
   108  		if err != nil {
   109  			return fmt.Errorf("NewHash %x: %v", v.Ticket, err)
   110  		}
   111  		eligibleTickets = append(eligibleTickets, h.String())
   112  	}
   113  
   114  	// The next step is to have the user's wallet sign the proposal
   115  	// votes for each ticket. The wallet password is needed for this.
   116  	var passphrase []byte
   117  	if c.Password != "" {
   118  		// Password was provided
   119  		passphrase = []byte(c.Password)
   120  	} else {
   121  		// Prompt user for password
   122  		passphrase, err = promptWalletPassword()
   123  		if err != nil {
   124  			return err
   125  		}
   126  	}
   127  
   128  	// Sign eligible tickets with vote preference
   129  	messages := make([]*walletrpc.SignMessagesRequest_Message, 0,
   130  		len(eligibleTickets))
   131  	for i, v := range ctr.TicketAddresses {
   132  		// ctr.TicketAddresses and eligibleTickets share the same ordering
   133  		msg := token + eligibleTickets[i] + voteBit
   134  		messages = append(messages, &walletrpc.SignMessagesRequest_Message{
   135  			Address: v.Address,
   136  			Message: msg,
   137  		})
   138  	}
   139  	sm := walletrpc.SignMessagesRequest{
   140  		Passphrase: passphrase,
   141  		Messages:   messages,
   142  	}
   143  	sigs, err := wc.wallet.SignMessages(ctx, &sm)
   144  	if err != nil {
   145  		return fmt.Errorf("SignMessages: %v", err)
   146  	}
   147  	for i, r := range sigs.Replies {
   148  		if r.Error != "" {
   149  			return fmt.Errorf("vote signature failed for ticket %v: %v",
   150  				eligibleTickets[i], err)
   151  		}
   152  	}
   153  
   154  	// Setup ballot request
   155  	votes := make([]tkv1.CastVote, 0, len(eligibleTickets))
   156  	for i, ticket := range eligibleTickets {
   157  		// eligibleTickets and sigs use the same index
   158  		votes = append(votes, tkv1.CastVote{
   159  			Token:     token,
   160  			Ticket:    ticket,
   161  			VoteBit:   voteBit,
   162  			Signature: hex.EncodeToString(sigs.Replies[i].Signature),
   163  		})
   164  	}
   165  	cb := tkv1.CastBallot{
   166  		Votes: votes,
   167  	}
   168  
   169  	// Send ballot request
   170  	cbr, err := pc.TicketVoteCastBallot(cb)
   171  	if err != nil {
   172  		return err
   173  	}
   174  
   175  	// Get the server pubkey so that we can validate the receipts.
   176  	version, err := client.Version()
   177  	if err != nil {
   178  		return fmt.Errorf("Version: %v", err)
   179  	}
   180  	serverID, err := identity.PublicIdentityFromString(version.PubKey)
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	// Check for any failed votes. Vote receipts don't include the
   186  	// ticket hash so in order to associate a failed receipt with a
   187  	// specific ticket, we need  to lookup the ticket hash and store
   188  	// it separately.
   189  	failedReceipts := make([]tkv1.CastVoteReply, 0, len(cbr.Receipts))
   190  	failedTickets := make([]string, 0, len(eligibleTickets))
   191  	for i, v := range cbr.Receipts {
   192  		// Lookup ticket hash. br.Receipts and eligibleTickets use the
   193  		// same ordering
   194  		h := eligibleTickets[i]
   195  
   196  		// Check for vote error
   197  		if v.ErrorCode != nil {
   198  			failedReceipts = append(failedReceipts, v)
   199  			failedTickets = append(failedTickets, h)
   200  			continue
   201  		}
   202  
   203  		// Verify receipts
   204  		sig, err := identity.SignatureFromString(v.Receipt)
   205  		if err != nil {
   206  			printf("Failed to decode receipt: %v\n", v.Ticket)
   207  			continue
   208  		}
   209  		clientSig := votes[i].Signature
   210  		if !serverID.VerifyMessage([]byte(clientSig), *sig) {
   211  			printf("Failed to verify receipt: %v", v.Ticket)
   212  			continue
   213  		}
   214  	}
   215  
   216  	// Print results
   217  	printf("Votes succeeded: %v\n", len(cbr.Receipts)-len(failedReceipts))
   218  	printf("Votes failed   : %v\n", len(failedReceipts))
   219  	for i, v := range failedReceipts {
   220  		printf("Failed vote    : %v %v\n", failedTickets[i], v.ErrorContext)
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  // castBallotHelpMsg is printed to stdout by the help command.
   227  const castBallotHelpMsg = `castballot "token" "voteid"
   228  
   229  Cast a ballot of dcr ticket votes. This command will only work when on testnet
   230  and when running dcrwallet locally on the default port.
   231  
   232  Arguments:
   233  1. token   (string, optional)  Proposal censorship token
   234  2. voteid  (string, optional)  Vote option ID (e.g. yes)
   235  
   236  Flags:
   237   --password  (string, optional)  Wallet password. You will be prompted for the
   238                                   password if one is not provided.
   239  `