Skip to content

Commit

Permalink
mixing: Introduce module.
Browse files Browse the repository at this point in the history
This introduces a new package and module containing code for supporting
peer-to-peer transaction mixing derived from CoinShuffle++.  The mixing
package contains the function and type primitives that will be used by mixing
clients to perform the slot reservation and XOR DC-nets.
  • Loading branch information
jrick authored May 13, 2024
1 parent fc673ed commit f303576
Show file tree
Hide file tree
Showing 15 changed files with 1,077 additions and 0 deletions.
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

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 {
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)
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][:]
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 {
SubmittingIndex uint32
}

// Error satisifies the error interface.
func (e *DecapsulateError) Error() string {
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

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
}
Loading

0 comments on commit f303576

Please sign in to comment.