diff --git a/wire/common.go b/wire/common.go index 404c72f3d8..749d9830d9 100644 --- a/wire/common.go +++ b/wire/common.go @@ -14,6 +14,7 @@ import ( "time" "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/crypto/blake256" ) const ( @@ -322,6 +323,38 @@ func readElement(r io.Reader, element interface{}) error { } return nil + // Mix identity + case *[33]byte: + _, err := io.ReadFull(r, e[:]) + if err != nil { + return err + } + return nil + + // Mix signature + case *[64]byte: + _, err := io.ReadFull(r, e[:]) + if err != nil { + return err + } + return nil + + // sntrup4591651 ciphertext + case *[1047]byte: + _, err := io.ReadFull(r, e[:]) + if err != nil { + return err + } + return nil + + // sntrup4591651 public key + case *[1218]byte: + _, err := io.ReadFull(r, e[:]) + if err != nil { + return err + } + return nil + case *ServiceFlag: rv, err := binarySerializer.Uint64(r, littleEndian) if err != nil { @@ -377,6 +410,20 @@ func writeElement(w io.Writer, element interface{}) error { // Attempt to write the element based on the concrete type via fast // type assertions first. switch e := element.(type) { + case uint8: + err := binarySerializer.PutUint8(w, e) + if err != nil { + return err + } + return nil + + case uint16: + err := binarySerializer.PutUint16(w, littleEndian, e) + if err != nil { + return err + } + return nil + case int32: err := binarySerializer.PutUint32(w, littleEndian, uint32(e)) if err != nil { @@ -441,6 +488,13 @@ func writeElement(w io.Writer, element interface{}) error { } return nil + case *[32]byte: + _, err := w.Write(e[:]) + if err != nil { + return err + } + return nil + case *chainhash.Hash: _, err := w.Write(e[:]) if err != nil { @@ -448,6 +502,30 @@ func writeElement(w io.Writer, element interface{}) error { } return nil + // Mix signature + case *[64]byte: + _, err := w.Write(e[:]) + if err != nil { + return err + } + return nil + + // sntrup4591761 ciphertext + case *[1047]byte: + _, err := w.Write(e[:]) + if err != nil { + return err + } + return nil + + // sntrup4591761 public key + case *[1218]byte: + _, err := w.Write(e[:]) + if err != nil { + return err + } + return nil + case ServiceFlag: err := binarySerializer.PutUint64(w, littleEndian, uint64(e)) if err != nil { @@ -765,3 +843,16 @@ func isStrictAscii(s string) bool { return true } + +// mustHash returns the hash of the serialized message. If message +// serialization errors, it panics with a wrapped error. +func mustHash(msg Message, pver uint32) chainhash.Hash { + h := blake256.New() + err := msg.BtcEncode(h, pver) + if err != nil { + err := fmt.Errorf("hash of %T failed due to serialization "+ + "error: %w", msg, err) + panic(err) + } + return *(*chainhash.Hash)(h.Sum(nil)) +} diff --git a/wire/error.go b/wire/error.go index d21279220d..a56e0a37cf 100644 --- a/wire/error.go +++ b/wire/error.go @@ -133,6 +133,18 @@ const ( // ErrTooManyTSpends is returned when the number of tspend hashes // exceeds the maximum allowed. ErrTooManyTSpends + + // ErrMixPRScriptClassTooLong is returned when a mixing script class + // type string is longer than allowed by the protocol. + ErrMixPRScriptClassTooLong + + // ErrTooManyMixPRUTXOs is returned when a MixPR message contains + // more UTXOs than allowed by the protocol. + ErrTooManyMixPRUTXOs + + // ErrTooManyPrevMixMsgs is returned when too many previous messages of + // a mix run are referenced by a message. + ErrTooManyPrevMixMsgs ) // Map of ErrorCode values back to their constant names for pretty printing. @@ -168,6 +180,9 @@ var errorCodeStrings = map[ErrorCode]string{ ErrTooManyInitStateTypes: "ErrTooManyInitStateTypes", ErrInitStateTypeTooLong: "ErrInitStateTypeTooLong", ErrTooManyTSpends: "ErrTooManyTSpends", + ErrMixPRScriptClassTooLong: "ErrMixPRScriptClassTooLong", + ErrTooManyMixPRUTXOs: "ErrTooManyMixPRUTXOs", + ErrTooManyPrevMixMsgs: "ErrTooManyPrevMixMsgs", } // String returns the ErrorCode as a human-readable name. diff --git a/wire/error_test.go b/wire/error_test.go index 155738518f..974e98ecab 100644 --- a/wire/error_test.go +++ b/wire/error_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2017 The btcsuite developers -// Copyright (c) 2015-2020 The Decred developers +// Copyright (c) 2015-2023 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -50,6 +50,9 @@ func TestMessageErrorCodeStringer(t *testing.T) { {ErrTooManyInitStateTypes, "ErrTooManyInitStateTypes"}, {ErrInitStateTypeTooLong, "ErrInitStateTypeTooLong"}, {ErrTooManyTSpends, "ErrTooManyTSpends"}, + {ErrMixPRScriptClassTooLong, "ErrMixPRScriptClassTooLong"}, + {ErrTooManyMixPRUTXOs, "ErrTooManyMixPRUTXOs"}, + {ErrTooManyPrevMixMsgs, "ErrTooManyPrevMixMsgs"}, {0xffff, "Unknown ErrorCode (65535)"}, } diff --git a/wire/go.mod b/wire/go.mod index e2ca97f58f..7f6801e5df 100644 --- a/wire/go.mod +++ b/wire/go.mod @@ -5,10 +5,8 @@ go 1.17 require ( github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/chaincfg/chainhash v1.0.4 + github.com/decred/dcrd/crypto/blake256 v1.0.1 lukechampine.com/blake3 v1.2.1 ) -require ( - github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect - github.com/klauspost/cpuid/v2 v2.0.9 // indirect -) +require github.com/klauspost/cpuid/v2 v2.0.9 // indirect diff --git a/wire/invvect.go b/wire/invvect.go index ca4b1aae14..d6443dd196 100644 --- a/wire/invvect.go +++ b/wire/invvect.go @@ -30,6 +30,7 @@ const ( InvTypeTx InvType = 1 InvTypeBlock InvType = 2 InvTypeFilteredBlock InvType = 3 + InvTypeMix InvType = 4 ) // Map of service flags back to their constant names for pretty printing. @@ -38,6 +39,7 @@ var ivStrings = map[InvType]string{ InvTypeTx: "MSG_TX", InvTypeBlock: "MSG_BLOCK", InvTypeFilteredBlock: "MSG_FILTERED_BLOCK", + InvTypeMix: "MSG_MIX", } // String returns the InvType in human-readable form. diff --git a/wire/invvect_test.go b/wire/invvect_test.go index 95158f9a6e..fb7b417d3d 100644 --- a/wire/invvect_test.go +++ b/wire/invvect_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015-2016 The Decred developers +// Copyright (c) 2015-2023 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -23,6 +23,7 @@ func TestInvTypeStringer(t *testing.T) { {InvTypeError, "ERROR"}, {InvTypeTx, "MSG_TX"}, {InvTypeBlock, "MSG_BLOCK"}, + {InvTypeMix, "MSG_MIX"}, {0xffffffff, "Unknown InvType (4294967295)"}, } diff --git a/wire/message.go b/wire/message.go index fe771a258a..66b8d32525 100644 --- a/wire/message.go +++ b/wire/message.go @@ -58,6 +58,13 @@ const ( CmdCFilterV2 = "cfilterv2" CmdGetInitState = "getinitstate" CmdInitState = "initstate" + CmdMixPR = "mixpr" + CmdMixKE = "mixke" + CmdMixCT = "mixct" + CmdMixSR = "mixsr" + CmdMixDC = "mixdc" + CmdMixCM = "mixcm" + CmdMixRS = "mixrs" ) // Message is an interface that describes a Decred message. A type that @@ -168,6 +175,27 @@ func makeEmptyMessage(command string) (Message, error) { case CmdInitState: msg = &MsgInitState{} + case CmdMixPR: + msg = &MsgMixPR{} + + case CmdMixKE: + msg = &MsgMixKE{} + + case CmdMixCT: + msg = &MsgMixCT{} + + case CmdMixSR: + msg = &MsgMixSR{} + + case CmdMixDC: + msg = &MsgMixDC{} + + case CmdMixCM: + msg = &MsgMixCM{} + + case CmdMixRS: + msg = &MsgMixRS{} + default: str := fmt.Sprintf("unhandled command [%s]", command) return nil, messageError(op, ErrUnknownCmd, str) diff --git a/wire/message_test.go b/wire/message_test.go index b29f14d418..1c2d3e07de 100644 --- a/wire/message_test.go +++ b/wire/message_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015-2021 The Decred developers +// Copyright (c) 2015-2023 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -80,6 +80,16 @@ func TestMessage(t *testing.T) { msgReject := NewMsgReject("block", RejectDuplicate, "duplicate block") msgGetInitState := NewMsgGetInitState() msgInitState := NewMsgInitState() + msgMixPR, err := NewMsgMixPR([33]byte{}, 1, 1, "", 1, 1, 1, 1, []MixPRUTXO{}, NewTxOut(0, []byte{})) + if err != nil { + t.Errorf("NewMsgMixPR: %v", err) + } + msgMixKE := NewMsgMixKE([33]byte{}, [32]byte{}, 1, 1, [33]byte{}, [1218]byte{}, [32]byte{}, []chainhash.Hash{}) + msgMixCT := NewMsgMixCT([33]byte{}, [32]byte{}, 1, 1, [][1047]byte{}, []chainhash.Hash{}) + msgMixSR := NewMsgMixSR([33]byte{}, [32]byte{}, 1, 1, [][][]byte{{{}}}, []chainhash.Hash{}) + msgMixDC := NewMsgMixDC([33]byte{}, [32]byte{}, 1, 1, []MixVec{*NewMixVec(1, 1)}, []chainhash.Hash{}) + msgMixCM := NewMsgMixCM([33]byte{}, [32]byte{}, 1, 1, NewMsgTx(), []chainhash.Hash{}) + msgMixRS := NewMsgMixRS([33]byte{}, [32]byte{}, 1, 1, [32]byte{}, [][]byte{}, [][]byte{}) tests := []struct { in Message // Value to encode @@ -112,6 +122,13 @@ func TestMessage(t *testing.T) { {msgCFTypes, msgCFTypes, pver, MainNet, 26}, {msgGetInitState, msgGetInitState, pver, MainNet, 25}, {msgInitState, msgInitState, pver, MainNet, 27}, + {msgMixPR, msgMixPR, pver, MainNet, 169}, + {msgMixKE, msgMixKE, pver, MainNet, 1449}, + {msgMixCT, msgMixCT, pver, MainNet, 166}, + {msgMixSR, msgMixSR, pver, MainNet, 169}, + {msgMixDC, msgMixDC, pver, MainNet, 171}, + {msgMixCM, msgMixCM, pver, MainNet, 181}, + {msgMixRS, msgMixRS, pver, MainNet, 213}, } t.Logf("Running %d tests", len(tests)) diff --git a/wire/mixvec.go b/wire/mixvec.go new file mode 100644 index 0000000000..146640afb2 --- /dev/null +++ b/wire/mixvec.go @@ -0,0 +1,45 @@ +// 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 wire + +import ( + "fmt" + "strings" +) + +// MixVec is a N-element vector of Msize []byte messages. +type MixVec struct { + N uint32 + Msize uint32 + Data []byte +} + +// NewMixVec returns a zero vector for holding n messages of msize length. +func NewMixVec(n, msize uint32) *MixVec { + return &MixVec{ + N: n, + Msize: msize, + Data: make([]byte, n*msize), + } +} + +func (v *MixVec) String() string { + m := func(i int) []byte { + off := uint32(i) * v.Msize + return v.Data[off : off+v.Msize] + } + + b := new(strings.Builder) + b.Grow(2 + int(v.N*(2*v.Msize+1))) + b.WriteString("[") + for i := 0; uint32(i) < v.N; i++ { + if i != 0 { + b.WriteString(" ") + } + fmt.Fprintf(b, "%x", m(i)) + } + b.WriteString("]") + return b.String() +} diff --git a/wire/msggetinitstate.go b/wire/msggetinitstate.go index 350b6fe4a9..9c9a627500 100644 --- a/wire/msggetinitstate.go +++ b/wire/msggetinitstate.go @@ -29,6 +29,10 @@ const ( // InitStateTSpends is the init state type used to request tpends for // voting. InitStateTSpends = "tspends" + + // InitStateMixPRs is the init state type used to request mixing pair + // request messages. + InitStateMixPRs = "mixprs" ) // MsgGetInitState implements the Message interface and represents a diff --git a/wire/msginitstate.go b/wire/msginitstate.go index 1ec60ec336..a0d67aaa1d 100644 --- a/wire/msginitstate.go +++ b/wire/msginitstate.go @@ -33,6 +33,7 @@ type MsgInitState struct { BlockHashes []chainhash.Hash VoteHashes []chainhash.Hash TSpendHashes []chainhash.Hash + MixPRHashes []chainhash.Hash // XXX serialize this depending on the protocol version } // AddBlockHash adds a new block hash to the message. Up to diff --git a/wire/msgmixcm.go b/wire/msgmixcm.go new file mode 100644 index 0000000000..0fd3cf8330 --- /dev/null +++ b/wire/msgmixcm.go @@ -0,0 +1,220 @@ +// 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 wire + +import ( + "fmt" + "io" + + "github.com/decred/dcrd/chaincfg/chainhash" +) + +// MsgMixCM contains a partially-signed mix transaction, with signatures +// contributed from the peer identity. When all CM messages are received, +// signatures can be merged and the transaction may be published, ending a +// successful mix session. +// +// It implements the Message interface. +type MsgMixCM struct { + Signature [64]byte + Identity [33]byte + SessionID [32]byte + Expiry int64 + Run uint32 + Mix *MsgTx + SeenDCs []chainhash.Hash +} + +// BtcDecode decodes r using the Decred protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgMixCM) BtcDecode(r io.Reader, pver uint32) error { + const op = "MsgMixCM.BtcDecode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := readElements(r, &msg.Signature, &msg.Identity, &msg.SessionID, + &msg.Expiry, &msg.Run) + if err != nil { + return err + } + + if msg.Mix == nil { + msg.Mix = NewMsgTx() + } + err = msg.Mix.BtcDecode(r, pver) + if err != nil { + return err + } + + count, err := ReadVarInt(r, pver) + if err != nil { + return err + } + if count > MaxPrevMixMsgs { + msg := fmt.Sprintf("too many previous referenced messages [%v]", count) + return messageError(op, ErrTooManyPrevMixMsgs, msg) + } + + seen := make([]chainhash.Hash, count) + for i := range seen { + err := readElement(r, &seen[i]) + if err != nil { + return err + } + } + msg.SeenDCs = seen + + return nil +} + +// BtcEncode encodes the receiver to w using the Decred protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgMixCM) BtcEncode(w io.Writer, pver uint32) error { + const op = "MsgMixCM.BtcEncode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := writeElement(w, &msg.Signature) + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, pver) + if err != nil { + return err + } + + return nil +} + +// writeMessageNoSignature serializes all elements of the message except for +// the signature. This allows code reuse between message serialization, and +// signing and verifying these message contents. +func (msg *MsgMixCM) writeMessageNoSignature(op string, w io.Writer, pver uint32) error { + err := writeElements(w, &msg.Identity, &msg.SessionID, msg.Expiry, + msg.Run) + if err != nil { + return err + } + + if msg.Mix == nil { + msg := "nil mix transaction" + return messageError(op, ErrInvalidMsg, msg) + } + err = msg.Mix.BtcEncode(w, pver) + if err != nil { + return err + } + + count := len(msg.SeenDCs) + if count > MaxPrevMixMsgs { + msg := fmt.Sprintf("too many previous referenced messages [%v]", count) + return messageError(op, ErrTooManyPrevMixMsgs, msg) + } + + err = WriteVarInt(w, pver, uint64(count)) + if err != nil { + return err + } + for i := range msg.SeenDCs { + err = writeElement(w, &msg.SeenDCs[i]) + if err != nil { + return err + } + } + + return nil +} + +// WriteSigned writes a tag identifying the message data, followed by all +// message fields excluding the signature. This is the data committed to when +// the message is signed. +func (msg *MsgMixCM) WriteSigned(w io.Writer) error { + const op = "MsgMixCM.WriteSigned" + + err := WriteVarString(w, MixVersion, CmdMixCM+"-sig") + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, MixVersion) + if err != nil { + return err + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgMixCM) Command() string { + return CmdMixCM +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgMixCM) MaxPayloadLength(pver uint32) uint32 { + return 16543 + MaxBlockPayloadV3 +} + +// Hash returns the hash of the serialized message. +func (msg *MsgMixCM) Hash() chainhash.Hash { + return mustHash(msg, MixVersion) +} + +// GetIdentity returns the message sender's public key identity. +func (msg *MsgMixCM) GetIdentity() []byte { + return msg.Identity[:] +} + +// GetSignature returns the message signature. +func (msg *MsgMixCM) GetSignature() []byte { + return msg.Signature[:] +} + +// Expires returns the block height at which the message expires. +func (msg *MsgMixCM) Expires() int64 { + return msg.Expiry +} + +// PrevMsgs returns the previous DC messages seen by the peer. +func (msg *MsgMixCM) PrevMsgs() []chainhash.Hash { + return msg.SeenDCs +} + +// Sid returns the session ID. +func (msg *MsgMixCM) Sid() []byte { + return msg.SessionID[:] +} + +// GetRun returns the run number. +func (msg *MsgMixCM) GetRun() uint32 { + return msg.Run +} + +// NewMsgMixCM returns a new mixke message that conforms to the Message +// interface using the passed parameters and defaults for the remaining fields. +func NewMsgMixCM(identity [33]byte, sid [32]byte, expiry int64, run uint32, + mix *MsgTx, seenDCs []chainhash.Hash) *MsgMixCM { + + if mix == nil { + mix = NewMsgTx() + } + + return &MsgMixCM{ + Identity: identity, + SessionID: sid, + Expiry: expiry, + Run: run, + Mix: mix, + SeenDCs: seenDCs, + } +} diff --git a/wire/msgmixcm_test.go b/wire/msgmixcm_test.go new file mode 100644 index 0000000000..d1fd02f749 --- /dev/null +++ b/wire/msgmixcm_test.go @@ -0,0 +1,70 @@ +// 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 wire + +import ( + "bytes" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/decred/dcrd/chaincfg/chainhash" +) + +func TestMixCMWire(t *testing.T) { + pver := MixVersion + + repeat := func(b byte, count int) []byte { + s := make([]byte, count) + for i := range s { + s[i] = b + } + return s + } + + // Create a fictitious message with easily-distinguishable fields. + + var sig [64]byte + copy(sig[:], repeat(0x80, 64)) + + var id [33]byte + copy(id[:], repeat(0x81, 33)) + + var sid [32]byte + copy(sid[:], repeat(0x82, 32)) + + const expiry = int64(0x0383838383838383) + const run = uint32(0x84848484) + + mix := NewMsgTx() + + seenDCs := make([]chainhash.Hash, 4) + for b := byte(0x85); b < 0x89; b++ { + copy(seenDCs[b-0x85][:], repeat(b, 32)) + } + + cm := NewMsgMixCM(id, sid, expiry, run, mix, seenDCs) + cm.Signature = sig + + buf := new(bytes.Buffer) + err := cm.BtcEncode(buf, pver) + if err != nil { + t.Fatal(err) + } + + decodedCM := new(MsgMixCM) + err = decodedCM.BtcDecode(bytes.NewReader(buf.Bytes()), pver) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(cm, decodedCM) { + t.Errorf("BtcDecode got: %s want: %s", + spew.Sdump(decodedCM), spew.Sdump(cm)) + } else { + t.Logf("bytes: %x", buf.Bytes()) + t.Logf("spew: %s", spew.Sdump(decodedCM)) + } +} diff --git a/wire/msgmixct.go b/wire/msgmixct.go new file mode 100644 index 0000000000..a34e371fef --- /dev/null +++ b/wire/msgmixct.go @@ -0,0 +1,216 @@ +// 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 wire + +import ( + "fmt" + "io" + + "github.com/decred/dcrd/chaincfg/chainhash" +) + +// MsgMixCT is used by mixing peers to share SNTRUP4591761 ciphertexts with +// other peers who have published their public keys. It implements the Message +// interface. +type MsgMixCT struct { + Signature [64]byte + Identity [33]byte + SessionID [32]byte + Expiry int64 + Run uint32 + Ciphertexts [][1047]byte + SeenKEs []chainhash.Hash +} + +// BtcDecode decodes r using the Decred protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgMixCT) BtcDecode(r io.Reader, pver uint32) error { + const op = "MsgMixCT.BtcDecode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := readElements(r, &msg.Signature, &msg.Identity, &msg.SessionID, + &msg.Expiry, &msg.Run) + if err != nil { + return err + } + + // Count is of both Ciphertexts and SeenKEs. + count, err := ReadVarInt(r, pver) + if err != nil { + return err + } + if count > MaxPrevMixMsgs { + msg := fmt.Sprintf("too many previous referenced messages [%v]", count) + return messageError(op, ErrTooManyPrevMixMsgs, msg) + } + + ciphertexts := make([][1047]byte, count) + for i := range ciphertexts { + err := readElement(r, &ciphertexts[i]) + if err != nil { + return err + } + } + msg.Ciphertexts = ciphertexts + + seen := make([]chainhash.Hash, count) + for i := range seen { + err := readElement(r, &seen[i]) + if err != nil { + return err + } + } + msg.SeenKEs = seen + + return nil +} + +// BtcEncode encodes the receiver to w using the Decred protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgMixCT) BtcEncode(w io.Writer, pver uint32) error { + const op = "MsgMixCT.BtcEncode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := writeElement(w, &msg.Signature) + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, pver) + if err != nil { + return err + } + + return nil +} + +// writeMessageNoSignature serializes all elements of the message except for +// the signature. This allows code reuse between message serialization, and +// signing and verifying these message contents. +func (msg *MsgMixCT) writeMessageNoSignature(op string, w io.Writer, pver uint32) error { + err := writeElements(w, &msg.Identity, &msg.SessionID, msg.Expiry, + msg.Run) + if err != nil { + return err + } + + count := len(msg.Ciphertexts) + if count != len(msg.SeenKEs) { + msg := "differing counts of ciphertexts and seen KE messages" + return messageError(op, ErrInvalidMsg, msg) + } + if count > MaxPrevMixMsgs { + msg := fmt.Sprintf("too many previous referenced messages [%v]", count) + return messageError(op, ErrTooManyPrevMixMsgs, msg) + } + + err = WriteVarInt(w, pver, uint64(count)) + if err != nil { + return err + } + for i := range msg.Ciphertexts { + err = writeElement(w, &msg.Ciphertexts[i]) + if err != nil { + return err + } + } + for i := range msg.SeenKEs { + err = writeElement(w, &msg.SeenKEs[i]) + if err != nil { + return err + } + } + + return nil +} + +// WriteSigned writes a tag identifying the message data, followed by all +// message fields excluding the signature. This is the data committed to when +// the message is signed. +func (msg *MsgMixCT) WriteSigned(w io.Writer) error { + const op = "MsgMixCT.WriteSigned" + + err := WriteVarString(w, MixVersion, CmdMixCT+"-sig") + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, MixVersion) + if err != nil { + return err + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgMixCT) Command() string { + return CmdMixCT +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgMixCT) MaxPayloadLength(pver uint32) uint32 { + return 552592 +} + +// Hash returns the hash of the serialized message. +func (msg *MsgMixCT) Hash() chainhash.Hash { + return mustHash(msg, MixVersion) +} + +// GetIdentity returns the message sender's public key identity. +func (msg *MsgMixCT) GetIdentity() []byte { + return msg.Identity[:] +} + +// GetSignature returns the message signature. +func (msg *MsgMixCT) GetSignature() []byte { + return msg.Signature[:] +} + +// Expires returns the block height at which the message expires. +func (msg *MsgMixCT) Expires() int64 { + return msg.Expiry +} + +// PrevMsgs returns the previous KE messages seen by the peer. +func (msg *MsgMixCT) PrevMsgs() []chainhash.Hash { + return msg.SeenKEs +} + +// Sid returns the session ID. +func (msg *MsgMixCT) Sid() []byte { + return msg.SessionID[:] +} + +// GetRun returns the run number. +func (msg *MsgMixCT) GetRun() uint32 { + return msg.Run +} + +// NewMsgMixCT returns a new mixct message that conforms to the Message +// interface using the passed parameters and defaults for the remaining fields. +func NewMsgMixCT(identity [33]byte, sid [32]byte, expires int64, run uint32, + ciphertexts [][1047]byte, seenKEs []chainhash.Hash) *MsgMixCT { + + return &MsgMixCT{ + Identity: identity, + SessionID: sid, + Expiry: expires, + Run: run, + Ciphertexts: ciphertexts, + SeenKEs: seenKEs, + } +} diff --git a/wire/msgmixct_test.go b/wire/msgmixct_test.go new file mode 100644 index 0000000000..ecb443db6b --- /dev/null +++ b/wire/msgmixct_test.go @@ -0,0 +1,73 @@ +// 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 wire + +import ( + "bytes" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/decred/dcrd/chaincfg/chainhash" +) + +func TestMixCTWire(t *testing.T) { + pver := MixVersion + + repeat := func(b byte, count int) []byte { + s := make([]byte, count) + for i := range s { + s[i] = b + } + return s + } + + // Create a fictitious message with easily-distinguishable fields. + + var sig [64]byte + copy(sig[:], repeat(0x80, 64)) + + var id [33]byte + copy(id[:], repeat(0x81, 33)) + + var sid [32]byte + copy(sid[:], repeat(0x82, 32)) + + const expiry = int64(0x0383838383838383) + const run = uint32(0x84848484) + + cts := make([][1047]byte, 4) + for b := byte(0x85); b < 0x89; b++ { + copy(cts[b-0x85][:], repeat(b, 1047)) + } + + seenKEs := make([]chainhash.Hash, 4) + for b := byte(0x89); b < 0x8D; b++ { + copy(seenKEs[b-0x89][:], repeat(b, 32)) + } + + ct := NewMsgMixCT(id, sid, expiry, run, cts, seenKEs) + ct.Signature = sig + + buf := new(bytes.Buffer) + err := ct.BtcEncode(buf, pver) + if err != nil { + t.Fatal(err) + } + + decodedCT := new(MsgMixCT) + err = decodedCT.BtcDecode(bytes.NewReader(buf.Bytes()), pver) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(ct, decodedCT) { + t.Errorf("BtcDecode got: %s want: %s", + spew.Sdump(decodedCT), spew.Sdump(ct)) + } else { + t.Logf("bytes: %x", buf.Bytes()) + t.Logf("spew: %s", spew.Sdump(decodedCT)) + } +} diff --git a/wire/msgmixdc.go b/wire/msgmixdc.go new file mode 100644 index 0000000000..6ea5bd674a --- /dev/null +++ b/wire/msgmixdc.go @@ -0,0 +1,283 @@ +// 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 wire + +import ( + "fmt" + "io" + + "github.com/decred/dcrd/chaincfg/chainhash" +) + +// MsgMixDC is the DC-net broadcast. It implements the Message interface. +type MsgMixDC struct { + Signature [64]byte + Identity [33]byte + SessionID [32]byte + Expiry int64 + Run uint32 + DCNet []MixVec + SeenSRs []chainhash.Hash +} + +// BtcDecode decodes r using the Decred protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgMixDC) BtcDecode(r io.Reader, pver uint32) error { + const op = "MsgMixDC.BtcDecode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := readElements(r, &msg.Signature, &msg.Identity, &msg.SessionID, + &msg.Expiry, &msg.Run) + if err != nil { + return err + } + + mcount, err := ReadVarInt(r, pver) + if err != nil { + return err + } + if mcount > MaxMixMcount { + msg := fmt.Sprintf("too many total mixed messages [%v]", mcount) + return messageError(op, ErrInvalidMsg, msg) + } + + dcnet := make([]MixVec, mcount) + for i := range dcnet { + err := readMixVec(op, r, pver, &dcnet[i]) + if err != nil { + return err + } + } + msg.DCNet = dcnet + + count, err := ReadVarInt(r, pver) + if err != nil { + return err + } + if count > MaxPrevMixMsgs { + msg := fmt.Sprintf("too many previous referenced messages [%v]", count) + return messageError(op, ErrTooManyPrevMixMsgs, msg) + } + + seen := make([]chainhash.Hash, count) + for i := range seen { + err := readElement(r, &seen[i]) + if err != nil { + return err + } + } + msg.SeenSRs = seen + + return nil +} + +// BtcEncode encodes the receiver to w using the Decred protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgMixDC) BtcEncode(w io.Writer, pver uint32) error { + const op = "MsgMixDC.BtcEncode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := writeElement(w, &msg.Signature) + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, pver) + if err != nil { + return err + } + + return nil +} + +// writeMessageNoSignature serializes all elements of the message except for +// the signature. This allows code reuse between message serialization, and +// signing and verifying these message contents. +func (msg *MsgMixDC) writeMessageNoSignature(op string, w io.Writer, pver uint32) error { + err := writeElements(w, &msg.Identity, &msg.SessionID, msg.Expiry, + msg.Run) + if err != nil { + return err + } + + mcount := len(msg.DCNet) + if mcount == 0 { + msg := fmt.Sprintf("too few mixed messages [%v]", mcount) + return messageError(op, ErrInvalidMsg, msg) + } + if mcount > MaxMixMcount { + msg := fmt.Sprintf("too many total mixed messages [%v]", mcount) + return messageError(op, ErrInvalidMsg, msg) + } + err = WriteVarInt(w, pver, uint64(mcount)) + if err != nil { + return err + } + + for i := range msg.DCNet { + err := writeMixVec(w, pver, &msg.DCNet[i]) + if err != nil { + return err + } + } + + count := len(msg.SeenSRs) + if count > MaxPrevMixMsgs { + msg := fmt.Sprintf("too many previous referenced messages [%v]", count) + return messageError(op, ErrTooManyPrevMixMsgs, msg) + } + + err = WriteVarInt(w, pver, uint64(count)) + if err != nil { + return err + } + for i := range msg.SeenSRs { + err = writeElement(w, &msg.SeenSRs[i]) + if err != nil { + return err + } + } + + return nil +} + +func writeMixVec(w io.Writer, pver uint32, vec *MixVec) error { + err := WriteVarInt(w, pver, uint64(vec.N)) + if err != nil { + return err + } + err = WriteVarInt(w, pver, uint64(vec.Msize)) + if err != nil { + return err + } + err = WriteVarBytes(w, pver, vec.Data) + if err != nil { + return err + } + + return nil +} + +func readMixVec(op string, r io.Reader, pver uint32, vec *MixVec) error { + n, err := ReadVarInt(r, pver) + if err != nil { + return err + } + if n > MaxMixKPCount { + msg := "too many mixing peers" + return messageError(op, ErrInvalidMsg, msg) + } + msize, err := ReadVarInt(r, pver) + if err != nil { + return err + } + if msize > 32 { + msg := "mixed message length exceeds max" + return messageError(op, ErrInvalidMsg, msg) + } + data, err := ReadVarBytes(r, pver, MaxMixKPCount*32, "Data") + if err != nil { + return err + } + if int(n*msize) != len(data) { + msg := "vec dimensions do not match data length" + return messageError(op, ErrInvalidMsg, msg) + } + + vec.N = uint32(n) + vec.Msize = uint32(msize) + vec.Data = data + + return nil +} + +// WriteSigned writes a tag identifying the message data, followed by all +// message fields excluding the signature. This is the data committed to when +// the message is signed. +func (msg *MsgMixDC) WriteSigned(w io.Writer) error { + const op = "MsgMixDC.WriteSigned" + + err := WriteVarString(w, MixVersion, CmdMixDC+"-sig") + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, MixVersion) + if err != nil { + return err + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgMixDC) Command() string { + return CmdMixDC +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgMixDC) MaxPayloadLength(pver uint32) uint32 { + return 16800915 +} + +// Hash returns the hash of the serialized message. +func (msg *MsgMixDC) Hash() chainhash.Hash { + return mustHash(msg, MixVersion) +} + +// GetIdentity returns the message sender's public key identity. +func (msg *MsgMixDC) GetIdentity() []byte { + return msg.Identity[:] +} + +// GetSignature returns the message signature. +func (msg *MsgMixDC) GetSignature() []byte { + return msg.Signature[:] +} + +// Expires returns the block height at which the message expires. +func (msg *MsgMixDC) Expires() int64 { + return msg.Expiry +} + +// PrevMsgs returns the previous SR messages seen by the peer. +func (msg *MsgMixDC) PrevMsgs() []chainhash.Hash { + return msg.SeenSRs +} + +// Sid returns the session ID. +func (msg *MsgMixDC) Sid() []byte { + return msg.SessionID[:] +} + +// GetRun returns the run number. +func (msg *MsgMixDC) GetRun() uint32 { + return msg.Run +} + +// NewMsgMixDC returns a new mixsr message that conforms to the Message +// interface using the passed parameters and defaults for the remaining fields. +func NewMsgMixDC(identity [33]byte, sid [32]byte, expiry int64, run uint32, + dcnet []MixVec, seenSRs []chainhash.Hash) *MsgMixDC { + + return &MsgMixDC{ + Identity: identity, + SessionID: sid, + Expiry: expiry, + Run: run, + DCNet: dcnet, + SeenSRs: seenSRs, + } +} diff --git a/wire/msgmixdc_test.go b/wire/msgmixdc_test.go new file mode 100644 index 0000000000..aa516cb3dd --- /dev/null +++ b/wire/msgmixdc_test.go @@ -0,0 +1,83 @@ +// 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 wire + +import ( + "bytes" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/decred/dcrd/chaincfg/chainhash" +) + +func TestMixDCWire(t *testing.T) { + pver := MixVersion + + repeat := func(b byte, count int) []byte { + s := make([]byte, count) + for i := range s { + s[i] = b + } + return s + } + + // Create a fictitious message with easily-distinguishable fields. + + var sig [64]byte + copy(sig[:], repeat(0x80, 64)) + + var id [33]byte + copy(id[:], repeat(0x81, 33)) + + var sid [32]byte + copy(sid[:], repeat(0x82, 32)) + + const expiry = int64(0x0383838383838383) + const run = uint32(0x84848484) + + mcount := 4 + var kpcount uint32 = 4 + dcnet := make([]MixVec, mcount) + // will add 4x4 field numbers of incrementing repeating byte values to + // dcnet, ranging from 0x85 through 0x94 + b := byte(0x85) + for i := 0; i < mcount; i++ { + dcnet[i].N = kpcount + dcnet[i].Msize = 32 + for j := 0; j < int(kpcount); j++ { + dcnet[i].Data = append(dcnet[i].Data, repeat(b, 32)...) + b++ + } + } + + seenSRs := make([]chainhash.Hash, 4) + for b := byte(0x95); b < 0x99; b++ { + copy(seenSRs[b-0x95][:], repeat(b, 32)) + } + + dc := NewMsgMixDC(id, sid, expiry, run, dcnet, seenSRs) + dc.Signature = sig + + buf := new(bytes.Buffer) + err := dc.BtcEncode(buf, pver) + if err != nil { + t.Fatal(err) + } + + decodedDC := new(MsgMixDC) + err = decodedDC.BtcDecode(bytes.NewReader(buf.Bytes()), pver) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(dc, decodedDC) { + t.Errorf("BtcDecode got: %s want: %s", + spew.Sdump(decodedDC), spew.Sdump(dc)) + } else { + t.Logf("bytes: %x", buf.Bytes()) + t.Logf("spew: %s", spew.Sdump(decodedDC)) + } +} diff --git a/wire/msgmixke.go b/wire/msgmixke.go new file mode 100644 index 0000000000..63a4a7252e --- /dev/null +++ b/wire/msgmixke.go @@ -0,0 +1,208 @@ +// 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 wire + +import ( + "fmt" + "io" + + "github.com/decred/dcrd/chaincfg/chainhash" +) + +const ( + // MaxPrevMixMsgs is the maximum number previous messages of a mix run + // that may be referenced by a message. + MaxPrevMixMsgs = 512 // XXX: PNOOMA +) + +// MsgMixKE implements the Message interface and represents a mixing key +// exchange message. It includes a commitment to secrets (private keys and +// discarded mixed addresses) in case they must be revealed for blame +// assignment. +type MsgMixKE struct { + Signature [64]byte + Identity [33]byte + SessionID [32]byte + Expiry int64 + Run uint32 + ECDH [33]byte // Secp256k1 public key + PQPK [1218]byte // Sntrup4591761 public key + Commitment [32]byte + SeenPRs []chainhash.Hash +} + +// BtcDecode decodes r using the Decred protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgMixKE) BtcDecode(r io.Reader, pver uint32) error { + const op = "MsgMixKE.BtcDecode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := readElements(r, &msg.Signature, &msg.Identity, &msg.SessionID, + &msg.Expiry, &msg.Run, &msg.ECDH, &msg.PQPK, &msg.Commitment) + if err != nil { + return err + } + + count, err := ReadVarInt(r, pver) + if err != nil { + return err + } + if count > MaxPrevMixMsgs { + msg := fmt.Sprintf("too many previous referenced messages [%v]", count) + return messageError(op, ErrTooManyPrevMixMsgs, msg) + } + + seen := make([]chainhash.Hash, count) + for i := range seen { + err := readElement(r, &seen[i]) + if err != nil { + return err + } + } + msg.SeenPRs = seen + + return nil +} + +// BtcEncode encodes the receiver to w using the Decred protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgMixKE) BtcEncode(w io.Writer, pver uint32) error { + const op = "MsgMixKE.BtcEncode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := writeElement(w, &msg.Signature) + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, pver) + if err != nil { + return err + } + + return nil +} + +// writeMessageNoSignature serializes all elements of the message except for +// the signature. This allows code reuse between message serialization, and +// signing and verifying these message contents. +func (msg *MsgMixKE) writeMessageNoSignature(op string, w io.Writer, pver uint32) error { + err := writeElements(w, &msg.Identity, &msg.SessionID, msg.Expiry, + msg.Run, &msg.ECDH, &msg.PQPK, &msg.Commitment) + if err != nil { + return err + } + + // Limit to max previous messages hashes. + count := len(msg.SeenPRs) + if count > MaxPrevMixMsgs { + msg := fmt.Sprintf("too many previous referenced messages [%v]", count) + return messageError(op, ErrTooManyPrevMixMsgs, msg) + } + + err = WriteVarInt(w, pver, uint64(count)) + if err != nil { + return err + } + for i := range msg.SeenPRs { + err := writeElement(w, &msg.SeenPRs[i]) + if err != nil { + return err + } + } + + return nil +} + +// WriteSigned writes a tag identifying the message data, followed by all +// message fields excluding the signature. This is the data committed to when +// the message is signed. +func (msg *MsgMixKE) WriteSigned(w io.Writer) error { + const op = "MsgMixKE.WriteSigned" + + err := WriteVarString(w, MixVersion, CmdMixKE+"-sig") + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, MixVersion) + if err != nil { + return err + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgMixKE) Command() string { + return CmdMixKE +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgMixKE) MaxPayloadLength(pver uint32) uint32 { + return 17811 +} + +// Hash returns the hash of the serialized message. +func (msg *MsgMixKE) Hash() chainhash.Hash { + return mustHash(msg, MixVersion) +} + +// GetIdentity returns the message sender's public key identity. +func (msg *MsgMixKE) GetIdentity() []byte { + return msg.Identity[:] +} + +// GetSignature returns the message signature. +func (msg *MsgMixKE) GetSignature() []byte { + return msg.Signature[:] +} + +// Expires returns the block height at which the message expires. +func (msg *MsgMixKE) Expires() int64 { + return msg.Expiry +} + +// PrevMsgs returns the previous PR messages seen by the peer. +func (msg *MsgMixKE) PrevMsgs() []chainhash.Hash { + return msg.SeenPRs +} + +// Sid returns the session ID. +func (msg *MsgMixKE) Sid() []byte { + return msg.SessionID[:] +} + +// GetRun returns the run number. +func (msg *MsgMixKE) GetRun() uint32 { + return msg.Run +} + +// NewMsgMixKE returns a new mixke message that conforms to the Message +// interface using the passed parameters and defaults for the remaining fields. +func NewMsgMixKE(identity [33]byte, sid [32]byte, expires int64, run uint32, + ecdh [33]byte, pqpk [1218]byte, commitment [32]byte, seenPRs []chainhash.Hash) *MsgMixKE { + + return &MsgMixKE{ + Identity: identity, + SessionID: sid, + Expiry: expires, + Run: run, + ECDH: ecdh, + PQPK: pqpk, + Commitment: commitment, + SeenPRs: seenPRs, + } +} diff --git a/wire/msgmixke_test.go b/wire/msgmixke_test.go new file mode 100644 index 0000000000..08c5030d71 --- /dev/null +++ b/wire/msgmixke_test.go @@ -0,0 +1,77 @@ +// 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 wire + +import ( + "bytes" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/decred/dcrd/chaincfg/chainhash" +) + +func TestMixKEWire(t *testing.T) { + pver := MixVersion + + repeat := func(b byte, count int) []byte { + s := make([]byte, count) + for i := range s { + s[i] = b + } + return s + } + + // Create a fictitious message with easily-distinguishable fields. + + var sig [64]byte + copy(sig[:], repeat(0x80, 64)) + + var id [33]byte + copy(id[:], repeat(0x81, 33)) + + var sid [32]byte + copy(sid[:], repeat(0x82, 32)) + + const expiry = int64(0x0383838383838383) + const run = uint32(0x84848484) + + var ecdh [33]byte + copy(ecdh[:], repeat(0x85, 33)) + + var pqpk [1218]byte + copy(pqpk[:], repeat(0x86, 1218)) + + var commitment [32]byte + copy(commitment[:], repeat(0x87, 32)) + + seenPRs := make([]chainhash.Hash, 4) + for b := byte(0x88); b < 0x8C; b++ { + copy(seenPRs[b-0x88][:], repeat(b, 32)) + } + + ke := NewMsgMixKE(id, sid, expiry, run, ecdh, pqpk, commitment, seenPRs) + ke.Signature = sig + + buf := new(bytes.Buffer) + err := ke.BtcEncode(buf, pver) + if err != nil { + t.Fatal(err) + } + + decodedKE := new(MsgMixKE) + err = decodedKE.BtcDecode(bytes.NewReader(buf.Bytes()), pver) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(ke, decodedKE) { + t.Errorf("BtcDecode got: %s want: %s", + spew.Sdump(decodedKE), spew.Sdump(ke)) + } else { + t.Logf("bytes: %x", buf.Bytes()) + t.Logf("spew: %s", spew.Sdump(decodedKE)) + } +} diff --git a/wire/msgmixpr.go b/wire/msgmixpr.go new file mode 100644 index 0000000000..4b86f00fcd --- /dev/null +++ b/wire/msgmixpr.go @@ -0,0 +1,399 @@ +// 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 wire + +import ( + "bytes" + "fmt" + "io" + + "github.com/decred/dcrd/chaincfg/chainhash" +) + +const ( + // MaxMixPRScriptClassLen is the maximum length allowable for a + // MsgMixPR script class. + MaxMixPRScriptClassLen = 32 + + // MaxMixPRUTXOs is the maximum number of unspent transaction outputs + // that may be contributed in a single MixPR message. + MaxMixPRUTXOs = 512 // XXX: PNOOMA + + // MaxMixPRUTXOScriptLen is the maximum length allowed for the unhashed + // P2SH script of a UTXO ownership proof. + // XXX: might want to limit this to standard script sizes + MaxMixPRUTXOScriptLen = 16384 // txscript.MaxScriptSize + + // MaxMixPRUTXOPubKeyLen is the maximum length allowed for the + // pubkey of a UTXO ownership proof. + MaxMixPRUTXOPubKeyLen = 33 + + // MaxMixPRUTXOSignatureLen is the maximum length allowed for the + // signature of a UTXO ownership proof. + MaxMixPRUTXOSignatureLen = 64 +) + +// MixPRUTXO describes an unspent transaction output to be spent in a mix. It +// includes a proof that the output is able to be spent, by containing a +// signature and the necessary data needed to prove key possession. +type MixPRUTXO struct { + OutPoint OutPoint + Script []byte // Only used for P2SH + PubKey []byte + Signature []byte +} + +// MsgMixPR implements the Message interface and represents a mixing pair +// request message. It describes a type of coinjoin to be created, unmixed data +// being contributed to the coinjoin, and proof of ability to sign the resulting +// coinjoin. +type MsgMixPR struct { + Signature [64]byte + Identity [33]byte + Expiry int64 + MixAmount int64 + ScriptClass string + TxVersion uint16 + LockTime uint32 + MessageCount uint32 + InputValue int64 + UTXOs []MixPRUTXO + Change *TxOut +} + +// Pairing returns a description of the coinjoin transaction being created. +// Different MixPR messages area compatible to perform a mix together if their +// pairing descriptions are identical. +func (msg *MsgMixPR) Pairing() ([]byte, error) { + w := bytes.NewBuffer(make([]byte, 0, 8+32+2+4)) + + err := writeElement(w, msg.MixAmount) + if err != nil { + return nil, err + } + + err = WriteVarString(w, MixVersion, msg.ScriptClass) + if err != nil { + return nil, err + } + + err = writeElements(w, msg.TxVersion, msg.LockTime) + if err != nil { + return nil, err + } + + return w.Bytes(), nil +} + +// BtcDecode decodes r using the Decred protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgMixPR) BtcDecode(r io.Reader, pver uint32) error { + const op = "MsgMixPR.BtcDecode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := readElements(r, &msg.Signature, &msg.Identity, &msg.Expiry, + &msg.MixAmount) + if err != nil { + return err + } + + sc, err := ReadAsciiVarString(r, pver, MaxMixPRScriptClassLen) + if err != nil { + return err + } + msg.ScriptClass = sc + + err = readElements(r, &msg.TxVersion, &msg.LockTime, + &msg.MessageCount, &msg.InputValue) + if err != nil { + return err + } + + count, err := ReadVarInt(r, pver) + if err != nil { + return err + } + if count > MaxMixPRUTXOs { + msg := fmt.Sprintf("too many UTXOs in message [%v]", count) + return messageError(op, ErrTooManyMixPRUTXOs, msg) + } + utxos := make([]MixPRUTXO, count) + for i := range utxos { + utxo := &utxos[i] + + err := ReadOutPoint(r, pver, msg.TxVersion, &utxo.OutPoint) + if err != nil { + return err + } + + script, err := ReadVarBytes(r, pver, MaxMixPRUTXOScriptLen, + "MixPRUTXO.Script") + if err != nil { + return err + } + utxo.Script = script + + pubkey, err := ReadVarBytes(r, pver, MaxMixPRUTXOPubKeyLen, + "MixPRUTXO.PubKey") + if err != nil { + return err + } + utxo.PubKey = pubkey + + sig, err := ReadVarBytes(r, pver, MaxMixPRUTXOSignatureLen, + "MixPRUTXO.Signature") + if err != nil { + return err + } + utxo.Signature = sig + } + msg.UTXOs = utxos + + var hasChange uint8 + err = readElement(r, &hasChange) + if err != nil { + return err + } + switch hasChange { + case 0: + case 1: + change := new(TxOut) + err := readTxOut(r, pver, msg.TxVersion, change) + if err != nil { + return err + } + msg.Change = change + default: + msg := "invalid change TxOut encoding" + return messageError(op, ErrInvalidMsg, msg) + } + + return nil +} + +// BtcEncode encodes the receiver to w using the Decred protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgMixPR) BtcEncode(w io.Writer, pver uint32) error { + const op = "MsgMixPR.BtcEncode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := writeElement(w, &msg.Signature) + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, pver) + if err != nil { + return err + } + + return nil +} + +// writeMessageNoSignature serializes all elements of the message except for +// the signature. This allows code reuse between message serialization, and +// signing and verifying these message contents. +func (msg *MsgMixPR) writeMessageNoSignature(op string, w io.Writer, pver uint32) error { + err := writeElements(w, &msg.Identity, msg.Expiry, msg.MixAmount) + if err != nil { + return err + } + + err = WriteVarString(w, pver, msg.ScriptClass) + if err != nil { + return err + } + + err = writeElements(w, msg.TxVersion, msg.LockTime, msg.MessageCount, + msg.InputValue) + if err != nil { + return err + } + + // Limit to max UTXOs per message. + count := len(msg.UTXOs) + if count > MaxMixPRUTXOs { + msg := fmt.Sprintf("too many UTXOs in message [%v]", count) + return messageError(op, ErrTooManyMixPRUTXOs, msg) + } + + err = WriteVarInt(w, pver, uint64(count)) + if err != nil { + return err + } + for i := range msg.UTXOs { + utxo := &msg.UTXOs[i] + + err := WriteOutPoint(w, pver, msg.TxVersion, &utxo.OutPoint) + if err != nil { + return err + } + + if l := len(utxo.Script); l > MaxMixPRUTXOScriptLen { + msg := fmt.Sprintf("UTXO script is too long [%v]", l) + return messageError(op, ErrVarBytesTooLong, msg) + } + err = WriteVarBytes(w, pver, utxo.Script) + if err != nil { + return err + } + + if l := len(utxo.PubKey); l > MaxMixPRUTXOPubKeyLen { + msg := fmt.Sprintf("UTXO public key is too long [%v]", l) + return messageError(op, ErrVarBytesTooLong, msg) + } + err = WriteVarBytes(w, pver, utxo.PubKey) + if err != nil { + return err + } + + if l := len(utxo.Signature); l > MaxMixPRUTXOSignatureLen { + msg := fmt.Sprintf("UTXO signature is too long [%v]", l) + return messageError(op, ErrVarBytesTooLong, msg) + } + err = WriteVarBytes(w, pver, utxo.Signature) + if err != nil { + return err + } + } + + var hasChange uint8 + if msg.Change != nil { + hasChange = 1 + } + err = writeElement(w, hasChange) + if err != nil { + return err + } + if msg.Change != nil { + err = writeTxOut(w, pver, msg.TxVersion, msg.Change) + if err != nil { + return err + } + } + + return nil +} + +// WriteSigned writes a tag identifying the message data, followed by all +// message fields excluding the signature. This is the data committed to when +// the message is signed. +func (msg *MsgMixPR) WriteSigned(w io.Writer) error { + const op = "MsgMixPR.WriteSigned" + + err := WriteVarString(w, MixVersion, CmdMixPR+"-sig") + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, MixVersion) + if err != nil { + return err + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgMixPR) Command() string { + return CmdMixPR +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgMixPR) MaxPayloadLength(pver uint32) uint32 { + if pver < MixVersion { + return 0 + } + + // PR contains a transaction, and the maximum transaction + // serialization size is limited to the max block payload. + return MaxBlockPayload +} + +// Hash returns the hash of the serialized message. +func (msg *MsgMixPR) Hash() chainhash.Hash { + return mustHash(msg, MixVersion) +} + +// GetIdentity returns the message sender's public key identity. +func (msg *MsgMixPR) GetIdentity() []byte { + return msg.Identity[:] +} + +// GetSignature returns the message signature. +func (msg *MsgMixPR) GetSignature() []byte { + return msg.Signature[:] +} + +// Expires returns the block height at which the message expires. +func (msg *MsgMixPR) Expires() int64 { + return msg.Expiry +} + +// PrevMsgs always returns nil. Pair request messages are the initial message. +func (msg *MsgMixPR) PrevMsgs() []chainhash.Hash { + return nil +} + +// Sid always returns nil. Pair request messages do not belong to a session. +func (msg *MsgMixPR) Sid() []byte { + return nil +} + +// GetRun always returns 0. Pair request messages do not belong to a session. +func (msg *MsgMixPR) GetRun() uint32 { + return 0 +} + +// NewMsgMixPR returns a new mixpr message that conforms to the Message +// interface using the passed parameters and defaults for the remaining fields. +func NewMsgMixPR(identity [33]byte, expiry int64, mixAmount int64, + scriptClass string, txVersion uint16, lockTime, messageCount uint32, + inputValue int64, utxos []MixPRUTXO, change *TxOut) (*MsgMixPR, error) { + + const op = "NewMsgMixPR" + lenScriptClass := len(scriptClass) + if lenScriptClass > MaxMixPRScriptClassLen { + msg := fmt.Sprintf("script class length is too long "+ + "[len %d, max %d]", lenScriptClass, + MaxMixPRScriptClassLen) + return nil, messageError(op, ErrMixPRScriptClassTooLong, msg) + } + + if !isStrictAscii(scriptClass) { + msg := "individual initial state type is not strict ASCII" + return nil, messageError(op, ErrMalformedStrictString, msg) + } + + if len(utxos) > MaxMixPRUTXOs { + msg := fmt.Sprintf("too many input UTXOs [len %d, max %d]", + len(utxos), MaxMixPRUTXOs) + return nil, messageError(op, ErrTooManyMixPRUTXOs, msg) + } + + msg := &MsgMixPR{ + Identity: identity, + Expiry: expiry, + MixAmount: mixAmount, + ScriptClass: scriptClass, + TxVersion: txVersion, + LockTime: lockTime, + MessageCount: messageCount, + InputValue: inputValue, + UTXOs: utxos, + Change: change, + } + return msg, nil +} diff --git a/wire/msgmixpr_test.go b/wire/msgmixpr_test.go new file mode 100644 index 0000000000..009a7a4cfd --- /dev/null +++ b/wire/msgmixpr_test.go @@ -0,0 +1,103 @@ +// 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 wire + +import ( + "bytes" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/decred/dcrd/chaincfg/chainhash" +) + +func TestMixPRWire(t *testing.T) { + pver := MixVersion + + repeat := func(b byte, count int) []byte { + s := make([]byte, count) + for i := range s { + s[i] = b + } + return s + } + rhash := func(b byte) chainhash.Hash { + var h chainhash.Hash + for i := range h { + h[i] = b + } + return h + } + + // Create a fictitious message with easily-distinguishable fields. + + var sig [64]byte + copy(sig[:], repeat(0x80, 64)) + + var id [33]byte + copy(id[:], repeat(0x81, 33)) + + const expiry = int64(0x0282828282828282) + const mixAmount = int64(0x0383838383838383) + const sc = "P2PKH-secp256k1-v0" + const txVersion = uint16(0x8484) + const lockTime = uint32(0x85858585) + const messageCount = uint32(0x86868686) + const inputValue = int64(0x0787878787878787) + + utxos := []MixPRUTXO{ + { + OutPoint: OutPoint{ + Hash: rhash(0x88), + Index: 0x89898989, + Tree: 0x0A, + }, + Script: []byte{}, + PubKey: repeat(0x8B, 33), + Signature: repeat(0x8C, 64), + }, + { + OutPoint: OutPoint{ + Hash: rhash(0x8D), + Index: 0x8E8E8E8E, + Tree: 0x0F, + }, + Script: repeat(0x90, 25), + PubKey: repeat(0x91, 33), + Signature: repeat(0x92, 64), + }, + } + + const changeValue = int64(0x1393939393939393) + pkScript := repeat(0x94, 25) + change := NewTxOut(changeValue, pkScript) + + pr, err := NewMsgMixPR(id, expiry, mixAmount, sc, txVersion, lockTime, + messageCount, inputValue, utxos, change) + if err != nil { + t.Fatal(err) + } + pr.Signature = sig + + buf := new(bytes.Buffer) + err = pr.BtcEncode(buf, pver) + if err != nil { + t.Fatal(err) + } + + decodedPR := new(MsgMixPR) + err = decodedPR.BtcDecode(bytes.NewReader(buf.Bytes()), pver) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(pr, decodedPR) { + t.Errorf("BtcDecode got: %s want: %s", + spew.Sdump(decodedPR), spew.Sdump(pr)) + } else { + t.Logf("bytes: %x", buf.Bytes()) + t.Logf("spew: %s", spew.Sdump(decodedPR)) + } +} diff --git a/wire/msgmixrs.go b/wire/msgmixrs.go new file mode 100644 index 0000000000..d4d8863f1c --- /dev/null +++ b/wire/msgmixrs.go @@ -0,0 +1,223 @@ +// 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 wire + +import ( + "fmt" + "io" + + "github.com/decred/dcrd/chaincfg/chainhash" +) + +// MsgMixRS reveals secrets of a failed mix run. After secrets are exposed, +// peers can determine which peers (if any) misbehaved and remove them from the +// next run in the session. +// +// It implements the Message interface. +type MsgMixRS struct { + Signature [64]byte + Identity [33]byte + SessionID [32]byte + Expiry int64 + Run uint32 + Seed [32]byte + SR [][]byte + M [][]byte +} + +// BtcDecode decodes r using the Decred protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgMixRS) BtcDecode(r io.Reader, pver uint32) error { + const op = "MsgMixRS.BtcDecode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := readElements(r, &msg.Signature, &msg.Identity, &msg.SessionID, + &msg.Expiry, &msg.Run, &msg.Seed) + if err != nil { + return err + } + + var numSRs uint64 + err = readElement(r, &numSRs) + if err != nil { + return err + } + if numSRs > MaxMixMcount { + msg := fmt.Sprintf("too many total mixed messages [%v]", numSRs) + return messageError(op, ErrInvalidMsg, msg) + } + msg.SR = make([][]byte, numSRs) + for i := uint64(0); i < numSRs; i++ { + sr, err := ReadVarBytes(r, pver, MaxMixFieldValLen, "SR") + if err != nil { + return err + } + msg.SR[i] = sr + } + + var numMs uint64 + err = readElement(r, &numMs) + if err != nil { + return err + } + if numMs > MaxMixMcount { + msg := fmt.Sprintf("too many total mixed messages [%v]", numMs) + return messageError(op, ErrInvalidMsg, msg) + } + msg.M = make([][]byte, numMs) + for i := uint64(0); i < numMs; i++ { + m, err := ReadVarBytes(r, pver, MaxMixFieldValLen, "M") + if err != nil { + return err + } + msg.M[i] = m + } + + return nil +} + +// BtcEncode encodes the receiver to w using the Decred protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgMixRS) BtcEncode(w io.Writer, pver uint32) error { + const op = "MsgMixRS.BtcEncode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := writeElement(w, &msg.Signature) + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, pver) + if err != nil { + return err + } + + return nil +} + +// writeMessageNoSignature serializes all elements of the message except for +// the signature. This allows code reuse between message serialization, and +// signing and verifying these message contents. +func (msg *MsgMixRS) writeMessageNoSignature(op string, w io.Writer, pver uint32) error { + err := writeElements(w, &msg.Identity, &msg.SessionID, msg.Expiry, + msg.Run, &msg.Seed) + if err != nil { + return err + } + + err = writeElement(w, uint64(len(msg.SR))) + if err != nil { + return err + } + for _, sr := range msg.SR { + err := WriteVarBytes(w, pver, sr) + if err != nil { + return err + } + } + + err = writeElement(w, uint64(len(msg.M))) + if err != nil { + return err + } + for _, m := range msg.M { + err := WriteVarBytes(w, pver, m) + if err != nil { + return err + } + } + + return nil +} + +// WriteSigned writes a tag identifying the message data, followed by all +// message fields excluding the signature. This is the data committed to when +// the message is signed. +func (msg *MsgMixRS) WriteSigned(w io.Writer) error { + const op = "MsgMixRS.WriteSigned" + + err := WriteVarString(w, MixVersion, CmdMixRS+"-sig") + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, MixVersion) + if err != nil { + return err + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgMixRS) Command() string { + return CmdMixRS +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgMixRS) MaxPayloadLength(pver uint32) uint32 { + return 67773 +} + +// Hash returns the hash of the serialized message. +func (msg *MsgMixRS) Hash() chainhash.Hash { + return mustHash(msg, MixVersion) +} + +// GetIdentity returns the message sender's public key identity. +func (msg *MsgMixRS) GetIdentity() []byte { + return msg.Identity[:] +} + +// GetSignature returns the message signature. +func (msg *MsgMixRS) GetSignature() []byte { + return msg.Signature[:] +} + +// Expires returns the block height at which the message expires. +func (msg *MsgMixRS) Expires() int64 { + return msg.Expiry +} + +// PrevMsgs returns the previous DC messages seen by the peer. +func (msg *MsgMixRS) PrevMsgs() []chainhash.Hash { + return nil // XXX +} + +// Sid returns the session ID. +func (msg *MsgMixRS) Sid() []byte { + return msg.SessionID[:] +} + +// GetRun returns the run number. +func (msg *MsgMixRS) GetRun() uint32 { + return msg.Run +} + +// NewMsgMixRS returns a new mixke message that conforms to the Message +// interface using the passed parameters and defaults for the remaining fields. +func NewMsgMixRS(identity [33]byte, sid [32]byte, expiry int64, run uint32, + seed [32]byte, srMsgs [][]byte, dcMsgs [][]byte) *MsgMixRS { + + return &MsgMixRS{ + Identity: identity, + SessionID: sid, + Expiry: expiry, + Run: run, + Seed: seed, + SR: srMsgs, + M: dcMsgs, + } +} diff --git a/wire/msgmixrs_test.go b/wire/msgmixrs_test.go new file mode 100644 index 0000000000..90d0c4fbd4 --- /dev/null +++ b/wire/msgmixrs_test.go @@ -0,0 +1,75 @@ +// 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 wire + +import ( + "bytes" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" +) + +func TestMixRSWire(t *testing.T) { + pver := MixVersion + + repeat := func(b byte, count int) []byte { + s := make([]byte, count) + for i := range s { + s[i] = b + } + return s + } + + // Create a fictitious message with easily-distinguishable fields. + + var sig [64]byte + copy(sig[:], repeat(0x80, 64)) + + var id [33]byte + copy(id[:], repeat(0x81, 33)) + + var sid [32]byte + copy(sid[:], repeat(0x82, 32)) + + const expiry = int64(0x0383838383838383) + const run = uint32(0x84848484) + + var seed [32]byte + copy(seed[:], repeat(0x85, 32)) + + sr := make([][]byte, 4) + for b := byte(0x86); b < 0x8A; b++ { + sr[b-0x86] = repeat(b, 32) + } + + m := make([][]byte, 4) + for b := byte(0x8A); b < 0x8E; b++ { + m[b-0x8A] = repeat(b, 32) + } + + rs := NewMsgMixRS(id, sid, expiry, run, seed, sr, m) + rs.Signature = sig + + buf := new(bytes.Buffer) + err := rs.BtcEncode(buf, pver) + if err != nil { + t.Fatal(err) + } + + decodedRS := new(MsgMixRS) + err = decodedRS.BtcDecode(bytes.NewReader(buf.Bytes()), pver) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(rs, decodedRS) { + t.Errorf("BtcDecode got: %s want: %s", + spew.Sdump(decodedRS), spew.Sdump(rs)) + } else { + t.Logf("bytes: %x", buf.Bytes()) + t.Logf("spew: %s", spew.Sdump(decodedRS)) + } +} diff --git a/wire/msgmixsr.go b/wire/msgmixsr.go new file mode 100644 index 0000000000..d1a310d70c --- /dev/null +++ b/wire/msgmixsr.go @@ -0,0 +1,294 @@ +// 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 wire + +import ( + "fmt" + "io" + + "github.com/decred/dcrd/chaincfg/chainhash" +) + +const ( + // MaxMixMcount is the maximum number of mixed messages that are allowed + // in a single mix. This restricts the total allowed size of the slot + // reservation and XOR DC-net matrices. + MaxMixMcount = 1024 // XXX: PNOOMA + + // MaxMixKPCount is the maximum number of peers allowed together in a + // single mix. This restricts the total size of the slot reservation + // and XOR DC-net matrices. + MaxMixKPCount = 512 // XXX: PNOOMA + + // MaxMixFieldValLen is the maximum number of bytes allowed to represent + // a value in the slot reservation mix bounded by the field prime. + MaxMixFieldValLen = 32 +) + +// MsgMixSR is the slot reservation broadcast. It implements the Message +// interface. +type MsgMixSR struct { + Signature [64]byte + Identity [33]byte + SessionID [32]byte + Expiry int64 + Run uint32 + DCMix [][][]byte // mcount-by-peers matrix of field numbers + SeenCTs []chainhash.Hash +} + +// BtcDecode decodes r using the Decred protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgMixSR) BtcDecode(r io.Reader, pver uint32) error { + const op = "MsgMixSR.BtcDecode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := readElements(r, &msg.Signature, &msg.Identity, &msg.SessionID, + &msg.Expiry, &msg.Run) + if err != nil { + return err + } + + // Read the DCMix + mcount, err := ReadVarInt(r, pver) + if err != nil { + return err + } + kpcount, err := ReadVarInt(r, pver) + if err != nil { + return err + } + if mcount == 0 { + msg := fmt.Sprintf("too few mixed messages [%v]", mcount) + return messageError(op, ErrInvalidMsg, msg) + } + if mcount > MaxMixMcount { + msg := fmt.Sprintf("too many total mixed messages [%v]", mcount) + return messageError(op, ErrInvalidMsg, msg) + } + if mcount == 0 { + msg := fmt.Sprintf("too few mixing peers [%v]", kpcount) + return messageError(op, ErrInvalidMsg, msg) + } + if kpcount > MaxMixKPCount { + msg := fmt.Sprintf("too many mixing peers [%v]", kpcount) + return messageError(op, ErrInvalidMsg, msg) + } + dcmix := make([][][]byte, mcount) + for i := range dcmix { + dcmix[i] = make([][]byte, kpcount) + for j := range dcmix[i] { + v, err := ReadVarBytes(r, pver, MaxMixFieldValLen, "fieldval") + if err != nil { + return err + } + dcmix[i][j] = v + } + } + msg.DCMix = dcmix + + count, err := ReadVarInt(r, pver) + if err != nil { + return err + } + if count > MaxPrevMixMsgs { + msg := fmt.Sprintf("too many previous referenced messages [%v]", count) + return messageError(op, ErrTooManyPrevMixMsgs, msg) + } + + seen := make([]chainhash.Hash, count) + for i := range seen { + err := readElement(r, &seen[i]) + if err != nil { + return err + } + } + msg.SeenCTs = seen + + return nil +} + +// BtcEncode encodes the receiver to w using the Decred protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgMixSR) BtcEncode(w io.Writer, pver uint32) error { + const op = "MsgMixSR.BtcEncode" + if pver < MixVersion { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + err := writeElement(w, &msg.Signature) + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, pver) + if err != nil { + return err + } + + return nil +} + +// writeMessageNoSignature serializes all elements of the message except for +// the signature. This allows code reuse between message serialization, and +// signing and verifying these message contents. +func (msg *MsgMixSR) writeMessageNoSignature(op string, w io.Writer, pver uint32) error { + err := writeElements(w, &msg.Identity, &msg.SessionID, msg.Expiry, + msg.Run) + if err != nil { + return err + } + + // Write the DCMix + mcount := len(msg.DCMix) + if mcount == 0 { + msg := fmt.Sprintf("too few mixed messages [%v]", mcount) + return messageError(op, ErrInvalidMsg, msg) + } + if mcount > MaxMixMcount { + msg := fmt.Sprintf("too many total mixed messages [%v]", mcount) + return messageError(op, ErrInvalidMsg, msg) + } + kpcount := len(msg.DCMix[0]) + if kpcount == 0 { + msg := fmt.Sprintf("too few mixing peers [%v]", kpcount) + return messageError(op, ErrInvalidMsg, msg) + } + if kpcount > MaxMixKPCount { + msg := fmt.Sprintf("too many mixing peers [%v]", kpcount) + return messageError(op, ErrInvalidMsg, msg) + } + err = WriteVarInt(w, pver, uint64(mcount)) + if err != nil { + return err + } + err = WriteVarInt(w, pver, uint64(kpcount)) + if err != nil { + return err + } + for i := range msg.DCMix { + if len(msg.DCMix[i]) != kpcount { + msg := "invalid matrix dimensions" + return messageError(op, ErrInvalidMsg, msg) + } + for j := range msg.DCMix[i] { + v := msg.DCMix[i][j] + if len(v) > MaxMixFieldValLen { + msg := "value exceeds bytes necessary to represent number in field" + return messageError(op, ErrInvalidMsg, msg) + } + err := WriteVarBytes(w, pver, v) + if err != nil { + return err + } + } + } + + count := len(msg.SeenCTs) + if count > MaxPrevMixMsgs { + msg := fmt.Sprintf("too many previous referenced messages [%v]", count) + return messageError(op, ErrTooManyPrevMixMsgs, msg) + } + + err = WriteVarInt(w, pver, uint64(count)) + if err != nil { + return err + } + for i := range msg.SeenCTs { + err = writeElement(w, &msg.SeenCTs[i]) + if err != nil { + return err + } + } + + return nil +} + +// WriteSigned writes a tag identifying the message data, followed by all +// message fields excluding the signature. This is the data committed to when +// the message is signed. +func (msg *MsgMixSR) WriteSigned(w io.Writer) error { + const op = "MsgMixSR.WriteSigned" + + err := WriteVarString(w, MixVersion, CmdMixSR+"-sig") + if err != nil { + return err + } + + err = msg.writeMessageNoSignature(op, w, MixVersion) + if err != nil { + return err + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgMixSR) Command() string { + return CmdMixSR +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgMixSR) MaxPayloadLength(pver uint32) uint32 { + return 17318038 +} + +// Hash returns the hash of the serialized message. +func (msg *MsgMixSR) Hash() chainhash.Hash { + return mustHash(msg, MixVersion) +} + +// GetIdentity returns the message sender's public key identity. +func (msg *MsgMixSR) GetIdentity() []byte { + return msg.Identity[:] +} + +// GetSignature returns the message signature. +func (msg *MsgMixSR) GetSignature() []byte { + return msg.Signature[:] +} + +// Expires returns the block height at which the message expires. +func (msg *MsgMixSR) Expires() int64 { + return msg.Expiry +} + +// PrevMsgs returns the previous CT messages seen by the peer. +func (msg *MsgMixSR) PrevMsgs() []chainhash.Hash { + return msg.SeenCTs +} + +// Sid returns the session ID. +func (msg *MsgMixSR) Sid() []byte { + return msg.SessionID[:] +} + +// GetRun returns the run number. +func (msg *MsgMixSR) GetRun() uint32 { + return msg.Run +} + +// NewMsgMixSR returns a new mixsr message that conforms to the Message +// interface using the passed parameters and defaults for the remaining fields. +func NewMsgMixSR(identity [33]byte, sid [32]byte, expires int64, run uint32, + dcmix [][][]byte, seenCTs []chainhash.Hash) *MsgMixSR { + + return &MsgMixSR{ + Identity: identity, + SessionID: sid, + Expiry: expires, + Run: run, + DCMix: dcmix, + SeenCTs: seenCTs, + } +} diff --git a/wire/msgmixsr_test.go b/wire/msgmixsr_test.go new file mode 100644 index 0000000000..18266b3ca9 --- /dev/null +++ b/wire/msgmixsr_test.go @@ -0,0 +1,82 @@ +// 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 wire + +import ( + "bytes" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/decred/dcrd/chaincfg/chainhash" +) + +func TestMixSRWire(t *testing.T) { + pver := MixVersion + + repeat := func(b byte, count int) []byte { + s := make([]byte, count) + for i := range s { + s[i] = b + } + return s + } + + // Create a fictitious message with easily-distinguishable fields. + + var sig [64]byte + copy(sig[:], repeat(0x80, 64)) + + var id [33]byte + copy(id[:], repeat(0x81, 33)) + + var sid [32]byte + copy(sid[:], repeat(0x82, 32)) + + const expiry = int64(0x0383838383838383) + const run = uint32(0x84848484) + + mcount := 4 + kpcount := 4 + dcmix := make([][][]byte, mcount) + // will add 4x4 field numbers of incrementing repeating byte values to + // dcmix, ranging from 0x85 through 0x94 + b := byte(0x85) + for i := 0; i < mcount; i++ { + dcmix[i] = make([][]byte, kpcount) + for j := 0; j < kpcount; j++ { + dcmix[i][j] = repeat(b, 32) + b++ + } + } + + seenCTs := make([]chainhash.Hash, 4) + for b := byte(0x95); b < 0x99; b++ { + copy(seenCTs[b-0x95][:], repeat(b, 32)) + } + + sr := NewMsgMixSR(id, sid, expiry, run, dcmix, seenCTs) + sr.Signature = sig + + buf := new(bytes.Buffer) + err := sr.BtcEncode(buf, pver) + if err != nil { + t.Fatal(err) + } + + decodedSR := new(MsgMixSR) + err = decodedSR.BtcDecode(bytes.NewReader(buf.Bytes()), pver) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(sr, decodedSR) { + t.Errorf("BtcDecode got: %s want: %s", + spew.Sdump(decodedSR), spew.Sdump(sr)) + } else { + t.Logf("bytes: %x", buf.Bytes()) + t.Logf("spew: %s", spew.Sdump(decodedSR)) + } +} diff --git a/wire/protocol.go b/wire/protocol.go index a49ddf1e99..c2585d4380 100644 --- a/wire/protocol.go +++ b/wire/protocol.go @@ -17,7 +17,7 @@ const ( InitialProcotolVersion uint32 = 1 // ProtocolVersion is the latest protocol version this package supports. - ProtocolVersion uint32 = 9 + ProtocolVersion uint32 = 10 // NodeBloomVersion is the protocol version which added the SFNodeBloom // service flag (unused). @@ -51,6 +51,9 @@ const ( // RemoveRejectVersion is the protocol version which removes support for the // reject message. RemoveRejectVersion uint32 = 9 + + // MixVersion is the protocol version which adds peer-to-peer mixing. + MixVersion uint32 = 10 ) // ServiceFlag identifies services supported by a Decred peer.