Skip to content

Commit 51a60c4

Browse files
Make trickler truly random instead of using random bucketized durations (#1368)
* Make trickler truly random instead of using random bucketized durations * Remove comments now that the code has been validated
1 parent 9019d9d commit 51a60c4

File tree

5 files changed

+246
-111
lines changed

5 files changed

+246
-111
lines changed

politeiawww/cmd/politeiavoter/config.go

+5-6
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ type config struct {
7979
ClientCert string `long:"clientcert" description:"Path to TLS certificate for client authentication (default: client.pem)"`
8080
ClientKey string `long:"clientkey" description:"Path to TLS client authentication key (default: client-key.pem)"`
8181

82-
voteDir string
83-
dial func(string, string) (net.Conn, error)
84-
voteDuration time.Duration // Parsed VoteDuration
85-
blocksPerDay uint64
82+
voteDir string
83+
dial func(string, string) (net.Conn, error)
84+
voteDuration time.Duration // Parsed VoteDuration
85+
blocksPerHour uint64
8686
}
8787

8888
// serviceOptions defines the configuration options for the daemon as a service
@@ -359,8 +359,7 @@ func loadConfig() (*config, []string, error) {
359359
}
360360

361361
// Calculate blocks per day
362-
cfg.blocksPerDay = uint64(24 * time.Hour /
363-
activeNetParams.TargetTimePerBlock)
362+
cfg.blocksPerHour = uint64(time.Hour / activeNetParams.TargetTimePerBlock)
364363

365364
// Determine default connections
366365
if cfg.PoliteiaWWW == "" {

politeiawww/cmd/politeiavoter/politeiavoter.go

+6-105
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,8 @@ type ctx struct {
193193
// timing intervals and vote details. This is a JSON structure for logging
194194
// purposes.
195195
type voteInterval struct {
196-
Vote v1.CastVote `json:"vote"` // RPC vote
197-
Votes int `json:"votes"` // Always 1 for now
198-
Total time.Duration `json:"total"` // Cumulative time
199-
At time.Duration `json:"at"` // Delay to fire off vote
196+
Vote v1.CastVote `json:"vote"` // RPC vote
197+
At time.Duration `json:"at"` // Delay to fire off vote
200198
}
201199

202200
func newClient(shutdownCtx context.Context, cfg *config) (*ctx, error) {
@@ -781,103 +779,6 @@ func (c *ctx) voteIntervalLen() uint64 {
781779
return uint64(c.voteIntervalQ.Len())
782780
}
783781

784-
// calculateTrickle calculates the trickle schedule.
785-
func (c *ctx) calculateTrickle(token, voteBit string, ctres *pb.CommittedTicketsResponse, smr *pb.SignMessagesResponse) error {
786-
votes := uint64(len(ctres.TicketAddresses))
787-
duration := c.cfg.voteDuration
788-
maxDelay := uint64(duration.Seconds() / float64(votes) * 2)
789-
minAvgInterval := uint64(35)
790-
fmt.Printf("Votes : %v\n", votes)
791-
fmt.Printf("Duration : %v\n", duration)
792-
fmt.Printf("Avg Interval: %v\n", time.Duration(maxDelay/2)*time.Second)
793-
794-
// Ensure that the duration allows for sufficiently randomized delays
795-
// in between votes
796-
if duration.Seconds() < float64(minAvgInterval)*float64(votes) {
797-
return fmt.Errorf("Vote duration must be at least %v",
798-
time.Duration(float64(minAvgInterval)*float64(votes))*time.Second)
799-
}
800-
801-
// Create array of work to be done. Vote delays are random durations
802-
// between [0, maxDelay] (exclusive) which means that the vote delay
803-
// average will converge to slightly less than duration/votes as the
804-
// number of votes increases. Vote duration is treated as a hard cap
805-
// and can not be exceeded.
806-
buckets := make([]*voteInterval, votes)
807-
var (
808-
done bool
809-
retries int
810-
)
811-
maxRetries := 100
812-
for retries = 0; !done && retries < maxRetries; retries++ {
813-
done = true
814-
var total time.Duration
815-
for i := 0; i < len(buckets); i++ {
816-
seconds, err := util.RandomUint64()
817-
if err != nil {
818-
return err
819-
}
820-
seconds %= maxDelay
821-
if i == 0 {
822-
// We always immediately vote the first time
823-
// around. This should help catch setup errors.
824-
seconds = 0
825-
}
826-
827-
// Assemble missing vote bits
828-
h, err := chainhash.NewHash(ctres.TicketAddresses[i].Ticket)
829-
if err != nil {
830-
return err
831-
}
832-
signature := hex.EncodeToString(smr.Replies[i].Signature)
833-
834-
t := time.Duration(seconds) * time.Second
835-
total += t
836-
buckets[i] = &voteInterval{
837-
Vote: v1.CastVote{
838-
Token: token,
839-
Ticket: h.String(),
840-
VoteBit: voteBit,
841-
Signature: signature,
842-
},
843-
Votes: 1,
844-
Total: total,
845-
At: t,
846-
}
847-
848-
// Make sure we are not going over our allotted time.
849-
if total > duration {
850-
done = false
851-
break
852-
}
853-
}
854-
}
855-
if retries >= maxRetries {
856-
// This should not happen
857-
return fmt.Errorf("Could not randomize vote delays")
858-
}
859-
860-
// Sanity
861-
if len(buckets) != len(ctres.TicketAddresses) {
862-
return fmt.Errorf("unexpected time bucket count got "+
863-
"%v, wanted %v", len(ctres.TicketAddresses),
864-
len(buckets))
865-
}
866-
867-
// Convert buckets to a list
868-
for _, v := range buckets {
869-
c.voteIntervalPush(v)
870-
}
871-
872-
// Log work
873-
err := c.jsonLog(workJournal, token, buckets)
874-
if err != nil {
875-
return err
876-
}
877-
878-
return nil
879-
}
880-
881782
// _voteTrickler trickles votes to the server. The idea here is to not issue
882783
// large number of votes in one go to the server at the same time giving away
883784
// which IP address owns what votes.
@@ -1138,14 +1039,14 @@ func (c *ctx) _vote(token, voteId string) error {
11381039
// Calculate vote duration if not set
11391040
if c.cfg.voteDuration.Seconds() == 0 {
11401041
blocksLeft := vs.EndHeight - bestBlock
1141-
if blocksLeft < c.cfg.blocksPerDay {
1142-
return fmt.Errorf("less than a day left to " +
1143-
"vote, please set --voteduration " +
1042+
if blocksLeft < c.cfg.blocksPerHour {
1043+
return fmt.Errorf("less than one hour left to" +
1044+
" vote, please set --voteduration " +
11441045
"manually")
11451046
}
11461047
c.cfg.voteDuration = activeNetParams.TargetTimePerBlock *
11471048
(time.Duration(blocksLeft) -
1148-
time.Duration(c.cfg.blocksPerDay))
1049+
time.Duration(c.cfg.blocksPerHour))
11491050
}
11501051

11511052
// Generate work
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package main
2+
3+
import (
4+
"encoding/hex"
5+
"fmt"
6+
"sort"
7+
"time"
8+
9+
"crypto/rand"
10+
11+
pb "decred.org/dcrwallet/rpc/walletrpc"
12+
"github.com/decred/dcrd/chaincfg/chainhash"
13+
v1 "github.com/decred/politeia/politeiawww/api/www/v1"
14+
"github.com/decred/politeia/politeiawww/cmd/politeiavoter/uniformprng"
15+
)
16+
17+
func (c *ctx) calculateTrickle(token, voteBit string, ctres *pb.CommittedTicketsResponse, smr *pb.SignMessagesResponse) error {
18+
votes := len(ctres.TicketAddresses)
19+
duration := c.cfg.voteDuration
20+
voteDuration := duration - time.Hour
21+
if voteDuration < time.Hour {
22+
return fmt.Errorf("not enough time left to trickle votes")
23+
}
24+
fmt.Printf("Total number of votes: %v\n", votes)
25+
fmt.Printf("Total vote duration : %v\n", duration)
26+
fmt.Printf("Duration calculated : %v\n", voteDuration)
27+
28+
prng, err := uniformprng.RandSource(rand.Reader)
29+
if err != nil {
30+
return err
31+
}
32+
33+
ts := make([]time.Duration, 0, votes)
34+
for i := 0; i < votes; i++ {
35+
ts = append(ts, time.Duration(prng.Int63n(int64(voteDuration))))
36+
}
37+
sort.Slice(ts, func(i, j int) bool { return ts[i] < ts[j] })
38+
var previous, t time.Duration
39+
40+
buckets := make([]*voteInterval, votes)
41+
for k := range ts {
42+
// Assemble missing vote bits
43+
h, err := chainhash.NewHash(ctres.TicketAddresses[k].Ticket)
44+
if err != nil {
45+
return err
46+
}
47+
signature := hex.EncodeToString(smr.Replies[k].Signature)
48+
49+
buckets[k] = &voteInterval{
50+
Vote: v1.CastVote{
51+
Token: token,
52+
Ticket: h.String(),
53+
VoteBit: voteBit,
54+
Signature: signature,
55+
},
56+
At: ts[k] - previous, // Delta to previous timestamp
57+
}
58+
t += ts[k] - previous
59+
previous = ts[k]
60+
}
61+
62+
// Should not happen
63+
if t > voteDuration {
64+
return fmt.Errorf("assert t > voteDuration - %v > %v",
65+
t, voteDuration)
66+
}
67+
68+
// Sanity
69+
if len(buckets) != len(ctres.TicketAddresses) {
70+
return fmt.Errorf("unexpected time bucket count got "+
71+
"%v, wanted %v", len(ctres.TicketAddresses),
72+
len(buckets))
73+
}
74+
75+
// Convert buckets to a list
76+
for _, v := range buckets {
77+
c.voteIntervalPush(v)
78+
}
79+
80+
// Log work
81+
err = c.jsonLog(workJournal, token, buckets)
82+
if err != nil {
83+
return err
84+
}
85+
86+
return nil
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package main
2+
3+
import (
4+
"container/list"
5+
"testing"
6+
"time"
7+
8+
pb "decred.org/dcrwallet/rpc/walletrpc"
9+
)
10+
11+
func fakeTickets(x int) (*pb.CommittedTicketsResponse, *pb.SignMessagesResponse) {
12+
ctres := pb.CommittedTicketsResponse{
13+
TicketAddresses: make([]*pb.CommittedTicketsResponse_TicketAddress, x),
14+
}
15+
for k := range ctres.TicketAddresses {
16+
ctres.TicketAddresses[k] = &pb.CommittedTicketsResponse_TicketAddress{
17+
Ticket: make([]byte, 32),
18+
}
19+
}
20+
smr := pb.SignMessagesResponse{
21+
Replies: make([]*pb.SignMessagesResponse_SignReply, x),
22+
}
23+
for k := range smr.Replies {
24+
smr.Replies[k] = &pb.SignMessagesResponse_SignReply{
25+
Signature: make([]byte, 64),
26+
}
27+
}
28+
29+
return &ctres, &smr
30+
}
31+
32+
func fakeCtx(d time.Duration, x int) *ctx {
33+
return &ctx{
34+
cfg: &config{
35+
voteDuration: d,
36+
},
37+
voteIntervalQ: new(list.List),
38+
}
39+
}
40+
41+
func TestTrickleNotEnoughTime(t *testing.T) {
42+
x := 10
43+
c := fakeCtx(time.Hour, x)
44+
ctres, smr := fakeTickets(x)
45+
err := c.calculateTrickle("", "", ctres, smr)
46+
if err == nil {
47+
t.Fatal("expected error")
48+
}
49+
}
50+
51+
func TestTrickle2(t *testing.T) {
52+
x := 10
53+
c := fakeCtx(24*time.Hour, x)
54+
ctres, smr := fakeTickets(x)
55+
err := c.calculateTrickle("", "", ctres, smr)
56+
if err != nil {
57+
t.Fatal(err)
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Package uniformprng implements a uniform, cryptographically secure
2+
// pseudo-random number generator.
3+
package uniformprng
4+
5+
import (
6+
"encoding/binary"
7+
"io"
8+
"math/bits"
9+
10+
"golang.org/x/crypto/chacha20"
11+
)
12+
13+
// Source returns cryptographically-secure pseudorandom numbers with uniform
14+
// distribution.
15+
type Source struct {
16+
buf [8]byte
17+
cipher *chacha20.Cipher
18+
}
19+
20+
var nonce = make([]byte, chacha20.NonceSize)
21+
22+
// NewSource seeds a Source from a 32-byte key.
23+
func NewSource(seed *[32]byte) *Source {
24+
cipher, _ := chacha20.NewUnauthenticatedCipher(seed[:], nonce)
25+
return &Source{cipher: cipher}
26+
}
27+
28+
// RandSource creates a Source with seed randomness read from rand.
29+
func RandSource(rand io.Reader) (*Source, error) {
30+
seed := new([32]byte)
31+
_, err := io.ReadFull(rand, seed[:])
32+
if err != nil {
33+
return nil, err
34+
}
35+
return NewSource(seed), nil
36+
}
37+
38+
// Uint32 returns a pseudo-random uint32.
39+
func (s *Source) Uint32() uint32 {
40+
b := s.buf[:4]
41+
for i := range b {
42+
b[i] = 0
43+
}
44+
s.cipher.XORKeyStream(b, b)
45+
return binary.LittleEndian.Uint32(b)
46+
}
47+
48+
// Uint32n returns a pseudo-random uint32 in range [0,n) without modulo bias.
49+
func (s *Source) Uint32n(n uint32) uint32 {
50+
if n < 2 {
51+
return 0
52+
}
53+
n--
54+
mask := ^uint32(0) >> bits.LeadingZeros32(n)
55+
for {
56+
u := s.Uint32() & mask
57+
if u <= n {
58+
return u
59+
}
60+
}
61+
}
62+
63+
// Int63 returns a pseudo-random 63-bit positive integer as an int64 without
64+
// modulo bias.
65+
func (s *Source) Int63() int64 {
66+
b := s.buf[:]
67+
for i := range b {
68+
b[i] = 0
69+
}
70+
s.cipher.XORKeyStream(b, b)
71+
return int64(binary.LittleEndian.Uint64(b) &^ (1 << 63))
72+
}
73+
74+
// Int63n returns, as an int64, a pseudo-random 63-bit positive integer in [0,n)
75+
// without modulo bias.
76+
// It panics if n <= 0.
77+
func (s *Source) Int63n(n int64) int64 {
78+
if n <= 0 {
79+
panic("invalid argument to Int63n")
80+
}
81+
n--
82+
mask := int64(^uint64(0) >> bits.LeadingZeros64(uint64(n)))
83+
for {
84+
i := s.Int63() & mask
85+
if i <= n {
86+
return i
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)