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 `