Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mixing: Introduce module. #3207

Merged
merged 1 commit into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
178 changes: 178 additions & 0 deletions mixing/dcnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (c) 2019-2024 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package mixing
jrick marked this conversation as resolved.
Show resolved Hide resolved

import (
"encoding/binary"
"math/big"

"github.com/decred/dcrd/crypto/blake256"
"github.com/decred/dcrd/wire"
)

// SRMixPads creates a vector of exponential DC-net pads from a vector of
// shared secrets with each participating peer in the DC-net.
func SRMixPads(kp [][]byte, my uint32) []*big.Int {
davecgh marked this conversation as resolved.
Show resolved Hide resolved
h := blake256.New()
scratch := make([]byte, 8)

pads := make([]*big.Int, len(kp))
partialPad := new(big.Int)
for j := uint32(0); j < uint32(len(kp)); j++ {
pads[j] = new(big.Int)
davecgh marked this conversation as resolved.
Show resolved Hide resolved
binary.LittleEndian.PutUint64(scratch, uint64(j)+1)
for i := uint32(0); i < uint32(len(kp)); i++ {
if my == i {
continue
}
h.Reset()
h.Write(kp[i])
h.Write(scratch)
digest := h.Sum(nil)
partialPad.SetBytes(digest)
if my > i {
pads[j].Add(pads[j], partialPad)
} else {
pads[j].Sub(pads[j], partialPad)
}
}
pads[j].Mod(pads[j], F)
}
return pads
}

// SRMix creates the padded {m**1, m**2, ..., m**n} message exponentials
// vector. Message must be bounded by the field prime and must be unique to
// every exponential SR run in a mix session to ensure anonymity.
func SRMix(m *big.Int, pads []*big.Int) []*big.Int {
mix := make([]*big.Int, len(pads))
exp := new(big.Int)
for i := int64(0); i < int64(len(mix)); i++ {
mexp := new(big.Int).Exp(m, exp.SetInt64(i+1), nil)
mix[i] = mexp.Add(mexp, pads[i])
mix[i].Mod(mix[i], F)
}
return mix
}

// IntVectorsFromBytes creates a 2-dimensional *big.Int slice from their absolute
// values as bytes.
func IntVectorsFromBytes(vs [][][]byte) [][]*big.Int {
ints := make([][]*big.Int, len(vs))
for i := range vs {
ints[i] = make([]*big.Int, len(vs[i]))
for j := range vs[i] {
ints[i][j] = new(big.Int).SetBytes(vs[i][j])
}
}
return ints
}

// IntVectorsToBytes creates a 2-dimensional slice of big.Int absolute values as
// bytes.
func IntVectorsToBytes(ints [][]*big.Int) [][][]byte {
bytes := make([][][]byte, len(ints))
for i := range ints {
bytes[i] = make([][]byte, len(ints[i]))
for j := range ints[i] {
bytes[i][j] = ints[i][j].Bytes()
}
}
return bytes
}

// AddVectors sums each vector element over F, returning a new vector. When
// peers are honest (DC-mix pads sum to zero) this creates the unpadded vector
// of message power sums.
func AddVectors(vs ...[]*big.Int) []*big.Int {
sums := make([]*big.Int, len(vs))
for i := range sums {
sums[i] = new(big.Int)
for j := range vs {
sums[i].Add(sums[i], vs[j][i])
}
sums[i].Mod(sums[i], F)
}
return sums
}

