From ede28866c013ed15801fb77b5f9cca647c1fb734 Mon Sep 17 00:00:00 2001 From: lukebp Date: Thu, 9 Dec 2021 07:59:25 -0600 Subject: [PATCH] politeiavoter: Improve hoursprior UX. This commit improves the --hoursprior UX by updating politeiavoter to the following behavior. By default, the trickler will trickle in votes for the full duration of the vote minus 12h. The --voteduration and the --hoursprior settings are not allowed to be used at the same time. An error is returned if they are attempted to be used together. If a --voteduration is specified, the votes are trickled in over the specified duration. If a --hoursprior is specified, the votes are trickled in over the full duration of the vote minus the specified hours prior. If the calculated vote duration is less than 24h, the user MUST manually set the duration using the --voteduration flag. **Example 1** A vote has 6 days remaining. The user attempts to trickle in their votes over a 3h period using --voteduration=3h. Previously, they would get the following error. ``` not enough time left to trickle votes: -9h0m0s < 12h0m0s, use --hoursprior to modify this behavior ``` Using this commit, no error is returned and the votes are trickled in over the 3h period. **Example 2** A vote has 6 hours remaining. The user attempts to trickle in their votes without specifying a --voteduration. Previously, they would get the following error. ``` not enough time left to trickle votes: -6h0m0s < 12h0m0s, use --hoursprior to modify this behavior ``` With this commit, they now get the following error. ``` there is only 6h0m0s left in the vote; when the remaining time is this low you must use --voteduration to manually set the duration that will be used to trickle in your votes, example --voteduration=6h ``` **Example 3** There is 28h left in a vote. The user attempts to trickle in their votes without specifying a --voteduration. Previously, they would get the following error. ``` not enough time left to trickle votes: 4h0m0s < 12h0m0s, use --hoursprior to modify this behavior ``` With this commit, the calculated vote duration is 16h (28h left in the vote minus the default 12h hours prior). 16h is less than the required 24h vote duration so they get the following error. ``` there is only 28h0m0s left in the vote; when the remaining time is this low you must use --voteduration to manually set the duration that will be used to trickle in your votes, example --voteduration=6h ``` --- politeiawww/cmd/politeiavoter/config.go | 24 +++++- .../cmd/politeiavoter/politeiavoter.go | 60 ++++++++++---- .../cmd/politeiavoter/politeiavoter_test.go | 78 +++++++++++++++++++ politeiawww/cmd/politeiavoter/trickle.go | 16 ++-- politeiawww/cmd/politeiavoter/trickle_test.go | 22 +----- 5 files changed, 155 insertions(+), 45 deletions(-) create mode 100644 politeiawww/cmd/politeiavoter/politeiavoter_test.go diff --git a/politeiawww/cmd/politeiavoter/config.go b/politeiawww/cmd/politeiavoter/config.go index 88df3ed89..5ea439f4e 100644 --- a/politeiawww/cmd/politeiavoter/config.go +++ b/politeiawww/cmd/politeiavoter/config.go @@ -39,8 +39,7 @@ const ( clientCertFile = "client.pem" clientKeyFile = "client-key.pem" - defaultHoursPrior = uint64(12) - defaultBunches = uint(1) + defaultBunches = uint(1) // Testing stuff testNormal = 0 @@ -56,6 +55,10 @@ var ( defaultWalletCert = filepath.Join(dcrwalletHomeDir, walletCertFile) defaultClientCert = filepath.Join(defaultHomeDir, clientCertFile) defaultClientKey = filepath.Join(defaultHomeDir, clientKeyFile) + + // defaultHoursPrior is the default HoursPrior config value. It's required + // to be var and not a const since the HoursPrior setting is a pointer. + defaultHoursPrior = uint64(12) ) // runServiceCommand is only set to a real function on Windows. It is used @@ -92,7 +95,7 @@ type config struct { // voting period and is set to a default of 12 hours. These extra // hours, prior to expiration gives the user some additional margin to // correct failures. - HoursPrior uint64 `long:"hoursprior" description:"Number of hours to subtract from available voting window."` + HoursPrior *uint64 `long:"hoursprior" description:"Number of hours prior to the end of the voting period that all votes will be trickled in by."` ClientCert string `long:"clientcert" description:"Path to TLS certificate for client authentication"` ClientKey string `long:"clientkey" description:"Path to TLS client authentication key"` @@ -100,6 +103,7 @@ type config struct { voteDir string dial func(string, string) (net.Conn, error) voteDuration time.Duration // Parsed VoteDuration + hoursPrior time.Duration // Converted HoursPrior blocksPerHour uint64 // Test only @@ -232,7 +236,7 @@ func loadConfig() (*config, []string, error) { ClientCert: defaultClientCert, ClientKey: defaultClientKey, Bunches: defaultBunches, - HoursPrior: defaultHoursPrior, + // HoursPrior default is set below } // Service options which are only added on Windows. @@ -512,6 +516,18 @@ func loadConfig() (*config, []string, error) { "%v", err) } } + + // Configure the hours prior setting + if cfg.HoursPrior != nil && cfg.VoteDuration != "" { + return nil, nil, fmt.Errorf("--hoursprior and " + + "--voteduration cannot both be set") + } + if cfg.HoursPrior == nil { + // Hours prior setting was not provided. Use the default. + cfg.HoursPrior = &defaultHoursPrior + } + cfg.hoursPrior = time.Duration(*cfg.HoursPrior) * time.Hour + // Number of bunches if cfg.Bunches < 1 || cfg.Bunches > 100 { return nil, nil, fmt.Errorf("invalid number of bunches "+ diff --git a/politeiawww/cmd/politeiavoter/politeiavoter.go b/politeiawww/cmd/politeiavoter/politeiavoter.go index ea8cf048c..072eda3dd 100644 --- a/politeiawww/cmd/politeiavoter/politeiavoter.go +++ b/politeiawww/cmd/politeiavoter/politeiavoter.go @@ -967,22 +967,20 @@ func (p *piv) _vote(token, voteID string) error { return fmt.Errorf("signature failed index %v: %v", k, v.Error) } + // Trickle in the votes if specified if p.cfg.Trickle { - go p.statsHandler() - - // Calculate vote duration if not set - if p.cfg.voteDuration.Seconds() == 0 { - blocksLeft := int64(vs.EndBlockHeight) - int64(bestBlock) - if blocksLeft < int64(p.cfg.HoursPrior*p.cfg.blocksPerHour) { - return fmt.Errorf("less than twelve hours " + - "left to vote, please set " + - "--voteduration manually") - } - p.cfg.voteDuration = activeNetParams.TargetTimePerBlock * - (time.Duration(blocksLeft) - - time.Duration(p.cfg.HoursPrior*p.cfg.blocksPerHour)) + // Setup the trickler vote duration + var ( + blocksLeft = int64(vs.EndBlockHeight) - int64(bestBlock) + blockTime = activeNetParams.TargetTimePerBlock + timeLeftInVote = time.Duration(blocksLeft) * blockTime + ) + err = p.setupVoteDuration(timeLeftInVote) + if err != nil { + return err } + // Trickle votes return p.alarmTrickler(token, voteBit, ctres, smr) } @@ -1025,6 +1023,42 @@ func (p *piv) _vote(token, voteID string) error { return nil } +// setupVoteDuration sets up the duration that will be used for trickling +// votes. The user can either set a duration manually using the --voteduration +// setting or this function will calculate a duration. The calculated duration +// is the remaining time left in the vote minus the --hoursprior setting. +func (p *piv) setupVoteDuration(timeLeftInVote time.Duration) error { + switch { + case p.cfg.voteDuration.Seconds() > 0: + // A vote duration was provided + if p.cfg.voteDuration > timeLeftInVote { + return fmt.Errorf("the provided --voteduration of %v is "+ + "greater than the remaining time in the vote of %v", + p.cfg.voteDuration, timeLeftInVote) + } + + case p.cfg.voteDuration.Seconds() == 0: + // A vote duration was not provided. The vote duration is set to + // the remaining time in the vote minus the hours prior setting. + p.cfg.voteDuration = timeLeftInVote - p.cfg.hoursPrior + + // Force the user to manually set the vote duration when the + // calculated duration is under 24h. + if p.cfg.voteDuration < (24 * time.Hour) { + return fmt.Errorf("there is only %v left in the vote; when "+ + "the remaining time is this low you must use --voteduration "+ + "to manually set the duration that will be used to trickle "+ + "in your votes, example --voteduration=6h", timeLeftInVote) + } + + default: + // Should not be possible + return fmt.Errorf("invalid vote duration %v", p.cfg.voteDuration) + } + + return nil +} + func (p *piv) vote(args []string) error { if len(args) != 2 { return fmt.Errorf("vote: not enough arguments %v", args) diff --git a/politeiawww/cmd/politeiavoter/politeiavoter_test.go b/politeiawww/cmd/politeiavoter/politeiavoter_test.go new file mode 100644 index 000000000..451639a9a --- /dev/null +++ b/politeiawww/cmd/politeiavoter/politeiavoter_test.go @@ -0,0 +1,78 @@ +// Copyright (c) 2021 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "testing" + "time" +) + +func TestSetupVoteDuration(t *testing.T) { + // Setup piv context + p, cleanup := fakePiv(t, 0, 1) + defer cleanup() + + // Setup tests + var tests = []struct { + name string + voteDuration time.Duration + hoursPrior time.Duration + timeLeftInVote time.Duration + wantErr bool + }{ + { + "provided vote duration exceeds remaining time", + 2 * time.Hour, + 0, + 1 * time.Hour, + true, + }, + { + "calculated vote duration is under 24 hours", + 0, + 12 * time.Hour, + 35 * time.Hour, + true, + }, + { + "vote duration provided success", + 1 * time.Hour, + 0, + 2 * time.Hour, + false, + }, + { + "vote duration not provided success", + 0, + 12 * time.Hour, + 36 * time.Hour, + false, + }, + } + + // Run tests + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup piv config + p.cfg.voteDuration = tc.voteDuration + p.cfg.hoursPrior = tc.hoursPrior + + // Run test + err := p.setupVoteDuration(tc.timeLeftInVote) + switch { + case err != nil && tc.wantErr: + // Test passes + return + case err == nil && !tc.wantErr: + // Test passes + return + default: + // Test fails + t.Errorf("got err %v, want err %v", + err == nil, tc.wantErr) + } + }) + } +} diff --git a/politeiawww/cmd/politeiavoter/trickle.go b/politeiawww/cmd/politeiavoter/trickle.go index 1a42dbff5..1eb6608e8 100644 --- a/politeiawww/cmd/politeiavoter/trickle.go +++ b/politeiawww/cmd/politeiavoter/trickle.go @@ -60,18 +60,10 @@ func (p *piv) generateVoteAlarm(token, voteBit string, ctres *pb.CommittedTicket } bunches := int(p.cfg.Bunches) - duration := p.cfg.voteDuration - voteDuration := duration - time.Duration(p.cfg.HoursPrior)*time.Hour - vd := time.Duration(p.cfg.HoursPrior) * time.Hour - if voteDuration < vd { - return nil, fmt.Errorf("not enough time left to trickle "+ - "votes: %v < %v, use --hoursprior to modify this "+ - "behavior", voteDuration, vd) - } + voteDuration := p.cfg.voteDuration fmt.Printf("Total number of votes : %v\n", len(ctres.TicketAddresses)) fmt.Printf("Total number of bunches: %v\n", bunches) - fmt.Printf("Total vote duration : %v\n", duration) - fmt.Printf("Duration calculated : %v\n", voteDuration) + fmt.Printf("Vote duration : %v\n", voteDuration) // Initialize bunches tStart := make([]time.Time, bunches) @@ -308,6 +300,7 @@ func randomTime(d time.Duration) (time.Time, time.Time, error) { } func (p *piv) alarmTrickler(token, voteBit string, ctres *pb.CommittedTicketsResponse, smr *pb.SignMessagesResponse) error { + // Generate work queue votes, err := p.generateVoteAlarm(token, voteBit, ctres, smr) if err != nil { return err @@ -319,6 +312,9 @@ func (p *piv) alarmTrickler(token, voteBit string, ctres *pb.CommittedTicketsRes return err } + // Launch the voting stats handler + go p.statsHandler() + // Launch voting go routines eg, ectx := errgroup.WithContext(p.ctx) p.ballotResults = make([]tkv1.CastVoteReply, 0, len(ctres.TicketAddresses)) diff --git a/politeiawww/cmd/politeiavoter/trickle_test.go b/politeiawww/cmd/politeiavoter/trickle_test.go index 7fa2446cc..99d7fc0f6 100644 --- a/politeiawww/cmd/politeiavoter/trickle_test.go +++ b/politeiawww/cmd/politeiavoter/trickle_test.go @@ -37,7 +37,7 @@ func fakeTickets(x uint) (*pb.CommittedTicketsResponse, *pb.SignMessagesResponse return &ctres, &smr } -func fakePiv(t *testing.T, d time.Duration, x uint, hoursPrior uint64) (*piv, func()) { +func fakePiv(t *testing.T, d time.Duration, x uint) (*piv, func()) { // Setup temp home dir homeDir, err := ioutil.TempDir("", "politeiavoter.test") if err != nil { @@ -61,29 +61,15 @@ func fakePiv(t *testing.T, d time.Duration, x uint, hoursPrior uint64) (*piv, fu HomeDir: homeDir, voteDir: filepath.Join(homeDir, defaultVoteDirname), voteDuration: d, - HoursPrior: hoursPrior, Bunches: x, testing: true, }, }, cleanup } -func TestTrickleNotEnoughTime(t *testing.T) { - x := uint(10) - c, cleanup := fakePiv(t, time.Hour, x, 1) - defer cleanup() - - ctres, smr := fakeTickets(x) - err := c.alarmTrickler("token", "voteBit", ctres, smr) - t.Logf("error received: %v", err) - if err == nil { - t.Fatal("expected error") - } -} - func TestTrickleWorkers(t *testing.T) { bunches := uint(3) - c, cleanup := fakePiv(t, time.Minute, bunches, 0) + c, cleanup := fakePiv(t, time.Minute, bunches) defer cleanup() nrVotes := uint(20) @@ -95,7 +81,7 @@ func TestTrickleWorkers(t *testing.T) { } func TestUnrecoverableTrickleWorkers(t *testing.T) { - c, cleanup := fakePiv(t, 10*time.Second, 1, 0) + c, cleanup := fakePiv(t, 10*time.Second, 1) defer cleanup() c.cfg.testingMode = testFailUnrecoverable @@ -113,7 +99,7 @@ func TestManyTrickleWorkers(t *testing.T) { } bunches := uint(10) - c, cleanup := fakePiv(t, 2*time.Minute, bunches, 0) + c, cleanup := fakePiv(t, 2*time.Minute, bunches) defer cleanup() nrVotes := uint(20000)