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 }