Skip to content

Commit

Permalink
Make trickler truly random instead of using random bucketized duratio…
Browse files Browse the repository at this point in the history
…ns (#1368)

* Make trickler truly random instead of using random bucketized durations

* Remove comments now that the code has been validated
  • Loading branch information
marcopeereboom authored Mar 4, 2021
1 parent 9019d9d commit 51a60c4
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 111 deletions.
11 changes: 5 additions & 6 deletions politeiawww/cmd/politeiavoter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ type config struct {
ClientCert string `long:"clientcert" description:"Path to TLS certificate for client authentication (default: client.pem)"`
ClientKey string `long:"clientkey" description:"Path to TLS client authentication key (default: client-key.pem)"`

voteDir string
dial func(string, string) (net.Conn, error)
voteDuration time.Duration // Parsed VoteDuration
blocksPerDay uint64
voteDir string
dial func(string, string) (net.Conn, error)
voteDuration time.Duration // Parsed VoteDuration
blocksPerHour uint64
}

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

// Calculate blocks per day
cfg.blocksPerDay = uint64(24 * time.Hour /
activeNetParams.TargetTimePerBlock)
cfg.blocksPerHour = uint64(time.Hour / activeNetParams.TargetTimePerBlock)

// Determine default connections
if cfg.PoliteiaWWW == "" {
Expand Down
111 changes: 6 additions & 105 deletions politeiawww/cmd/politeiavoter/politeiavoter.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,8 @@ type ctx struct {
// timing intervals and vote details. This is a JSON structure for logging
// purposes.
type voteInterval struct {
Vote v1.CastVote `json:"vote"` // RPC vote
Votes int `json:"votes"` // Always 1 for now
Total time.Duration `json:"total"` // Cumulative time
At time.Duration `json:"at"` // Delay to fire off vote
Vote v1.CastVote `json:"vote"` // RPC vote
At time.Duration `json:"at"` // Delay to fire off vote
}

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

// calculateTrickle calculates the trickle schedule.
func (c *ctx) calculateTrickle(token, voteBit string, ctres *pb.CommittedTicketsResponse, smr *pb.SignMessagesResponse) error {
votes := uint64(len(ctres.TicketAddresses))
duration := c.cfg.voteDuration
maxDelay := uint64(duration.Seconds() / float64(votes) * 2)
minAvgInterval := uint64(35)
fmt.Printf("Votes : %v\n", votes)
fmt.Printf("Duration : %v\n", duration)
fmt.Printf("Avg Interval: %v\n", time.Duration(maxDelay/2)*time.Second)

// Ensure that the duration allows for sufficiently randomized delays
// in between votes
if duration.Seconds() < float64(minAvgInterval)*float64(votes) {
return fmt.Errorf("Vote duration must be at least %v",
time.Duration(float64(minAvgInterval)*float64(votes))*time.Second)
}

// Create array of work to be done. Vote delays are random durations
// between [0, maxDelay] (exclusive) which means that the vote delay
// average will converge to slightly less than duration/votes as the
// number of votes increases. Vote duration is treated as a hard cap
// and can not be exceeded.
buckets := make([]*voteInterval, votes)
var (
done bool
retries int
)
maxRetries := 100
for retries = 0; !done && retries < maxRetries; retries++ {
done = true
var total time.Duration
for i := 0; i < len(buckets); i++ {
seconds, err := util.RandomUint64()
if err != nil {
return err
}
seconds %= maxDelay
if i == 0 {
// We always immediately vote the first time
// around. This should help catch setup errors.
seconds = 0
}

// Assemble missing vote bits
h, err := chainhash.NewHash(ctres.TicketAddresses[i].Ticket)
if err != nil {
return err
}
signature := hex.EncodeToString(smr.Replies[i].Signature)

t := time.Duration(seconds) * time.Second
total += t
buckets[i] = &voteInterval{
Vote: v1.CastVote{
Token: token,
Ticket: h.String(),
VoteBit: voteBit,
Signature: signature,
},
Votes: 1,
Total: total,
At: t,
}

// Make sure we are not going over our allotted time.
if total > duration {
done = false
break
}
}
}
if retries >= maxRetries {
// This should not happen
return fmt.Errorf("Could not randomize vote delays")
}

// Sanity
if len(buckets) != len(ctres.TicketAddresses) {
return fmt.Errorf("unexpected time bucket count got "+
"%v, wanted %v", len(ctres.TicketAddresses),
len(buckets))
}

// Convert buckets to a list
for _, v := range buckets {
c.voteIntervalPush(v)
}

// Log work
err := c.jsonLog(workJournal, token, buckets)
if err != nil {
return err
}

return nil
}

// _voteTrickler trickles votes to the server. The idea here is to not issue
// large number of votes in one go to the server at the same time giving away
// which IP address owns what votes.
Expand Down Expand Up @@ -1138,14 +1039,14 @@ func (c *ctx) _vote(token, voteId string) error {
// Calculate vote duration if not set
if c.cfg.voteDuration.Seconds() == 0 {
blocksLeft := vs.EndHeight - bestBlock
if blocksLeft < c.cfg.blocksPerDay {
return fmt.Errorf("less than a day left to " +
"vote, please set --voteduration " +
if blocksLeft < c.cfg.blocksPerHour {
return fmt.Errorf("less than one hour left to" +
" vote, please set --voteduration " +
"manually")
}
c.cfg.voteDuration = activeNetParams.TargetTimePerBlock *
(time.Duration(blocksLeft) -
time.Duration(c.cfg.blocksPerDay))
time.Duration(c.cfg.blocksPerHour))
}

// Generate work
Expand Down
87 changes: 87 additions & 0 deletions politeiawww/cmd/politeiavoter/trickle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

import (
"encoding/hex"
"fmt"
"sort"
"time"

"crypto/rand"

pb "decred.org/dcrwallet/rpc/walletrpc"
"github.com/decred/dcrd/chaincfg/chainhash"
v1 "github.com/decred/politeia/politeiawww/api/www/v1"
"github.com/decred/politeia/politeiawww/cmd/politeiavoter/uniformprng"
)

func (c *ctx) calculateTrickle(token, voteBit string, ctres *pb.CommittedTicketsResponse, smr *pb.SignMessagesResponse) error {
votes := len(ctres.TicketAddresses)
duration := c.cfg.voteDuration
voteDuration := duration - time.Hour
if voteDuration < time.Hour {
return fmt.Errorf("not enough time left to trickle votes")
}
fmt.Printf("Total number of votes: %v\n", votes)
fmt.Printf("Total vote duration : %v\n", duration)
fmt.Printf("Duration calculated : %v\n", voteDuration)

prng, err := uniformprng.RandSource(rand.Reader)
if err != nil {
return err
}

ts := make([]time.Duration, 0, votes)
for i := 0; i < votes; i++ {
ts = append(ts, time.Duration(prng.Int63n(int64(voteDuration))))
}
sort.Slice(ts, func(i, j int) bool { return ts[i] < ts[j] })
var previous, t time.Duration

buckets := make([]*voteInterval, votes)
for k := range ts {
// Assemble missing vote bits
h, err := chainhash.NewHash(ctres.TicketAddresses[k].Ticket)
if err != nil {
return err
}
signature := hex.EncodeToString(smr.Replies[k].Signature)

buckets[k] = &voteInterval{
Vote: v1.CastVote{
Token: token,
Ticket: h.String(),
VoteBit: voteBit,
Signature: signature,
},
At: ts[k] - previous, // Delta to previous timestamp
}
t += ts[k] - previous
previous = ts[k]
}

// Should not happen
if t > voteDuration {
return fmt.Errorf("assert t > voteDuration - %v > %v",
t, voteDuration)
}

// Sanity
if len(buckets) != len(ctres.TicketAddresses) {
return fmt.Errorf("unexpected time bucket count got "+
"%v, wanted %v", len(ctres.TicketAddresses),
len(buckets))
}

// Convert buckets to a list
for _, v := range buckets {
c.voteIntervalPush(v)
}

// Log work
err = c.jsonLog(workJournal, token, buckets)
if err != nil {
return err
}

return nil
}
59 changes: 59 additions & 0 deletions politeiawww/cmd/politeiavoter/trickle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package main

import (
"container/list"
"testing"
"time"

pb "decred.org/dcrwallet/rpc/walletrpc"
)

func fakeTickets(x int) (*pb.CommittedTicketsResponse, *pb.SignMessagesResponse) {
ctres := pb.CommittedTicketsResponse{
TicketAddresses: make([]*pb.CommittedTicketsResponse_TicketAddress, x),
}
for k := range ctres.TicketAddresses {
ctres.TicketAddresses[k] = &pb.CommittedTicketsResponse_TicketAddress{
Ticket: make([]byte, 32),
}
}
smr := pb.SignMessagesResponse{
Replies: make([]*pb.SignMessagesResponse_SignReply, x),
}
for k := range smr.Replies {
smr.Replies[k] = &pb.SignMessagesResponse_SignReply{
Signature: make([]byte, 64),
}
}

return &ctres, &smr
}

func fakeCtx(d time.Duration, x int) *ctx {
return &ctx{
cfg: &config{
voteDuration: d,
},
voteIntervalQ: new(list.List),
}
}

func TestTrickleNotEnoughTime(t *testing.T) {
x := 10
c := fakeCtx(time.Hour, x)
ctres, smr := fakeTickets(x)
err := c.calculateTrickle("", "", ctres, smr)
if err == nil {
t.Fatal("expected error")
}
}

func TestTrickle2(t *testing.T) {
x := 10
c := fakeCtx(24*time.Hour, x)
ctres, smr := fakeTickets(x)
err := c.calculateTrickle("", "", ctres, smr)
if err != nil {
t.Fatal(err)
}
}
89 changes: 89 additions & 0 deletions politeiawww/cmd/politeiavoter/uniformprng/prng.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Package uniformprng implements a uniform, cryptographically secure
// pseudo-random number generator.
package uniformprng

import (
"encoding/binary"
"io"
"math/bits"

"golang.org/x/crypto/chacha20"
)

// Source returns cryptographically-secure pseudorandom numbers with uniform
// distribution.
type Source struct {
buf [8]byte
cipher *chacha20.Cipher
}

var nonce = make([]byte, chacha20.NonceSize)

// NewSource seeds a Source from a 32-byte key.
func NewSource(seed *[32]byte) *Source {
cipher, _ := chacha20.NewUnauthenticatedCipher(seed[:], nonce)
return &Source{cipher: cipher}
}

// RandSource creates a Source with seed randomness read from rand.
func RandSource(rand io.Reader) (*Source, error) {
seed := new([32]byte)
_, err := io.ReadFull(rand, seed[:])
if err != nil {
return nil, err
}
return NewSource(seed), nil
}

// Uint32 returns a pseudo-random uint32.
func (s *Source) Uint32() uint32 {
b := s.buf[:4]
for i := range b {
b[i] = 0
}
s.cipher.XORKeyStream(b, b)
return binary.LittleEndian.Uint32(b)
}

// Uint32n returns a pseudo-random uint32 in range [0,n) without modulo bias.
func (s *Source) Uint32n(n uint32) uint32 {
if n < 2 {
return 0
}
n--
mask := ^uint32(0) >> bits.LeadingZeros32(n)
for {
u := s.Uint32() & mask
if u <= n {
return u
}
}
}

// Int63 returns a pseudo-random 63-bit positive integer as an int64 without
// modulo bias.
func (s *Source) Int63() int64 {
b := s.buf[:]
for i := range b {
b[i] = 0
}
s.cipher.XORKeyStream(b, b)
return int64(binary.LittleEndian.Uint64(b) &^ (1 << 63))
}

// Int63n returns, as an int64, a pseudo-random 63-bit positive integer in [0,n)
// without modulo bias.
// It panics if n <= 0.
func (s *Source) Int63n(n int64) int64 {
if n <= 0 {
panic("invalid argument to Int63n")
}
n--
mask := int64(^uint64(0) >> bits.LeadingZeros64(uint64(n)))
for {
i := s.Int63() & mask
if i <= n {
return i
}
}
}

0 comments on commit 51a60c4

Please sign in to comment.