diff --git a/internal/blockchain/chain.go b/internal/blockchain/chain.go index 1c897e78ef..8a36c8763e 100644 --- a/internal/blockchain/chain.go +++ b/internal/blockchain/chain.go @@ -297,6 +297,11 @@ type BlockChain struct { bulkImportMode bool } +// ChainParams returns the chain parameters. +func (b *BlockChain) ChainParams() *chaincfg.Params { + return b.chainParams +} + const ( // stakeMajorityCacheKeySize is comprised of the stake version and the // hash size. The stake version is a little endian uint32, hence we diff --git a/mixing/dcnet.go b/mixing/dcnet.go new file mode 100644 index 0000000000..9e80377475 --- /dev/null +++ b/mixing/dcnet.go @@ -0,0 +1,179 @@ +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); int(j) < len(kp); j++ { + pads[j] = new(big.Int) + for i := uint32(0); int(i) < len(kp); i++ { + if my == i { + continue + } + binary.LittleEndian.PutUint64(scratch, uint64(j)+1) + 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.MixVec, msize, my uint32) Vec { + n := uint32(len(kp)) + pads := Vec{ + N: n, + Msize: msize, + Data: make([]byte, n*msize), + } + for i := range kp { + if i == int(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 { + dcmix := *pads + dcmix.Data = make([]byte, len(pads.Data)) + copy(dcmix.Data, pads.Data) + slotm := dcmix.M(slot) + if len(m) != len(slotm) { + panic("message sizes are not equal") + } + for i := range m { + slotm[i] ^= m[i] + } + return dcmix +} diff --git a/mixing/field.go b/mixing/field.go new file mode 100644 index 0000000000..b0b6cda167 --- /dev/null +++ b/mixing/field.go @@ -0,0 +1,17 @@ +package mixing + +import ( + "math/big" +) + +// F 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 +} diff --git a/mixing/go.mod b/mixing/go.mod new file mode 100644 index 0000000000..6e53aeddb3 --- /dev/null +++ b/mixing/go.mod @@ -0,0 +1,30 @@ +module github.com/decred/dcrd/mixing + +go 1.17 + +require ( + decred.org/cspp/v2 v2.0.1-0.20230307024253-8a22691aa376 + github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a + github.com/davecgh/go-spew v1.1.1 + 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/dcrutil/v4 v4.0.1 + github.com/decred/dcrd/txscript/v4 v4.1.0 + github.com/decred/dcrd/wire v1.6.0 + github.com/decred/slog v1.2.0 + golang.org/x/crypto v0.7.0 +) + +require ( + github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect + github.com/dchest/siphash v1.2.3 // indirect + github.com/decred/base58 v1.0.5 // indirect + github.com/decred/dcrd/crypto/ripemd160 v1.0.2 // indirect + github.com/decred/dcrd/dcrec v1.0.1 // indirect + github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + golang.org/x/sys v0.6.0 // indirect + lukechampine.com/blake3 v1.2.1 // indirect +) diff --git a/mixing/go.sum b/mixing/go.sum new file mode 100644 index 0000000000..079364897d --- /dev/null +++ b/mixing/go.sum @@ -0,0 +1,81 @@ +decred.org/cspp/v2 v2.0.1-0.20230307024253-8a22691aa376 h1:739v8a7LMXuCTFNodcKYVpfj70CKWvJeE3NKFDn/65I= +decred.org/cspp/v2 v2.0.1-0.20230307024253-8a22691aa376/go.mod h1:+/9jr1RhVshWnc0U/eXxMlxfiu9/f7ia6TTyS0Oh5n0= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/companyzero/sntrup4591761 v0.0.0-20200131011700-2b0d299dbd22/go.mod h1:LoZJNGDWmVPqMEHmeJzj4Weq4Stjc6FKY6FVpY3Hem0= +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/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/decred/base58 v1.0.5 h1:hwcieUM3pfPnE/6p3J100zoRfGkQxBulZHo7GZfOqic= +github.com/decred/base58 v1.0.5/go.mod h1:s/8lukEHFA6bUQQb/v3rjUySJ2hu+RioCzLukAVkrfw= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +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.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +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/crypto/ripemd160 v1.0.2 h1:TvGTmUBHDU75OHro9ojPLK+Yv7gDl2hnUvRocRCjsys= +github.com/decred/dcrd/crypto/ripemd160 v1.0.2/go.mod h1:uGfjDyePSpa75cSQLzNdVmWlbQMBuiJkvXw/MNKRY4M= +github.com/decred/dcrd/dcrec v1.0.1 h1:gDzlndw0zYxM5BlaV17d7ZJV6vhRe9njPBFeg4Db2UY= +github.com/decred/dcrd/dcrec v1.0.1/go.mod h1:CO+EJd8eHFb8WHa84C7ZBkXsNUIywaTHb+UAuI5uo6o= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 h1:l/lhv2aJCUignzls81+wvga0TFlyoZx8QxRMQgXpZik= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3/go.mod h1:AKpV6+wZ2MfPRJnTbQ6NPgWrKzbe9RCIlCF/FKzMtM8= +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/dcrutil/v4 v4.0.1 h1:E+d2TNbpOj0f1L9RqkZkEm1QolFjajvkzxWC5WOPf1s= +github.com/decred/dcrd/dcrutil/v4 v4.0.1/go.mod h1:7EXyHYj8FEqY+WzMuRkF0nh32ueLqhutZDoW4eQ+KRc= +github.com/decred/dcrd/txscript/v4 v4.1.0 h1:uEdcibIOl6BuWj3AqmXZ9xIK/qbo6lHY9aNk29FtkrU= +github.com/decred/dcrd/txscript/v4 v4.1.0/go.mod h1:OVguPtPc4YMkgssxzP8B6XEMf/J3MB6S1JKpxgGQqi0= +github.com/decred/dcrd/wire v1.5.0/go.mod h1:fzAjVqw32LkbAZIt5mnrvBR751GTa3e0rRQdOIhPY3w= +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/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM= +github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jrick/wsrpc/v2 v2.3.4/go.mod h1:XPYs8BnRWl99lCvXRM5SLpZmTPqWpSOPkDIqYTwDPfU= +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= diff --git a/mixing/internal/chacha20prng/prng.go b/mixing/internal/chacha20prng/prng.go new file mode 100644 index 0000000000..cca9a23574 --- /dev/null +++ b/mixing/internal/chacha20prng/prng.go @@ -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 +} diff --git a/mixing/keyagreement.go b/mixing/keyagreement.go new file mode 100644 index 0000000000..5fed6b7901 --- /dev/null +++ b/mixing/keyagreement.go @@ -0,0 +1,235 @@ +// 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 mixing + +import ( + cryptorand "crypto/rand" + "encoding/binary" + "errors" + "io" + + "github.com/companyzero/sntrup4591761" + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/mixing/internal/chacha20prng" + "github.com/decred/dcrd/wire" +) + +// Aliases for sntrup4591761 types +type ( + PQPublicKey = [sntrup4591761.PublicKeySize]byte + PQPrivateKey = [sntrup4591761.PrivateKeySize]byte + PQCiphertext = [sntrup4591761.CiphertextSize]byte + PQSharedKey = [sntrup4591761.SharedKeySize]byte +) + +func generateSecp256k1(rand io.Reader) (*secp256k1.PublicKey, *secp256k1.PrivateKey, error) { + if rand == nil { + rand = cryptorand.Reader + } + + privateKey, err := secp256k1.GeneratePrivateKeyFromRand(rand) + if err != nil { + return nil, nil, err + } + + publicKey := privateKey.PubKey() + + return publicKey, privateKey, nil +} + +// KX contains the client public and private keys to perform shared key exchange +// with other peers. +type KX struct { + ECDHPublicKey *secp256k1.PublicKey + ECDHPrivateKey *secp256k1.PrivateKey + PQPublicKey *PQPublicKey + PQPrivateKey *PQPrivateKey + PQCleartexts []PQSharedKey +} + +// NewKX generates a mixing identity's public and private keys for a interactive +// key exchange, with randomness read from a run's PRNG. +func NewKX(prng io.Reader) (*KX, error) { + ecdhPublic, ecdhPrivate, err := generateSecp256k1(prng) + if err != nil { + return nil, err + } + + pqPublic, pqPrivate, err := sntrup4591761.GenerateKey(prng) + if err != nil { + return nil, err + } + + kx := &KX{ + ECDHPublicKey: ecdhPublic, + ECDHPrivateKey: ecdhPrivate, + PQPublicKey: pqPublic, + PQPrivateKey: pqPrivate, + } + return kx, nil +} + +func (kx *KX) ecdhSharedKey(pub *secp256k1.PublicKey) []byte { + secret := secp256k1.GenerateSharedSecret(kx.ECDHPrivateKey, pub) + hash := blake256.Sum256(secret) + return hash[:] +} + +func (kx *KX) pqSharedKey(ciphertext *PQCiphertext) ([]byte, error) { + pqSharedKey, ok := sntrup4591761.Decapsulate(ciphertext, kx.PQPrivateKey) + if ok != 1 { + return nil, errors.New("sntrup4591761: decapsulate failure") + } + return pqSharedKey[:], nil +} + +// Encapsulate performs encapsulation for sntrup4591761 key exchanges with each +// other peer in the DC-net. It populates the PQCleartexts field of kx and +// return encrypted cyphertexts of these shared keys. +// +// Encapsulation in the DC-net requires randomness from a CSPRNG seeded by a +// committed secret; blame assignment is not possible otherwise. +func (kx *KX) Encapsulate(prng io.Reader, pubkeys []*PQPublicKey, my int) ([]PQCiphertext, error) { + cts := make([][sntrup4591761.CiphertextSize]byte, len(pubkeys)) + kx.PQCleartexts = make([][32]byte, len(pubkeys)) + + for i, pk := range pubkeys { + ciphertext, cleartext, err := sntrup4591761.Encapsulate(prng, pk) + if err != nil { + return nil, err + } + cts[i] = *ciphertext + kx.PQCleartexts[i] = *cleartext + } + + return cts, nil +} + +// RevealedKeys... +type RevealedKeys struct { + ECDHPublicKeys []*secp256k1.PublicKey + Ciphertexts []PQCiphertext + MyIndex uint32 +} + +type SharedSecrets struct { + SRSecrets [][][]byte + DCSecrets [][]wire.MixVec +} + +// SharedKeys creates the pairwise SR and DC shared secret keys for +// mcounts[k.MyIndex] mixes. ecdhPubs, cts, and mcounts must all share the same +// slice length. +func (kx *KX) SharedSecrets(k *RevealedKeys, sid []byte, msize, run uint32, mcounts []uint32) (SharedSecrets, error) { + var s SharedSecrets + + if len(k.ECDHPublicKeys) != len(mcounts) { + err := errors.New("ECDH public key count must match peer count") + return s, err + } + if len(k.Ciphertexts) != len(mcounts) { + err := errors.New("ciphertext count must match peer count") + return s, err + } + + mcount := mcounts[k.MyIndex] + var mtot uint32 + for i := range mcounts { + mtot += mcounts[i] + } + + h := blake256.New() + s.SRSecrets = make([][][]byte, mcount) + s.DCSecrets = make([][]wire.MixVec, mcount) + + for i := uint32(0); i < mcount; i++ { + s.SRSecrets[i] = make([][]byte, mtot) + s.DCSecrets[i] = make([]wire.MixVec, mtot) + var m int + for peer := uint32(0); int(peer) < len(mcounts); peer++ { + if peer == k.MyIndex && mcount == 1 { + m++ + continue + } + + sharedKey := kx.ecdhSharedKey(k.ECDHPublicKeys[peer]) + pqSharedKey, err := kx.pqSharedKey(&k.Ciphertexts[peer]) + if err != nil { + return s, err + } + + // XOR x25519 and both sntrup4591761 keys into a single + // shared key. If sntrup4591761 is discovered to be + // broken in the future, the security only reduces to + // that of x25519. + // If the message belongs to our own peer, only XOR + // the sntrup4591761 key once. The decapsulated and + // cleartext keys are equal in this case, and would + // cancel each other out otherwise. + xor := func(dst, src []byte) { + if len(dst) != len(src) { + panic("dcnet: different lengths in xor") + } + for i := range dst { + dst[i] ^= src[i] + } + } + xor(sharedKey, pqSharedKey[:]) + if peer != k.MyIndex { + xor(sharedKey, kx.PQCleartexts[peer][:]) + } + + // Create the prefix of a PRNG seed preimage. A counter + // will be appended before creating each PRNG, one for + // each message pair. + prngSeedPreimage := make([]byte, len(sid)+len(sharedKey)+4) + l := copy(prngSeedPreimage, sid) + l += copy(prngSeedPreimage[l:], sharedKey) + seedCounterBytes := prngSeedPreimage[l:] + + // Read from the PRNG to create shared keys for each + // message the peer is mixing. + for j := uint32(0); j < mcounts[peer]; j++ { + if k.MyIndex == peer && j == i { + m++ + continue + } + + // Create the PRNG seed using the combined shared key. + // A unique seed is generated for each message pair, + // determined using the message index of the peer with + // the lower peer index. The PRNG nonce is the message + // number of the peer with the higher peer index. + // When creating shared keys with our own peer, the PRNG + // seed counter and nonce must be reversed for the second + // half of our generated keys. + seedCounter := i + nonce := j + if k.MyIndex > peer || (k.MyIndex == peer && j > i) { + seedCounter = j + nonce = i + } + binary.LittleEndian.PutUint32(seedCounterBytes, uint32(seedCounter)) + + h.Reset() + h.Write(prngSeedPreimage) + prngSeed := h.Sum(nil) + prng := chacha20prng.New(prngSeed, uint32(nonce)) + + s.SRSecrets[i][m] = prng.Next(32) + s.DCSecrets[i][m] = wire.MixVec{ + N: mtot, + Msize: msize, + Data: prng.Next(int(mtot * msize)), + } + + m++ + } + } + } + + return s, nil +} diff --git a/mixing/message.go b/mixing/message.go new file mode 100644 index 0000000000..bda76ba40c --- /dev/null +++ b/mixing/message.go @@ -0,0 +1,34 @@ +// 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 mixing + +import ( + "io" + + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/wire" +) + +// Message is a mixing message. In addition to the implementing wire encoding, +// these messages are signed by an ephemeral mixing participant identity, +// declare the previous messages that have been observed by a peer in a mixing +// session, and include expiry information to increase resilience to +// replay and denial-of-service attacks. +// +// All mixing messages satisify this interface, however, the pair request +// message returns nil for some fields that do not apply, as it is the first +// message in the protocol. +type Message interface { + wire.Message + + Hash() chainhash.Hash + GetIdentity() []byte // XXX Get is ugly but avoids msg field name conflicts + GetSignature() []byte + WriteSigned(io.Writer) error + Expires() int64 + PrevMsgs() []chainhash.Hash // PR returns nil + Sid() []byte // PR returns nil + GetRun() uint32 // PR returns 0 +} diff --git a/mixing/mixpool/log.go b/mixing/mixpool/log.go new file mode 100644 index 0000000000..596dcf9072 --- /dev/null +++ b/mixing/mixpool/log.go @@ -0,0 +1,31 @@ +// Copyright (c) 2020-2021 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mixpool + +import ( + "github.com/decred/slog" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +// The default amount of logging is none. +var log = slog.Disabled + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using slog. +func UseLogger(logger slog.Logger) { + log = logger +} + +// pickNoun returns the singular or plural form of a noun depending on the +// provided count. +func pickNoun(n uint64, singular, plural string) string { + if n == 1 { + return singular + } + return plural +} diff --git a/mixing/mixpool/mixpool.go b/mixing/mixpool/mixpool.go new file mode 100644 index 0000000000..2775a400c9 --- /dev/null +++ b/mixing/mixpool/mixpool.go @@ -0,0 +1,926 @@ +// 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 mixpool provides an in-memory pool of mixing messages for full nodes +// that relay these messages and mixing wallets that send and receive them. +package mixpool + +import ( + "bytes" + "context" + "fmt" + "sort" + "sync" + "time" + + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrutil/v4" + "github.com/decred/dcrd/mixing" + "github.com/decred/dcrd/mixing/utxoproof" + "github.com/decred/dcrd/txscript/v4/stdscript" + "github.com/decred/dcrd/wire" +) + +const minconf = 2 +const feeRate = 0.0001e8 + +type idPubKey = [33]byte + +// Message type constants, for quickly checking looked up entries by message +// hash match the expected type (without performing a type assertion). +// Excludes PR. +const ( + msgtypeKE = 1 + iota + msgtypeCT + msgtypeSR + msgtypeDC + msgtypeCM + msgtypeRS + + nmsgtypes = msgtypeRS +) + +// entry describes non-PR messages accepted to the pool. +type entry struct { + hash chainhash.Hash + sid [32]byte + msg mixing.Message + msgtype int + run uint32 +} + +type session struct { + sid [32]byte + runs []runstate + expiry int64 + bc broadcast +} + +type runstate struct { + run uint32 + npeers uint32 + counts [nmsgtypes]uint32 + hashes map[chainhash.Hash]struct{} +} + +type broadcast struct { + funcs []func() + ch chan struct{} + mu sync.Mutex +} + +// wait returns the wait channel that is closed when a broadcast is made due to +// receiving the expected number of messages for a session. +// Waiters must acquire the pool lock before reading messages. +func (b *broadcast) wait() <-chan struct{} { + b.mu.Lock() + ch := b.ch + b.mu.Unlock() + + return ch +} + +func (b *broadcast) signal() { + b.mu.Lock() + close(b.ch) + b.ch = make(chan struct{}) + b.mu.Unlock() +} + +// Pool records in-memory mix messages that have been broadcast over the +// peer-to-peer network. +type Pool struct { + mtx sync.RWMutex + prs map[chainhash.Hash]*wire.MsgMixPR + pool map[chainhash.Hash]entry + messagesByIdentity map[idPubKey][]chainhash.Hash + sessions map[[32]byte]*session + + blockchain BlockChain + utxoFetcher UtxoFetcher + feeRate int64 + params *chaincfg.Params +} + +// UtxoEntry provides details regarding unspent transaction outputs. +type UtxoEntry interface { + IsSpent() bool + PkScript() []byte + BlockHeight() int64 + Amount() int64 +} + +// UtxoFetcher defines methods used to validate unspent transaction outputs in +// the pair request message. It is optional, but should be implemented by full +// nodes that have this capability to detect and stop relay of spam and junk +// messages. +type UtxoFetcher interface { + // FetchUtxoEntry defines the function to use to fetch unspent + // transaction output information. + FetchUtxoEntry(wire.OutPoint) (UtxoEntry, error) +} + +// BlockChain queries the current status of the blockchain. Its methods should +// be able to be implemented by both full nodes and SPV wallets. +type BlockChain interface { + // ChainParams identifies which chain parameters the mixing pool is + // associated with. + ChainParams() *chaincfg.Params + + // BestHeader returns the hash and height of the current tip block. + BestHeader() (chainhash.Hash, int64) +} + +// NewPool returns a new mixing pool that accepts and validates mixing messages +// required for distributed transaction mixing. +func NewPool(blockchain BlockChain) *Pool { + pool := &Pool{ + prs: make(map[chainhash.Hash]*wire.MsgMixPR), + pool: make(map[chainhash.Hash]entry), + messagesByIdentity: make(map[idPubKey][]chainhash.Hash), + sessions: make(map[[32]byte]*session), + blockchain: blockchain, + feeRate: feeRate, + params: blockchain.ChainParams(), + } + if u, ok := blockchain.(UtxoFetcher); ok { + pool.utxoFetcher = u + } + return pool +} + +// MixPRHashes returns the hashes of all MixPR messages recorded by the pool. +// This data is provided to peers requesting inital state of the mixpool. +func (p *Pool) MixPRHashes() []chainhash.Hash { + p.mtx.RLock() + hashes := make([]chainhash.Hash, 0, len(p.prs)) + for hash := range p.prs { + hashes = append(hashes, hash) + } + p.mtx.RUnlock() + + return hashes +} + +// Message searches the mixing pool for a message by its hash. +func (p *Pool) Message(query *chainhash.Hash) (mixing.Message, error) { + p.mtx.RLock() + pr := p.prs[*query] + e, ok := p.pool[*query] + p.mtx.RUnlock() + if pr != nil { + return pr, nil + } + if !ok || e.msg == nil { + return nil, fmt.Errorf("message not found") + } + return e.msg, nil +} + +// HaveMessage checks whether the mixing pool contains a message by its hash. +func (p *Pool) HaveMessage(query *chainhash.Hash) bool { + p.mtx.RLock() + e, ok := p.pool[*query] + p.mtx.RUnlock() + if !ok || e.msg == nil { + return false + } + return true +} + +// MixPR searches the mixing pool for a PR message by its hash. +func (p *Pool) MixPR(query *chainhash.Hash) (*wire.MsgMixPR, error) { + var pr *wire.MsgMixPR + + p.mtx.RLock() + e, ok := p.pool[*query] + p.mtx.RUnlock() + if ok { + pr, _ = e.msg.(*wire.MsgMixPR) + } + + if pr == nil { + return nil, fmt.Errorf("PR message not found") + } + + return pr, nil +} + +// MixPRs returns all MixPR messages with hashes matching the query. Unknown +// messages are ignored. +// +// If query is nil, all PRs are returned. +func (p *Pool) MixPRs(query []chainhash.Hash) []*wire.MsgMixPR { + res := make([]*wire.MsgMixPR, 0, len(query)) + + p.mtx.RLock() + defer p.mtx.RUnlock() + + if query == nil { + res = make([]*wire.MsgMixPR, 0, len(p.prs)) + for _, pr := range p.prs { + res = append(res, pr) + } + return res + } + + for i := range query { + e, ok := p.pool[query[i]] + if !ok { + continue + } + + pr, ok := e.msg.(*wire.MsgMixPR) + if ok { + res = append(res, pr) + } + } + + return res +} + +// CompatiblePRs returns all MixPR messages with pairing descriptions matching +// the parameter. +func (p *Pool) CompatiblePRs(pairing []byte) []*wire.MsgMixPR { + p.mtx.RLock() + defer p.mtx.RUnlock() + + res := make([]*wire.MsgMixPR, 0, len(p.prs)) + for _, pr := range p.prs { + prPairing, _ := pr.Pairing() + if bytes.Equal(pairing, prPairing) { + res = append(res, pr) + } + } + return res +} + +// ExpireMessages removes all messages and sessions that indicate an expiry +// height at or before the height parameter. +func (p *Pool) ExpireMessages(height int64) { + p.mtx.Lock() + defer p.mtx.Unlock() + + // Expire sessions and their messages + for sid, ses := range p.sessions { + if ses.expiry > height { + continue + } + + delete(p.sessions, sid) + for _, r := range ses.runs { + for hash := range r.hashes { + delete(p.pool, hash) + } + } + } + + // Expire PRs and remove identity tracking + for hash, pr := range p.prs { + if pr.Expiry > height { + continue + } + + delete(p.prs, hash) + delete(p.messagesByIdentity, pr.Identity) + } +} + +// RemoveSession removes all messages from a completed session. PR messages +// should be removed if the session was successful. +func (p *Pool) RemoveSession(sid [32]byte, removePRs bool) { + p.mtx.Lock() + defer p.mtx.Unlock() + + ses := p.sessions[sid] + if ses == nil { + return + } + + delete(p.sessions, sid) + for _, r := range ses.runs { + for hash := range r.hashes { + delete(p.pool, hash) + if removePRs { + delete(p.prs, hash) + } + } + } +} + +// RemoveRun removes all messages from a failed run in a mix session. +func (p *Pool) RemoveRun(sid [32]byte, run uint32) { + p.mtx.Lock() + defer p.mtx.Unlock() + + ses := p.sessions[sid] + if ses == nil { + return + } + + if run >= uint32(len(ses.runs)) { + return + } + + rs := &ses.runs[run] + for msghash := range rs.hashes { + delete(p.pool, msghash) + } +} + +// ReceiveKEs returns all KE messages in the pool that reference PRs of a +// particular pairing. +func (p *Pool) ReceiveKEs(pairing []byte) []*wire.MsgMixKE { + p.mtx.RLock() + defer p.mtx.RUnlock() + + var kes []*wire.MsgMixKE + for _, e := range p.pool { + ke, ok := e.msg.(*wire.MsgMixKE) + if !ok { + continue + } + for _, prHash := range ke.SeenPRs { + pr := p.prs[prHash] + if pr == nil { + continue + } + prPairing, err := pr.Pairing() + if err != nil { + continue + } + if bytes.Equal(pairing, prPairing) { + kes = append(kes, ke) + } + } + } + + return kes +} + +// Received is a parameter for Pool.Receive describing the session and run to +// receive messages for, and slices for returning results. Only non-nil slices +// will be appended to. Received messages are unsorted. +type Received struct { + Sid [32]byte + Run uint32 + KEs []*wire.MsgMixKE + CTs []*wire.MsgMixCT + SRs []*wire.MsgMixSR + DCs []*wire.MsgMixDC + CMs []*wire.MsgMixCM + RSs []*wire.MsgMixRS +} + +// Receive returns messages matching a session, run, and message type, waiting +// until all described messages have been received, or earlier with the +// messages received so far if the context is cancelled before this point. +func (p *Pool) Receive(ctx context.Context, r *Received) error { + sid := r.Sid + run := r.Run + var bc *broadcast + var rs *runstate + var err error + + p.mtx.RLock() + ses, ok := p.sessions[sid] + if !ok { + p.mtx.Unlock() + return fmt.Errorf("unknown session") + } + bc = &ses.bc + p.mtx.RUnlock() + + select { + case <-ctx.Done(): + // Set error to be returned, but still collect received + // messages + err = ctx.Err() + case <-bc.wait(): + } + + p.mtx.RLock() + defer p.mtx.RUnlock() + + if run >= uint32(len(ses.runs)) { + return fmt.Errorf("unknown run") + } + + rs = &ses.runs[run] + for hash := range rs.hashes { + msg := p.pool[hash].msg + switch msg := msg.(type) { + case *wire.MsgMixKE: + if r.KEs != nil { + r.KEs = append(r.KEs, msg) + } + case *wire.MsgMixCT: + if r.CTs != nil { + r.CTs = append(r.CTs, msg) + } + case *wire.MsgMixSR: + if r.SRs != nil { + r.SRs = append(r.SRs, msg) + } + case *wire.MsgMixDC: + if r.DCs != nil { + r.DCs = append(r.DCs, msg) + } + case *wire.MsgMixCM: + if r.CMs != nil { + r.CMs = append(r.CMs, msg) + } + case *wire.MsgMixRS: + if r.RSs != nil { + r.RSs = append(r.RSs, msg) + } + } + } + + return err +} + +// AcceptMessage accepts a mixing message to the pool. +// +// Messages must contain the mixing participant's identity and contain a valid +// signature committing to all non-signature fields. +// +// PR messages will not be accepted if they reference an unknown UTXO or if not +// enough fee is contributed. Any other message will not be accepted if it +// references previous messages that are not recorded by the pool. +func (p *Pool) AcceptMessage(msg mixing.Message) (accepted mixing.Message, err error) { + // Check if already accepted. + hash := msg.Hash() + p.mtx.RLock() + _, ok := p.pool[hash] + p.mtx.RUnlock() + if ok { + return nil, nil + } + + // Require message to be signed by the presented identity. + if !mixing.VerifyMessageSignature(msg) { + return nil, fmt.Errorf("invalid message signature") + } + id := (*idPubKey)(msg.GetIdentity()) + + // Check that expiry has not been reached, nor that it is too far + // into the future. This limits replay attacks. + // XXX could cache the best known header/height as it is updated to + // prune expired messages. + _, curHeight := p.blockchain.BestHeader() + err = checkExpiry(msg, curHeight, p.params) + if err != nil { + return nil, err + } + + var msgtype int + switch msg := msg.(type) { + case *wire.MsgMixPR: + err := p.acceptPR(msg, &hash, id) + if err != nil { + return nil, err + } + return msg, nil + + case *wire.MsgMixKE: + err := p.acceptKE(msg, &hash, id) + if err != nil { + return nil, err + } + return msg, nil + + case *wire.MsgMixCT: + msgtype = msgtypeCT + case *wire.MsgMixSR: + msgtype = msgtypeSR + case *wire.MsgMixDC: + msgtype = msgtypeDC + case *wire.MsgMixCM: + msgtype = msgtypeCM + case *wire.MsgMixRS: + msgtype = msgtypeRS + default: + return nil, fmt.Errorf("unknown mix message type %T", msg) + } + + sid := *(*[32]byte)(msg.Sid()) + + p.mtx.Lock() + defer p.mtx.Unlock() + + // Check if already accepted. + if _, ok := p.pool[hash]; ok { + return nil, nil + } + + // Check prior message existence in the pool, and only accept messages + // that reference other known and accepted messages of the correct type + // and sid. + // + // XXX This could return an error containing the unknown messages, so + // they can be getdata'd, and if they are not received or are garbage, + // peers can be kicked. + prevMsgs := msg.PrevMsgs() + prevEntries := make([]entry, 0, len(prevMsgs)) + for i := range prevMsgs { + looktype := msgtype - 1 + if msgtype == msgtypeRS { + looktype = 0 + } + e, ok := p.lookupEntry(prevMsgs[i], looktype, &sid) + if !ok { + return nil, fmt.Errorf("reference to unknown " + + "previous message") + } + + prevEntries = append(prevEntries, e) + } + + // Check that a message from this identity does not reuse a run number + // for the session. + for _, prevHash := range p.messagesByIdentity[*id] { + e := p.pool[prevHash] + if e.msgtype == msgtype && e.msg.GetRun() == msg.GetRun() && + bytes.Equal(e.msg.Sid(), msg.Sid()) { + return nil, fmt.Errorf("reused run number from identity") + } + } + + ses := p.sessions[sid] + if ses == nil { + return nil, fmt.Errorf("message belongs to unknown session") + } + + err = p.acceptEntry(msg, msgtype, &hash, id, ses) + if err != nil { + return nil, err + } + return msg, nil +} + +func checkExpiry(msg mixing.Message, curHeight int64, params *chaincfg.Params) error { + expires := msg.Expires() + target := params.TargetTimePerBlock + maxExpiry := curHeight + int64(2*time.Hour/target+target) + switch { + case curHeight >= expires: + return fmt.Errorf("message has expired") + case expires > maxExpiry: + return fmt.Errorf("expiry is too far into future") + } + return nil +} + +func (p *Pool) acceptPR(pr *wire.MsgMixPR, hash *chainhash.Hash, id *idPubKey) error { + if len(pr.UTXOs) == 0 { + return fmt.Errorf("at least one UTXO must be submitted") + } + + // If able, sanity check UTXOs. + if p.utxoFetcher != nil { + err := p.checkUTXOs(pr) + if err != nil { + return err + } + } + + // Require known script classes. + switch mixing.ScriptClass(pr.ScriptClass) { + case mixing.ScriptClassP2PKHv0: + default: + return fmt.Errorf("unsupported mixing script class") + } + + // Require enough fee contributed from this mixing participant. + // Size estimation assumes mixing.ScriptClassP2PKHv0 outputs and inputs. + err := checkFee(pr, p.feeRate) + if err != nil { + return err + } + + p.mtx.Lock() + defer p.mtx.Unlock() + + // Check if already accepted. + if _, ok := p.pool[*hash]; ok { + return nil + } + + // Discourage identity reuse. PRs should be the first message sent by + // this identity, and there should only be one PR per identity. + if len(p.messagesByIdentity[*id]) != 0 { + return fmt.Errorf("identity reused for a PR message") + } + + // Accept the PR + p.prs[*hash] = pr + p.messagesByIdentity[*id] = append(make([]chainhash.Hash, 0, 16), *hash) + + return nil +} + +// Check that UTXOs exist, have confirmations, sum of UTXO values matches the +// input value, and proof of ownership is valid. +func (p *Pool) checkUTXOs(pr *wire.MsgMixPR) error { + var totalValue int64 + _, curHeight := p.blockchain.BestHeader() + + for i := range pr.UTXOs { + utxo := &pr.UTXOs[i] + entry, err := p.utxoFetcher.FetchUtxoEntry(utxo.OutPoint) + if err != nil { + return err + } + if entry.IsSpent() { + return fmt.Errorf("output is not unspent") + } + height := entry.BlockHeight() + if !confirmed(minconf, height, curHeight) { + return fmt.Errorf("output is unconfirmed") + } + + // Check proof of key ownership and ability to sign coinjoin + // inputs. + utxoPkScript := entry.PkScript() + var valid bool + switch { + case stdscript.IsPubKeyHashScriptV0(utxoPkScript): + valid = validateOwnerProofP2PKHv0(utxoPkScript, + utxo.PubKey, utxo.Signature, pr.Expires()) + default: + return fmt.Errorf("unsupported UTXO output script") + } + if !valid { + return fmt.Errorf("invalid UTXO ownership proof") + } + + totalValue += entry.Amount() + } + + if totalValue != pr.InputValue { + return fmt.Errorf("input value does not match sum of UTXO " + + "values") + } + + return nil +} + +// Tags prepended to signed ownership proof messages. +const ( + ownerproofP2PKHv0 = "mixpr-ownerproof-P2PKH-secp256k1-v0-" +) + +func validateOwnerProofP2PKHv0(pkscript, pubkey, sig []byte, expires int64) bool { + extractedHash160 := stdscript.ExtractPubKeyHashV0(pkscript) + pubkeyHash160 := dcrutil.Hash160(pubkey) + if !bytes.Equal(extractedHash160, pubkeyHash160) { + return false + } + + return utxoproof.ValidateSecp256k1P2PKH(pubkey, sig, expires) +} + +func (p *Pool) acceptKE(ke *wire.MsgMixKE, hash *chainhash.Hash, id *idPubKey) error { + sid := ke.SessionID + + // In all runs, previous PR messages in the KE must be sorted. + // This defines the initial unmixed peer positions. + sorted := sort.SliceIsSorted(ke.SeenPRs, func(i, j int) bool { + a := ke.SeenPRs[i][:] + b := ke.SeenPRs[j][:] + return bytes.Compare(a, b) == -1 + }) + if !sorted { + return fmt.Errorf("KE message contains unsorted previous PR " + + "hashes") + } + + // Run-0 KE messages define a session ID by hashing all previously-seen + // PR message hashes. This must match the sid also present in the + // message. Later runs after a failed run may drop peers from the + // SeenPRs set, but the sid remains the same. A sid can not be conjured + // out of thin air, and other messages seen from the network for an + // unknown session are not accepted. + if ke.Run == 0 { + derivedSid := mixing.DeriveSessionID(ke.SeenPRs) + if sid != derivedSid { + return fmt.Errorf("invalid session ID for run-0 KE") + } + } + + p.mtx.Lock() + defer p.mtx.Unlock() + + // Check if already accepted. + if _, ok := p.pool[*hash]; ok { + return nil + } + + // Only accept messages that reference known PRs, and require that these + // PRs are compatible with each other. + prevMsgs := ke.PrevMsgs() + prs := make([]*wire.MsgMixPR, len(prevMsgs)) + var pairing []byte + for i, prevHash := range prevMsgs { + pr, ok := p.prs[prevHash] + if !ok { + return fmt.Errorf("reference to unknown PR") + } + prs[i] = pr + if i == 0 { + var err error + pairing, err = pr.Pairing() + if err != nil { + return err + } + } else { + pairing2, err := pr.Pairing() + if err != nil { + return err + } + if !bytes.Equal(pairing, pairing2) { + return fmt.Errorf("referenced PRs are incompatible") + } + } + } + + ses := p.sessions[sid] + + // Create a session for the first run-0 KE + if ses == nil { + if ke.Run != 0 { + return fmt.Errorf("unknown session for run-%d KE", + ke.Run) + } + + expiry := int64(1<<63 - 1) + hashes := make(map[chainhash.Hash]struct{}) + for i := range prevMsgs { + hashes[prevMsgs[i]] = struct{}{} + prExpiry := prs[i].Expires() + if expiry > prExpiry { + expiry = prExpiry + } + } + ses = &session{ + sid: sid, + runs: make([]runstate, 0, 4), + expiry: expiry, + bc: broadcast{ch: make(chan struct{})}, + } + p.sessions[sid] = ses + } + + return p.acceptEntry(ke, msgtypeKE, hash, id, ses) +} + +func (p *Pool) acceptEntry(msg mixing.Message, msgtype int, hash *chainhash.Hash, + id *[33]byte, ses *session) error { + if msg.Expires() != ses.expiry { + return fmt.Errorf("message has inappropriate expiry") + } + + run := msg.GetRun() + if msg.GetRun() > uint32(len(ses.runs)) { + return fmt.Errorf("message skips runs") + } + + var rs *runstate + if msgtype == msgtypeKE && msg.GetRun() == uint32(len(ses.runs)) { + // Add a runstate for the next run. + ses.runs = append(ses.runs, runstate{ + run: msg.GetRun(), + npeers: uint32(len(msg.PrevMsgs())), + hashes: make(map[chainhash.Hash]struct{}), + }) + rs = &ses.runs[len(ses.runs)-1] + } else { + // Add to existing runstate + rs = &ses.runs[run] + } + + rs.hashes[*hash] = struct{}{} + e := entry{ + hash: *hash, + sid: ses.sid, + msg: msg, + msgtype: msgtype, + run: msg.GetRun(), + } + p.pool[*hash] = e + p.messagesByIdentity[*id] = append(p.messagesByIdentity[*id], *hash) + + count := &rs.counts[msgtype-1] // msgtypes start at 1 + *count++ + if *count == rs.npeers { + ses.bc.signal() + } + + return nil +} + +// lookupEntry returns the message entry matching a message hash with msgtype +// and session id. If msgtype is zero, any message type can be looked up. +func (p *Pool) lookupEntry(hash chainhash.Hash, msgtype int, sid *[32]byte) (entry, bool) { + e, ok := p.pool[hash] + if !ok { + return entry{}, false + } + if msgtype != 0 && e.msgtype != msgtype { + return entry{}, false + } + if e.sid != *sid { + return entry{}, false + } + + return e, true +} + +func confirmed(minConf, txHeight, curHeight int64) bool { + return confirms(txHeight, curHeight) >= minConf +} +func confirms(txHeight, curHeight int64) int64 { + switch { + case txHeight == -1, txHeight > curHeight: + return 0 + default: + return curHeight - txHeight + 1 + } +} + +func checkFee(pr *wire.MsgMixPR, feeRate int64) error { + fee := pr.InputValue - int64(pr.MessageCount)*pr.MixAmount + if pr.Change != nil { + fee -= pr.Change.Value + } + + estimatedSize := estimateP2PKHv0SerializeSize(len(pr.UTXOs), + int(pr.MessageCount), pr.Change != nil) + requiredFee := feeForSerializeSize(feeRate, estimatedSize) + if fee < requiredFee { + return fmt.Errorf("not enough input value, or too low fee") + } + + return nil +} + +func feeForSerializeSize(relayFeePerKb int64, txSerializeSize int) int64 { + fee := relayFeePerKb * int64(txSerializeSize) / 1000 + + if fee == 0 && relayFeePerKb > 0 { + fee = relayFeePerKb + } + + const maxAmount = 21e6 * 1e8 + if fee < 0 || fee > maxAmount { + fee = maxAmount + } + + return fee +} + +const ( + redeemP2PKHv0SigScriptSize = 1 + 73 + 1 + 33 + p2pkhv0PkScriptSize = 1 + 1 + 1 + 20 + 1 + 1 +) + +func estimateP2PKHv0SerializeSize(inputs, outputs int, hasChange bool) int { + // Sum the estimated sizes of the inputs and outputs. + txInsSize := inputs * estimateInputSize(redeemP2PKHv0SigScriptSize) + txOutsSize := outputs * estimateOutputSize(p2pkhv0PkScriptSize) + + changeSize := 0 + if hasChange { + changeSize = estimateOutputSize(p2pkhv0PkScriptSize) + outputs++ + } + + // 12 additional bytes are for version, locktime and expiry. + return 12 + (2 * wire.VarIntSerializeSize(uint64(inputs))) + + wire.VarIntSerializeSize(uint64(outputs)) + + txInsSize + txOutsSize + changeSize +} + +// estimateInputSize returns the worst case serialize size estimate for a tx input +func estimateInputSize(scriptSize int) int { + return 32 + // previous tx + 4 + // output index + 1 + // tree + 8 + // amount + 4 + // block height + 4 + // block index + wire.VarIntSerializeSize(uint64(scriptSize)) + // size of script + scriptSize + // script itself + 4 // sequence +} + +// estimateOutputSize returns the worst case serialize size estimate for a tx output +func estimateOutputSize(scriptSize int) int { + return 8 + // previous tx + 2 + // version + wire.VarIntSerializeSize(uint64(scriptSize)) + // size of script + scriptSize // script itself +} diff --git a/mixing/mixpool/mixpool_test.go b/mixing/mixpool/mixpool_test.go new file mode 100644 index 0000000000..9315252ec5 --- /dev/null +++ b/mixing/mixpool/mixpool_test.go @@ -0,0 +1,418 @@ +// 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 mixpool + +import ( + cryptorand "crypto/rand" + "encoding/hex" + "errors" + "flag" + "fmt" + "io" + "math/big" + "os" + "testing" + + "decred.org/cspp/v2/solverrpc" + "github.com/davecgh/go-spew/spew" + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrutil/v4" + "github.com/decred/dcrd/mixing" + "github.com/decred/dcrd/mixing/internal/chacha20prng" + "github.com/decred/dcrd/mixing/utxoproof" + "github.com/decred/dcrd/txscript/v4" + "github.com/decred/dcrd/wire" +) + +var params = chaincfg.SimNetParams() + +var seed [32]byte + +func testPRNG(t *testing.T) *chacha20prng.Reader { + t.Logf("PRNG seed: %x\n", seed) + return chacha20prng.New(seed[:], 0) +} + +var utxoStore struct { + byName map[string]*mockUTXO + byOutpoint map[wire.OutPoint]UtxoEntry +} + +func TestMain(m *testing.M) { + seedFlag := flag.String("seed", "", "use deterministic PRNG seed (32 bytes, hex)") + flag.Parse() + if *seedFlag != "" { + b, err := hex.DecodeString(*seedFlag) + if err != nil { + fmt.Fprintln(os.Stderr, "invalid -seed:", err) + os.Exit(1) + } + if len(b) != 32 { + fmt.Fprintln(os.Stderr, "invalid -seed: must be 32 bytes") + os.Exit(1) + } + copy(seed[:], b) + } else { + cryptorand.Read(seed[:]) + } + + rand := chacha20prng.New(seed[:], 0) + + utxoStore.byName = makeMockUTXOs(rand) + utxoStore.byOutpoint = make(map[wire.OutPoint]UtxoEntry) + for _, m := range utxoStore.byName { + utxoStore.byOutpoint[m.outpoint] = m + } + + os.Exit(m.Run()) +} + +func generateSecp256k1(rand io.Reader) (*secp256k1.PublicKey, *secp256k1.PrivateKey, error) { + if rand == nil { + rand = cryptorand.Reader + } + + privateKey, err := secp256k1.GeneratePrivateKeyFromRand(rand) + if err != nil { + return nil, nil, err + } + + publicKey := privateKey.PubKey() + + return publicKey, privateKey, nil +} + +func mustDecodeChainhash(s string) chainhash.Hash { + h, err := chainhash.NewHashFromStr(s) + if err != nil { + panic(err) + } + return *h +} + +func mustDecodeHex(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return b +} + +type mockUTXO struct { + outpoint wire.OutPoint + tx *wire.MsgTx + output *wire.TxOut + blockHeight int64 + pubkey []byte + privkey *secp256k1.PrivateKey +} + +func (m *mockUTXO) IsSpent() bool { return false } +func (m *mockUTXO) PkScript() []byte { return m.output.PkScript } +func (m *mockUTXO) BlockHeight() int64 { return m.blockHeight } +func (m *mockUTXO) Amount() int64 { return m.output.Value } + +func makeMockUTXO(rand io.Reader, value int64) *mockUTXO { + tx := wire.NewMsgTx() + + pub, priv, err := generateSecp256k1(rand) + if err != nil { + panic(err) + } + pubSerialized := pub.SerializeCompressed() + + p2pkhScript := []byte{ + 0: txscript.OP_DUP, + 1: txscript.OP_HASH160, + 2: txscript.OP_DATA_20, + 23: txscript.OP_EQUALVERIFY, + 24: txscript.OP_CHECKSIG, + } + hash160 := dcrutil.Hash160(pubSerialized) + copy(p2pkhScript[3:23], hash160) + + output := wire.NewTxOut(value, p2pkhScript) + tx.AddTxOut(output) + + return &mockUTXO{ + outpoint: wire.OutPoint{ + Hash: tx.TxHash(), + Index: 0, + Tree: 0, + }, + tx: tx, + output: output, + pubkey: pubSerialized, + privkey: priv, + } +} + +func (m *mockUTXO) mixprutxo(expires int64) wire.MixPRUTXO { + k := utxoproof.Secp256k1KeyPair{ + Pub: m.pubkey, + Priv: m.privkey, + } + sig, err := k.SignUtxoProof(expires) + if err != nil { + panic(err) + } + + return wire.MixPRUTXO{ + OutPoint: m.outpoint, + PubKey: m.pubkey, + Signature: sig, + } +} + +func makeMockUTXOs(rand io.Reader) map[string]*mockUTXO { + return map[string]*mockUTXO{ + "A": makeMockUTXO(rand, 20e8), + } +} + +type fakechain struct { + hash chainhash.Hash + height int64 +} + +func (c *fakechain) ChainParams() *chaincfg.Params { + return params +} + +func (c *fakechain) setBestHeader(hash chainhash.Hash, height int64) { + c.hash = hash + c.height = height +} + +func (c *fakechain) BestHeader() (chainhash.Hash, int64) { + return c.hash, c.height +} + +func (c *fakechain) FetchUtxoEntry(op wire.OutPoint) (UtxoEntry, error) { + entry := utxoStore.byOutpoint[op] + if entry == nil { + return nil, errors.New("no utxo entry") + } + return entry, nil +} + +func TestAccept(t *testing.T) { + t.Parallel() + testRand := testPRNG(t) + + c := new(fakechain) + c.height = 1000 + p := NewPool(c) + + identityPub, identityPriv, err := generateSecp256k1(testRand) + if err != nil { + t.Fatal(err) + } + identity := *(*[33]byte)(identityPub.SerializeCompressed()) + + var ( + expires int64 = 1010 + mixAmount int64 = 10e8 + scriptClass = mixing.ScriptClassP2PKHv0 + txVersion uint16 = wire.TxVersion + lockTime uint32 = 0 + messageCount uint32 = 1 + inputValue int64 = 20e8 + utxos []wire.MixPRUTXO + change *wire.TxOut + ) + utxos = []wire.MixPRUTXO{ + utxoStore.byName["A"].mixprutxo(expires), + } + pr, err := wire.NewMsgMixPR(identity, expires, mixAmount, + string(scriptClass), txVersion, lockTime, messageCount, + inputValue, utxos, change) + if err != nil { + t.Fatal(err) + } + err = mixing.SignMessage(pr, identityPriv) + if err != nil { + t.Fatal(err) + } + _, err = p.AcceptMessage(pr) + if err != nil { + t.Fatal(err) + } + + prngSeed := testRand.Next(32) + runPRNG := chacha20prng.New(prngSeed, 0) + kx, err := mixing.NewKX(runPRNG) + if err != nil { + t.Fatal(err) + } + + // Generate unpadded SR and DC messages. + var msize uint32 = 20 + var mcount uint32 = 2 + srMsg := make([]*big.Int, mcount) + for i := range srMsg { + srMsg[i], err = cryptorand.Int(testRand, mixing.F) + if err != nil { + t.Fatal(err) + } + } + dcMsg := make([][]byte, mcount) + for i := range dcMsg { + dcMsg[i] = testRand.Next(int(msize)) + } + t.Logf("SR messages %+x\n", srMsg) + t.Logf("DC messages %+x\n", dcMsg) + + var ( + seenPRs = []chainhash.Hash{pr.Hash()} + sid [32]byte = mixing.DeriveSessionID(seenPRs) + run uint32 = 0 + ecdh [33]byte = *(*[33]byte)(kx.ECDHPublicKey.SerializeCompressed()) + pqpk [1218]byte = *kx.PQPublicKey + commitment [32]byte // XXX: hash of RS message + ) + ke := wire.NewMsgMixKE(identity, sid, expires, run, ecdh, pqpk, + commitment, seenPRs) + err = mixing.SignMessage(ke, identityPriv) + if err != nil { + t.Fatal(err) + } + _, err = p.AcceptMessage(ke) + if err != nil { + t.Fatal(err) + } + + pqPubkeys := []*mixing.PQPublicKey{kx.PQPublicKey} + ciphertexts, err := kx.Encapsulate(runPRNG, pqPubkeys, 0) + if err != nil { + t.Fatal(err) + } + + seenKEs := []chainhash.Hash{ke.Hash()} + ct := wire.NewMsgMixCT(identity, sid, expires, run, ciphertexts, seenKEs) + err = mixing.SignMessage(ct, identityPriv) + if err != nil { + t.Fatal(err) + } + _, err = p.AcceptMessage(ct) + if err != nil { + t.Fatal(err) + } + + mcounts := []uint32{mcount} + revealedKeys := &mixing.RevealedKeys{ + ECDHPublicKeys: []*secp256k1.PublicKey{kx.ECDHPublicKey}, + Ciphertexts: ciphertexts, + MyIndex: 0, + } + secrets, err := kx.SharedSecrets(revealedKeys, sid[:], msize, run, mcounts) + if err != nil { + t.Fatal(err) + } + + // Pad SR messages + srmix := make([][]*big.Int, mcount) + myStart := uint32(0) + for i := uint32(0); i < mcount; i++ { + pads := mixing.SRMixPads(secrets.SRSecrets[i], myStart+i) + padded := mixing.SRMix(srMsg[i], pads) + srmix[i] = make([]*big.Int, len(padded)) + for j := 0; j < len(padded); j++ { + srmix[i][j] = padded[j] + } + } + srmixBytes := make([][][]byte, len(srmix)) + for i := range srmix { + srmixBytes[i] = make([][]byte, len(srmix[i])) + for j := range srmix[i] { + srmixBytes[i][j] = srmix[i][j].Bytes() + } + } + t.Logf("SR mix %+x\n", srmix) + + seenCTs := []chainhash.Hash{ct.Hash()} + sr := wire.NewMsgMixSR(identity, sid, expires, run, srmixBytes, seenCTs) + err = mixing.SignMessage(sr, identityPriv) + if err != nil { + t.Fatal(err) + } + _, err = p.AcceptMessage(sr) + if err != nil { + t.Fatal(err) + } + + vs := srmix + powerSums := mixing.AddVectors(vs...) + coeffs := mixing.Coefficients(powerSums) + roots, err := solverrpc.Roots(coeffs, mixing.F) + if err != nil { + t.Fatal(err) + } + t.Logf("solved roots %+x\n", roots) + + // Pad DC messages + dcmix := make([]wire.MixVec, mcount) + var slots []int + for i := 0; i < int(mcount); i++ { + slots = append(slots, i) + } + for i, slot := range slots { + my := myStart + uint32(i) + pads := mixing.DCMixPads(secrets.DCSecrets[i], msize, my) + dcmix[i] = wire.MixVec(mixing.DCMix(&pads, dcMsg[i], uint32(slot))) + } + + seenSRs := []chainhash.Hash{sr.Hash()} + dc := wire.NewMsgMixDC(identity, sid, expires, run, dcmix, seenSRs) + err = mixing.SignMessage(dc, identityPriv) + if err != nil { + t.Fatal(err) + } + _, err = p.AcceptMessage(dc) + if err != nil { + t.Fatal(err) + } + + dcVecs := make([]mixing.Vec, 0, len(dcmix)) + for _, vec := range dcmix { + dcVecs = append(dcVecs, mixing.Vec(vec)) + } + res := mixing.XorVectors(dcVecs) + t.Logf("recovered message set %v", res.String()) + + tx := wire.NewMsgTx() + for i := uint32(0); i < res.N; i++ { + hash160 := res.M(i) + pkscript := []byte{ + 0: txscript.OP_DUP, + 1: txscript.OP_HASH160, + 2: txscript.OP_DATA_20, + 23: txscript.OP_EQUALVERIFY, + 24: txscript.OP_CHECKSIG, + } + copy(pkscript[3:23], hash160) + tx.AddTxOut(wire.NewTxOut(mixAmount, pkscript)) + } + t.Logf("mixed tx hash %v", tx.TxHash()) + + seenDCs := []chainhash.Hash{dc.Hash()} + cm := wire.NewMsgMixCM(identity, sid, expires, run, tx, seenDCs) + err = mixing.SignMessage(cm, identityPriv) + if err != nil { + t.Fatal(err) + } + _, err = p.AcceptMessage(cm) + if err != nil { + t.Fatal(err) + } + + t.Logf("%s", spew.Sdump(tx)) +} + +func TestX(t *testing.T) { + t.Parallel() +} diff --git a/mixing/scriptclass.go b/mixing/scriptclass.go new file mode 100644 index 0000000000..849d67d8d4 --- /dev/null +++ b/mixing/scriptclass.go @@ -0,0 +1,12 @@ +package mixing + +// ScriptClass describes the type and format of scripts that can be used for +// mixed outputs. A mix may only be performed among all participants who agree +// on the same script class. +type ScriptClass string + +// Script class descriptors for the mixed outputs. +// Only secp256k1 P2PKH is allowed at this time. +const ( + ScriptClassP2PKHv0 ScriptClass = "P2PKH-secp256k1-v0" +) diff --git a/mixing/sid.go b/mixing/sid.go new file mode 100644 index 0000000000..7aef48c579 --- /dev/null +++ b/mixing/sid.go @@ -0,0 +1,17 @@ +package mixing + +import ( + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/crypto/blake256" +) + +// DeriveSessionID creates the mix session identifier from an initial sorted +// slice of PR message hashes. +func DeriveSessionID(seenPRs []chainhash.Hash) [32]byte { + h := blake256.New() + h.Write([]byte("decred-mix-session")) + for i := range seenPRs { + h.Write(seenPRs[i][:]) + } + return *(*[32]byte)(h.Sum(nil)) +} diff --git a/mixing/signatures.go b/mixing/signatures.go new file mode 100644 index 0000000000..13c755fa43 --- /dev/null +++ b/mixing/signatures.go @@ -0,0 +1,62 @@ +// 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 mixing + +import ( + "io" + + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr" +) + +type Signed interface { + GetIdentity() []byte + GetSignature() []byte + WriteSigned(io.Writer) error +} + +func SignMessage(m Signed, priv *secp256k1.PrivateKey) error { + sig, err := sign(priv, m) + if err != nil { + return err + } + copy(m.GetSignature(), sig) + return nil +} + +func VerifyMessageSignature(m Signed) bool { + return verify(m.GetIdentity(), m, m.GetSignature()) +} + +func sign(priv *secp256k1.PrivateKey, m Signed) ([]byte, error) { + h := blake256.New() + err := m.WriteSigned(h) + if err != nil { + return nil, err + } + sig, err := schnorr.Sign(priv, h.Sum(nil)) + if err != nil { + return nil, err + } + return sig.Serialize(), nil +} + +func verify(pk []byte, m Signed, sig []byte) bool { + pkParsed, err := secp256k1.ParsePubKey(pk) + if err != nil { + return false + } + sigParsed, err := schnorr.ParseSignature(sig) + if err != nil { + return false + } + h := blake256.New() + err = m.WriteSigned(h) + if err != nil { + return false + } + return sigParsed.Verify(h.Sum(nil), pkParsed) +} diff --git a/mixing/utxoproof/utxoproof.go b/mixing/utxoproof/utxoproof.go new file mode 100644 index 0000000000..e1cde63b52 --- /dev/null +++ b/mixing/utxoproof/utxoproof.go @@ -0,0 +1,71 @@ +// 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 utxoproof + +import ( + "encoding/binary" + + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr" +) + +const ( + tag = "mixpr-utxoproof" + + // schemes + secp256k1P2PKH = "P2PKH(EC-Schnorr-DCRv0)" +) + +type Secp256k1KeyPair struct { + Pub []byte + Priv *secp256k1.PrivateKey +} + +func (k *Secp256k1KeyPair) SignUtxoProof(expires int64) ([]byte, error) { + const scheme = secp256k1P2PKH + + h := blake256.New() + h.Write([]byte(tag)) + h.Write([]byte{byte(len(scheme))}) + h.Write([]byte(scheme)) + h.Write(k.Pub) + expiresBytes := binary.BigEndian.AppendUint64(make([]byte, 0, 8), + uint64(expires)) + h.Write(expiresBytes) + hash := h.Sum(nil) + + sig, err := schnorr.Sign(k.Priv, hash) + if err != nil { + return nil, err + } + + return sig.Serialize(), nil +} + +func ValidateSecp256k1P2PKH(pubkey, sig []byte, expires int64) bool { + const scheme = secp256k1P2PKH + + pubkeyParsed, err := secp256k1.ParsePubKey(pubkey) + if err != nil { + return false + } + sigParsed, err := schnorr.ParseSignature(sig) + if err != nil { + return false + } + + h := blake256.New() + h.Write([]byte(tag)) + h.Write([]byte{byte(len(scheme))}) + h.Write([]byte(scheme)) + h.Write(pubkey) + expiresBytes := binary.BigEndian.AppendUint64(make([]byte, 0, 8), + uint64(expires)) + h.Write(expiresBytes) + hash := h.Sum(nil) + + return sigParsed.Verify(hash, pubkeyParsed) +} diff --git a/mixing/vec.go b/mixing/vec.go new file mode 100644 index 0000000000..65c27fca30 --- /dev/null +++ b/mixing/vec.go @@ -0,0 +1,78 @@ +package mixing + +import ( + "bytes" + "fmt" + "strings" +) + +// Vec is a N-element vector of Msize []byte messages. +type Vec struct { + N uint32 + Msize uint32 + Data []byte +} + +// NewVec returns a zero vector for holding n messages of msize length. +func NewVec(n, msize uint32) *Vec { + return &Vec{ + N: n, + Msize: msize, + Data: make([]byte, n*msize), + } +} + +// IsDim returns whether the Vec has dimensions n-by-msize. +func (v *Vec) IsDim(n, msize uint32) bool { + return v.N == n && v.Msize == msize && uint32(len(v.Data)) == n*msize +} + +// Equals returns whether the two vectors have equal dimensions and data. +func (v *Vec) Equals(other *Vec) bool { + return other.IsDim(v.N, v.Msize) && bytes.Equal(other.Data, v.Data) +} + +// M returns the i'th message of the vector. +func (v *Vec) M(i uint32) []byte { + return v.Data[i*v.Msize : i*v.Msize+v.Msize] +} + +func (v *Vec) String() string { + b := new(strings.Builder) + b.Grow(int(2 + v.N*(2*v.Msize+1))) + b.WriteString("[") + for i := uint32(0); i < v.N; i++ { + if i != 0 { + b.WriteString(" ") + } + fmt.Fprintf(b, "%x", v.M(i)) + } + b.WriteString("]") + return b.String() +} + +// Xor writes the xor of each vector element of src1 and src2 into v. +// Source and destination vectors are allowed to be equal. +// Panics if vectors do not share identical dimensions. +func (v *Vec) Xor(src1, src2 *Vec) { + switch { + case v.N != src1.N, v.Msize != src1.Msize, len(v.Data) != len(src1.Data): + fallthrough + case v.N != src2.N, v.Msize != src2.Msize, len(v.Data) != len(src2.Data): + panic("dcnet: vectors do not share identical dimensions") + } + for i := range v.Data { + v.Data[i] = src1.Data[i] ^ src2.Data[i] + } +} + +// XorVectors calculates the xor of all vectors. +// Panics if vectors do not share identical dimensions. +func XorVectors(vs []Vec) *Vec { + msize := vs[0].Msize + res := NewVec(uint32(len(vs)), msize) + for i := range vs { + res.Xor(res, &vs[i]) + } + return res +}