Skip to content

Commit

Permalink
jitter
Browse files Browse the repository at this point in the history
  • Loading branch information
jrick committed Jun 13, 2024
1 parent 4a5ac92 commit d17a30f
Show file tree
Hide file tree
Showing 6 changed files with 564 additions and 34 deletions.
198 changes: 198 additions & 0 deletions mixing/internal/uniform/rand.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright (c) 2024 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
//
// Uniform random algorithms modified from the Go math/rand/v2 package with
// the following license:
//
// Copyright (c) 2009 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

// Package uniform provides uniformly distributed, cryptographically secure
// random numbers with randomness obtained from a CSPRNG.
//
// Random sources are required to never error; any errors reading the random
// source will result in a panic.
package uniform

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

"github.com/decred/dcrd/mixing/internal/uprng"
)

func readRand(buf []byte) {
uprng.Read(buf)
}

// Uint32 returns a uniform random uint32.
func Uint32() uint32 {
b := make([]byte, 4)
readRand(b)
return binary.LittleEndian.Uint32(b)
}

// Uint64 returns a uniform random uint64.
func Uint64() uint64 {
b := make([]byte, 8)
readRand(b)
return binary.LittleEndian.Uint64(b)
}

// Uint32n returns a random uint32 in range [0,n) without modulo bias.
func Uint32n(n uint32) uint32 {
if n&(n-1) == 0 { // n is power of two, can mask
return uint32(Uint64()) & (n - 1)
}
// On 64-bit systems we still use the uint64 code below because
// the probability of a random uint64 lo being < a uint32 n is near zero,
// meaning the unbiasing loop almost never runs.
// On 32-bit systems, here we need to implement that same logic in 32-bit math,
// both to preserve the exact output sequence observed on 64-bit machines
// and to preserve the optimization that the unbiasing loop almost never runs.
//
// We want to compute
// hi, lo := bits.Mul64(r.Uint64(), n)
// In terms of 32-bit halves, this is:
// x1:x0 := r.Uint64()
// 0:hi, lo1:lo0 := bits.Mul64(x1:x0, 0:n)
// Writing out the multiplication in terms of bits.Mul32 allows
// using direct hardware instructions and avoiding
// the computations involving these zeros.
x := Uint64()
lo1a, lo0 := bits.Mul32(uint32(x), n)
hi, lo1b := bits.Mul32(uint32(x>>32), n)
lo1, c := bits.Add32(lo1a, lo1b, 0)
hi += c
if lo1 == 0 && lo0 < n {
n64 := uint64(n)
thresh := uint32(-n64 % n64)
for lo1 == 0 && lo0 < thresh {
x := Uint64()
lo1a, lo0 = bits.Mul32(uint32(x), n)
hi, lo1b = bits.Mul32(uint32(x>>32), n)
lo1, c = bits.Add32(lo1a, lo1b, 0)
hi += c
}
}
return hi
}

const is32bit = ^uint(0)>>32 == 0

// Uint64n returns a random uint32 in range [0,n) without modulo bias.
func Uint64n(n uint64) uint64 {
if is32bit && uint64(uint32(n)) == n {
return uint64(Uint32n(uint32(n)))
}
if n&(n-1) == 0 { // n is power of two, can mask
return Uint64() & (n - 1)
}

// Suppose we have a uint64 x uniform in the range [0,2⁶⁴)
// and want to reduce it to the range [0,n) preserving exact uniformity.
// We can simulate a scaling arbitrary precision x * (n/2⁶⁴) by
// the high bits of a double-width multiply of x*n, meaning (x*n)/2⁶⁴.
// Since there are 2⁶⁴ possible inputs x and only n possible outputs,
// the output is necessarily biased if n does not divide 2⁶⁴.
// In general (x*n)/2⁶⁴ = k for x*n in [k*2⁶⁴,(k+1)*2⁶⁴).
// There are either floor(2⁶⁴/n) or ceil(2⁶⁴/n) possible products
// in that range, depending on k.
// But suppose we reject the sample and try again when
// x*n is in [k*2⁶⁴, k*2⁶⁴+(2⁶⁴%n)), meaning rejecting fewer than n possible
// outcomes out of the 2⁶⁴.
// Now there are exactly floor(2⁶⁴/n) possible ways to produce
// each output value k, so we've restored uniformity.
// To get valid uint64 math, 2⁶⁴ % n = (2⁶⁴ - n) % n = -n % n,
// so the direct implementation of this algorithm would be:
//
// hi, lo := bits.Mul64(r.Uint64(), n)
// thresh := -n % n
// for lo < thresh {
// hi, lo = bits.Mul64(r.Uint64(), n)
// }
//
// That still leaves an expensive 64-bit division that we would rather avoid.
// We know that thresh < n, and n is usually much less than 2⁶⁴, so we can
// avoid the last four lines unless lo < n.
//
// See also:
// https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction
// https://lemire.me/blog/2016/06/30/fast-random-shuffling
hi, lo := bits.Mul64(Uint64(), n)
if lo < n {
thresh := -n % n
for lo < thresh {
hi, lo = bits.Mul64(Uint64(), n)
}
}
return hi
}

// Int32 returns a random 31-bit non-negative integer as an int32 without
// modulo bias.
func Int32() int32 {
return int32(Uint32() & 0x7FFFFFFF)
}

// Int32n returns, as an int32, a random 31-bit non-negative integer in [0,n)
// without modulo bias.
// Panics if n <= 0.
func Int32n(n int32) int32 {
if n <= 0 {
panic("uniform: invalid argument to Int32n")
}
return int32(Uint32n(uint32(n)))
}