// Coefficients calculates a{0}..a{n} for the polynomial:
//
// g(x) = a{0} + a{1}x + a{2}x**2 + ... + a{n-1}x**(n-1) + a{n}x**n (mod F)
//
// where
//
// a{n} = -1
// a{n-1} = -(1/1) * a{n}*S{0}
// a{n-2} = -(1/2) * (a{n-1}*S{0} + a{n}*S{1})
// a{n-3} = -(1/3) * (a{n-2}*S{0} + a{n-1}*S{1} + a{n}*S{2})
// ...
//
// The roots of this polynomial are the set of recovered messages.
//
// Note that the returned slice of coefficients is one element larger than the
// slice of partial sums.
func Coefficients(S []*big.Int) []*big.Int {
n := len(S) + 1
a := make([]*big.Int, n)
a[len(a)-1] = big.NewInt(-1)
a[len(a)-1].Add(a[len(a)-1], F) // a{n} = -1 (mod F) = F - 1
scratch := new(big.Int)
for i := 0; i < len(a)-1; i++ {
a[n-2-i] = new(big.Int)
for j := 0; j <= i; j++ {
a[n-2-i].Add(a[n-2-i], scratch.Mul(a[n-1-i+j], S[j]))
}
xinv := scratch.ModInverse(scratch.SetInt64(int64(i)+1), F)
xinv.Neg(xinv)
a[n-2-i].Mul(a[n-2-i], xinv)
a[n-2-i].Mod(a[n-2-i], F)
}
return a
}

// IsRoot checks that the message m is a root of the polynomial with
// coefficients a (mod F) without solving for every root.
func IsRoot(m *big.Int, a []*big.Int) bool {
sum := new(big.Int)
scratch := new(big.Int)
for i := range a {
scratch.Exp(m, scratch.SetInt64(int64(i)), F)
scratch.Mul(scratch, a[i])
sum.Add(sum, scratch)
}
sum.Mod(sum, F)
return sum.Sign() == 0
}

// DCMixPads creates the vector of DC-net pads from shared secrets with each mix
// participant.
func DCMixPads(kp []wire.MixVect, my uint32) Vec {
pads := make(Vec, len(kp))
for i := range kp {
if uint32(i) == my {
continue
}
pads.Xor(pads, Vec(kp[i]))
}
return pads
}

// DCMix creates the DC-net vector of message m xor'd into m's reserved
// anonymous slot position of the pads DC-net pads. Panics if len(m) is not the
// vector's message size.
func DCMix(pads Vec, m []byte, slot uint32) Vec {
if len(m) != Msize {
panic("m is not len Msize")
}

dcmix := make(Vec, len(pads))
copy(dcmix, pads)
slotm := dcmix[slot][:]
Copy link
Member

@davecgh davecgh May 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is unfortunately rife with panic footguns like this (e.g. all of the panics when slice lengths don't match Msize and the like). Since this is a development module, we can allow it for now, but I'd really like to see all of these methods that can fail with trivially incorrect input updated to return proper errors instead of instead of being so brittle with panics everywhere. The panics with incorrectly sized slices could all be avoided by just taking concrete arrays of the proper size.

This is the type of code that exported dcrd code everywhere else tries to avoid because small things that should just be normal errors like off by one bugs can bring down an entire process.

for i := range m {
slotm[i] ^= m[i]
}
return dcmix
}
28 changes: 28 additions & 0 deletions mixing/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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 mixing

import (
"errors"
"fmt"
)

var (
errInvalidPROrder = errors.New("invalid pair request order")

errInvalidSessionID = errors.New("invalid session ID")
)

// DecapsulateError identifies the unmixed peer position of a peer who
// submitted an undecryptable ciphertext.
type DecapsulateError struct {
jrick marked this conversation as resolved.
Show resolved Hide resolved
SubmittingIndex uint32
}

// Error satisifies the error interface.
func (e *DecapsulateError) Error() string {
jrick marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Sprintf("decapsulate failure of ciphertext by peer %d",
e.SubmittingIndex)
}
18 changes: 18 additions & 0 deletions mixing/expiry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) 2023-2024 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package mixing

import (
"time"

"github.com/decred/dcrd/chaincfg/v3"
)

// MaxExpiry returns the maximum allowed expiry for a new pair request message
// created with a blockchain tip at tipHeight.
func MaxExpiry(tipHeight uint32, params *chaincfg.Params) uint32 {
target := params.TargetTimePerBlock
return tipHeight + uint32(60*time.Minute/target) + 1
}
17 changes: 17 additions & 0 deletions mixing/field.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package mixing

import (
"math/big"
)

// FieldPrime is the field prime 2**127 - 1.
var F *big.Int

