Skip to content

Commit

Permalink
politeiavoter: Improve hoursprior UX.
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
lukebp authored Dec 9, 2021
1 parent ba9da84 commit ede2886
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 45 deletions.
24 changes: 20 additions & 4 deletions politeiawww/cmd/politeiavoter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -92,14 +95,15 @@ 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"`

voteDir string
dial func(string, string) (net.Conn, error)
voteDuration time.Duration // Parsed VoteDuration
hoursPrior time.Duration // Converted HoursPrior
blocksPerHour uint64

// Test only
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 "+
Expand Down
60 changes: 47 additions & 13 deletions politeiawww/cmd/politeiavoter/politeiavoter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down
78 changes: 78 additions & 0 deletions politeiawww/cmd/politeiavoter/politeiavoter_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
16 changes: 6 additions & 10 deletions politeiawww/cmd/politeiavoter/trickle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand Down
22 changes: 4 additions & 18 deletions politeiawww/cmd/politeiavoter/trickle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand Down

0 comments on commit ede2886

Please sign in to comment.