// Int64 returns a random 63-bit non-negative integer as an int64 without
// modulo bias.
func Int64() int64 {
return int64(Uint64() & 0x7FFFFFFF_FFFFFFFF)
}

// Int64n returns, as an int64, a random 63-bit non-negative integer in [0,n)
// without modulo bias.
// Panics if n <= 0.
func Int64n(n int64) int64 {
if n <= 0 {
panic("uniform: invalid argument to Int64n")
}
return int64(Uint64n(uint64(n)))
}

// Duration returns a random duration in [0,n) without modulo bias.
// Panics if n <= 0.
func Duration(n time.Duration) time.Duration {
if n <= 0 {
panic("uniform: invalid argument to Duration")
}
return time.Duration(Uint64n(uint64(n)))
}
118 changes: 118 additions & 0 deletions mixing/internal/uprng/uprng.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) 2024 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package uprng

import (
cryptorand "crypto/rand"
"encoding/binary"
"io"
"math/bits"
"sync"
"time"

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

// Reader implements a cryptographically secure userspace PRNG that is
// periodically reseeded with entropy obtained from crypto/rand.
// Reader is safe for concurrent access.
var Reader io.Reader

// Read fills b with random bytes obtained from the userspace PRNG.
//
// When possible, prefer Read over using Reader to avoid escape analysis
// unnecessarily allocating b's backing array on the heap.
func Read(b []byte) {
defaultReader.Read(b)
}

var defaultReader = newPRNG()

func init() {
Reader = defaultReader
}

const (
maxCipherRead = 4 * 1024 * 1024 // 4 MiB
maxCipherDuration = 20 * time.Second
)

// nonce implements a 12-byte little endian counter suitable for use as an
// incrementing ChaCha20 nonce.
type nonce [chacha20.NonceSize]byte

func (n *nonce) inc() {
n0 := binary.LittleEndian.Uint32(n[0:4])
n1 := binary.LittleEndian.Uint32(n[4:8])
n2 := binary.LittleEndian.Uint32(n[8:12])

var carry uint32
n0, carry = bits.Add32(n0, 1, carry)
n1, carry = bits.Add32(n1, 0, carry)
n2, _ = bits.Add32(n2, 0, carry)

binary.LittleEndian.PutUint32(n[0:4], n0)
binary.LittleEndian.PutUint32(n[4:8], n1)
binary.LittleEndian.PutUint32(n[8:12], n2)
}

type prng struct {
cipher *chacha20.Cipher
read int
t time.Time
key []byte
nonce nonce
mu sync.Mutex
}

func newPRNG() *prng {
p := &prng{
key: make([]byte, chacha20.KeySize),
}
p.seed()
return p
}

// seed reseeds the prng with kernel and existing cipher entropy, if the
// cipher has been originally seeded.
// Panics only during initial seeding if a crypto/rand read errors.
func (p *prng) seed() {
_, err := cryptorand.Read(p.key)
if err != nil && p.cipher == nil {
panic(err)
}
if p.cipher != nil {
p.cipher.XORKeyStream(p.key, p.key)
}

// never errors with correct key and nonce sizes
cipher, _ := chacha20.NewUnauthenticatedCipher(p.key, p.nonce[:])
p.nonce.inc()

p.cipher = cipher
p.read = 0
p.t = time.Now().Add(maxCipherDuration)
}

func (p *prng) Read(s []byte) (n int, err error) {
p.mu.Lock()
defer p.mu.Unlock()

if time.Now().After(p.t) {
p.seed()
}

for p.read+len(s) > maxCipherRead {
l := maxCipherRead - p.read
p.cipher.XORKeyStream(s[:l], s[:l])
p.seed()
n += l
s = s[l:]
}
p.cipher.XORKeyStream(s, s)
p.read += len(s)
n += len(s)
return
}
12 changes: 7 additions & 5 deletions mixing/mixclient/blame.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ func (c *Client) blame(ctx context.Context, sesRun *sessionRun) (err error) {
}
}()

err = c.forLocalPeers(ctx, sesRun, func(p *peer) error {
err = c.sendLocalPeerMsgs(ctx, sesRun, func(p *peer) mixing.Message {
// Send initial secrets messages from any peers who detected
// misbehavior.
if !p.triggeredBlame {
return nil
}
return p.signAndSubmit(p.rs)
return p.rs
})
if err != nil {
return err
Expand All @@ -88,21 +88,23 @@ func (c *Client) blame(ctx context.Context, sesRun *sessionRun) (err error) {
}

// Send remaining secrets messages.
err = c.forLocalPeers(ctx, sesRun, func(p *peer) error {
err = c.sendLocalPeerMsgs(ctx, sesRun, func(p *peer) mixing.Message {
if p.triggeredBlame {
p.triggeredBlame = false
return nil
}
p.rs.SeenSecrets = rsHashes
return p.signAndSubmit(p.rs)
return p.rs
})
if err != nil {
return err
}

// Wait for all secrets, or timeout.
rcv.RSs = make([]*wire.MsgMixSecrets, 0, len(sesRun.prs))
_ = mp.Receive(ctx, rcv)
rcvCtx, rcvCtxCancel := context.WithTimeout(ctx, timeoutDuration)
_ = mp.Receive(rcvCtx, rcv)
rcvCtxCancel()
rss := rcv.RSs
for _, rs := range rcv.RSs {
if idx, ok := identityIndices[rs.Identity]; ok {
Expand Down
Loading

0 comments on commit d17a30f

Please sign in to comment.