func init() {
F, _ = new(big.Int).SetString("7fffffffffffffffffffffffffffffff", 16)
}

// InField returns whether x is bounded by the field F.
func InField(x *big.Int) bool {
return x.Sign() != -1 && x.Cmp(F) == -1
}
8 changes: 8 additions & 0 deletions mixing/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package mixing

const (
// PRFlagCanSolveRoots describes a bit in the pair request flags field
// indicating support for solving and publishing factored slot
// reservation polynomials.
PRFlagCanSolveRoots byte = 1 << iota
)
21 changes: 21 additions & 0 deletions mixing/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module github.com/decred/dcrd/mixing
davecgh marked this conversation as resolved.
Show resolved Hide resolved

go 1.17

require (
github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a
github.com/decred/dcrd/chaincfg/chainhash v1.0.4
github.com/decred/dcrd/chaincfg/v3 v3.2.0
github.com/decred/dcrd/crypto/blake256 v1.0.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
github.com/decred/dcrd/wire v1.6.0
golang.org/x/crypto v0.7.0
)

require (
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
golang.org/x/sys v0.6.0 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)

replace github.com/decred/dcrd/wire => ../wire
55 changes: 55 additions & 0 deletions mixing/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a h1:clYxJ3Os0EQUKDDVU8M0oipllX0EkuFNBfhVQuIfyF0=
github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a/go.mod h1:z/9Ck1EDixEbBbZ2KH2qNHekEmDLTOZ+FyoIPWWSVOI=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/chaincfg/chainhash v1.0.4 h1:zRCv6tdncLfLTKYqu7hrXvs7hW+8FO/NvwoFvGsrluU=
github.com/decred/dcrd/chaincfg/chainhash v1.0.4/go.mod h1:hA86XxlBWwHivMvxzXTSD0ZCG/LoYsFdWnCekkTMCqY=
github.com/decred/dcrd/chaincfg/v3 v3.2.0 h1:6WxA92AGBkycEuWvxtZMvA76FbzbkDRoK8OGbsR2muk=
github.com/decred/dcrd/chaincfg/v3 v3.2.0/go.mod h1:2rHW1TKyFmwZTVBLoU/Cmf0oxcpBjUEegbSlBfrsriI=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/wire v1.6.0 h1:YOGwPHk4nzGr6OIwUGb8crJYWDiVLpuMxfDBCCF7s/o=
github.com/decred/dcrd/wire v1.6.0/go.mod h1:XQ8Xv/pN/3xaDcb7sH8FBLS9cdgVctT7HpBKKGsIACk=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
53 changes: 53 additions & 0 deletions mixing/internal/chacha20prng/prng.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package chacha20prng

import (
"encoding/binary"
"strconv"

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

// SeedSize is the required length of seeds for New.
const SeedSize = 32

// Reader is a ChaCha20 PRNG for a DC-net run. It implements io.Reader.
type Reader struct {
cipher *chacha20.Cipher
}

// New creates a ChaCha20 PRNG seeded by a 32-byte key and a run iteration. The
// returned reader is not safe for concurrent access. This will panic if the
// length of seed is not SeedSize bytes.
func New(seed []byte, run uint32) *Reader {
if l := len(seed); l != SeedSize {
panic("chacha20prng: bad seed length " + strconv.Itoa(l))
}

nonce := make([]byte, chacha20.NonceSize)
binary.LittleEndian.PutUint32(nonce[:4], run)

cipher, _ := chacha20.NewUnauthenticatedCipher(seed, nonce)
return &Reader{cipher: cipher}
}

// Read implements io.Reader.
func (r *Reader) Read(b []byte) (int, error) {
// Zero the source such that the destination is written with just the
// keystream. Destination and source are allowed to overlap (exactly).
for i := range b {
b[i] = 0
}
r.cipher.XORKeyStream(b, b)
return len(b), nil
}

// Next returns the next n bytes from the reader.
func (r *Reader) Next(n int) []byte {
b := make([]byte, n)
r.cipher.XORKeyStream(b, b)
return b
}