diff --git a/Makefile b/Makefile index b6f9df22644b196178f6fa17222f586149198794..64cdc581e683cdbb63240888c31193b0fe10eb3d 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ update_release: GOFLAGS="" go get gitlab.com/xx_network/primitives@release GOFLAGS="" go get gitlab.com/elixxir/primitives@release GOFLAGS="" go get gitlab.com/xx_network/crypto@release - GOFLAGS="" go get gitlab.com/elixxir/crypto@release + GOFLAGS="" go get gitlab.com/elixxir/crypto@singleUseMultiPartRequest GOFLAGS="" go get gitlab.com/xx_network/comms@release GOFLAGS="" go get gitlab.com/elixxir/comms@release GOFLAGS="" go get gitlab.com/elixxir/ekv@master diff --git a/go.mod b/go.mod index 6a99bc114b871f87af2b56acb96e2ff603f2dda9..417152e5eba15cd54e8c92b4b6f92bca92a9e325 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( github.com/spf13/viper v1.7.1 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 gitlab.com/elixxir/comms v0.0.4-0.20220323190139-9ed75f3a8b2c - gitlab.com/elixxir/crypto v0.0.7-0.20220415180223-ec8d560270a1 - gitlab.com/elixxir/ekv v0.1.7 + gitlab.com/elixxir/crypto v0.0.7-0.20220418163058-a76028e93dd3 + gitlab.com/elixxir/ekv v0.1.6 gitlab.com/elixxir/primitives v0.0.3-0.20220330212736-cce83b5f948f gitlab.com/xx_network/comms v0.0.4-0.20220315161313-76acb14429ac gitlab.com/xx_network/crypto v0.0.5-0.20220317171841-084640957d71 diff --git a/go.sum b/go.sum index 8b274f94dffe0f1a5eb524a70567efb96e5e82cd..e763825e430744f3ed713446cbe28f812517f615 100644 --- a/go.sum +++ b/go.sum @@ -283,7 +283,6 @@ gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp0 gitlab.com/elixxir/crypto v0.0.7-0.20220222221347-95c7ae58da6b/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10= gitlab.com/elixxir/crypto v0.0.7-0.20220309234716-1ba339865787 h1:+qmsWov412+Yn7AKUhTbOcDgAydNXlNLPmFpO2W5LwY= gitlab.com/elixxir/crypto v0.0.7-0.20220309234716-1ba339865787/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10= -gitlab.com/elixxir/crypto v0.0.7-0.20220317172048-3de167bd9406/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10= gitlab.com/elixxir/crypto v0.0.7-0.20220325215559-7489d68d7714 h1:epnov8zyFWod14MUNtGHSbZCVSkZjN4NvoiBs1TgEV8= gitlab.com/elixxir/crypto v0.0.7-0.20220325215559-7489d68d7714/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10= gitlab.com/elixxir/crypto v0.0.7-0.20220325224306-705ce59288bb h1:WdlmG+KPaM2Pjo1EFiFFPYEVSMV64Di1CitQnXGWBOQ= @@ -298,14 +297,8 @@ gitlab.com/elixxir/crypto v0.0.7-0.20220331001626-1829e71edf56 h1:1HJHlRwh3dDbvw gitlab.com/elixxir/crypto v0.0.7-0.20220331001626-1829e71edf56/go.mod h1:JkByWX/TXCjdu6pRJsx+jwttbBGvlAljYSJMImDmt+4= gitlab.com/elixxir/crypto v0.0.7-0.20220406193349-d25222ea3c6e h1:P+E0+AdevTNWBdqf4+covcmTrRfe6rKPLtevFrjbKQA= gitlab.com/elixxir/crypto v0.0.7-0.20220406193349-d25222ea3c6e/go.mod h1:JkByWX/TXCjdu6pRJsx+jwttbBGvlAljYSJMImDmt+4= -gitlab.com/elixxir/crypto v0.0.7-0.20220414175442-6d2304df43d7 h1:xEE795GeUyQaa4lRAI8IjyH31glm2OvFgzY9eiMEr1M= -gitlab.com/elixxir/crypto v0.0.7-0.20220414175442-6d2304df43d7/go.mod h1:JkByWX/TXCjdu6pRJsx+jwttbBGvlAljYSJMImDmt+4= -gitlab.com/elixxir/crypto v0.0.7-0.20220414225314-6f3eb9c073a5 h1:yw3G8ZEiWu2eSZWRQmj6nBhiJIYK3Cw2MJzDPkNHYVA= -gitlab.com/elixxir/crypto v0.0.7-0.20220414225314-6f3eb9c073a5/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10= -gitlab.com/elixxir/crypto v0.0.7-0.20220415172207-7de5e3cdb340 h1:f1JsT60cKFXcHPoaOD1ohIOA22FQd42vbKjF9wrKfNs= -gitlab.com/elixxir/crypto v0.0.7-0.20220415172207-7de5e3cdb340/go.mod h1:JkByWX/TXCjdu6pRJsx+jwttbBGvlAljYSJMImDmt+4= -gitlab.com/elixxir/crypto v0.0.7-0.20220415180223-ec8d560270a1 h1:stzHgpYxHQu3JgvQu5Vr0hr4nzJUk5CxspZEoNx26eQ= -gitlab.com/elixxir/crypto v0.0.7-0.20220415180223-ec8d560270a1/go.mod h1:JkByWX/TXCjdu6pRJsx+jwttbBGvlAljYSJMImDmt+4= +gitlab.com/elixxir/crypto v0.0.7-0.20220418163058-a76028e93dd3 h1:tYr7CjBj3p4tmUmvEmsX5n0m0GsfG8eJOu1YLoBHE2g= +gitlab.com/elixxir/crypto v0.0.7-0.20220418163058-a76028e93dd3/go.mod h1:JkByWX/TXCjdu6pRJsx+jwttbBGvlAljYSJMImDmt+4= gitlab.com/elixxir/ekv v0.1.6 h1:M2hUSNhH/ChxDd+s8xBqSEKgoPtmE6hOEBqQ73KbN6A= gitlab.com/elixxir/ekv v0.1.6/go.mod h1:e6WPUt97taFZe5PFLPb1Dupk7tqmDCTQu1kkstqJvw4= gitlab.com/elixxir/ekv v0.1.7 h1:OW2z+N4QCqqMFzouAwFTWWMKz0Y/PDhyYReN7gQ5NiQ= diff --git a/single/cypher.go b/single/cypher.go index 44a5c96287528e6ae02b9afca5699f97dc5ebd14..1e51d5ecb8acabebd274b6c8cb4ce06a9b9d6d24 100644 --- a/single/cypher.go +++ b/single/cypher.go @@ -18,15 +18,21 @@ import ( "gitlab.com/elixxir/primitives/format" ) +type newKeyFn func(dhKey *cyclic.Int, keyNum uint64) []byte +type newFpFn func(dhKey *cyclic.Int, keyNum uint64) format.Fingerprint + // makeCyphers generates all fingerprints for a given number of messages. -func makeCyphers(dhKey *cyclic.Int, messageCount uint8) []cypher { +func makeCyphers(dhKey *cyclic.Int, messageCount uint8, newKey newKeyFn, + newFp newFpFn) []cypher { cypherList := make([]cypher, messageCount) for i := uint8(0); i < messageCount; i++ { cypherList[i] = cypher{ - dhKey: dhKey, - num: i, + dhKey: dhKey, + num: i, + newKey: newKey, + newFp: newFp, } } @@ -34,16 +40,18 @@ func makeCyphers(dhKey *cyclic.Int, messageCount uint8) []cypher { } type cypher struct { - dhKey *cyclic.Int - num uint8 + dhKey *cyclic.Int + num uint8 + newKey newKeyFn // Function used to create new key + newFp newFpFn // Function used to create new fingerprint } func (rk *cypher) getKey() []byte { - return singleUse.NewResponseKey(rk.dhKey, uint64(rk.num)) + return rk.newKey(rk.dhKey, uint64(rk.num)) } func (rk *cypher) GetFingerprint() format.Fingerprint { - return singleUse.NewResponseFingerprint(rk.dhKey, uint64(rk.num)) + return rk.newFp(rk.dhKey, uint64(rk.num)) } func (rk *cypher) Encrypt(rp message.ResponsePart) ( diff --git a/single/listener.go b/single/listener.go index ce2bffbb019578c310d684dc3190a6bb24a24db5..c41d5fde017e715cc4145e8fa1fb952e061d01cd 100644 --- a/single/listener.go +++ b/single/listener.go @@ -14,7 +14,7 @@ import ( ) type Receiver interface { - Callback(*Request, receptionID.EphemeralIdentity, rounds.Round) + Callback(*Request, receptionID.EphemeralIdentity, []rounds.Round) } type Listener interface { @@ -74,7 +74,7 @@ func (l *listener) Process(ecrMsg format.Message, // Generate DH key and symmetric key senderPubkey := requestMsg.GetPubKey(l.grp) dhKey := l.grp.Exp(senderPubkey, l.myPrivKey, l.grp.NewInt(1)) - key := singleUse.NewTransmitKey(dhKey) + key := singleUse.NewRequestKey(dhKey) // Verify the MAC if !singleUse.VerifyMAC(key, requestMsg.GetPayload(), ecrMsg.GetMac()) { @@ -95,20 +95,56 @@ func (l *listener) Process(ecrMsg format.Message, return } - used := uint32(0) - - r := Request{ - sender: payload.GetRecipientID(requestMsg.GetPubKey(l.grp)), - senderPubKey: senderPubkey, - dhKey: dhKey, - tag: l.tag, - maxParts: 0, - used: &used, - requestPayload: payload.GetContents(), - net: l.net, + cbFunc := func(payloadContents []byte, rounds []rounds.Round) { + used := uint32(0) + + r := Request{ + sender: payload.GetRecipientID(requestMsg.GetPubKey(l.grp)), + senderPubKey: senderPubkey, + dhKey: dhKey, + tag: l.tag, + maxParts: payload.GetMaxResponseParts(), + used: &used, + requestPayload: payloadContents, + net: l.net, + } + + go l.cb.Callback(&r, receptionID, rounds) } - go l.cb.Callback(&r, receptionID, round) + if numParts := payload.GetNumParts(); numParts > 1 { + c := message.NewCollator(numParts) + _, _, err = c.Collate(payload) + if err != nil { + + return + } + cyphers := makeCyphers(dhKey, numParts, + singleUse.NewRequestPartKey, singleUse.NewRequestPartFingerprint) + ridCollector := newRoundIdCollector(int(numParts)) + for i, cy := range cyphers { + key = singleUse.NewRequestPartKey(dhKey, uint64(i+1)) + fp = singleUse.NewRequestPartFingerprint(dhKey, uint64(i+1)) + p := &requestPartProcessor{ + myId: l.myId, + tag: l.tag, + cb: cbFunc, + c: c, + cy: cy, + roundIDs: ridCollector, + } + err = l.net.AddFingerprint(l.myId, fp, p) + if err != nil { + jww.ERROR.Printf("Failed to add fingerprint for request part "+ + "%d of %d (%s): %+v", i, numParts, l.tag, err) + return + } + } + + l.net.CheckInProgressMessages() + } else { + cbFunc(payload.GetContents(), []rounds.Round{round}) + } } func (l *listener) Stop() { diff --git a/single/message/collator.go b/single/message/collator.go index d2eff3a366f657e1d21f3c031cabd74a3b6f0850..9e59eb9e95f00b5b4ec8b0550bf403523e130cec 100644 --- a/single/message/collator.go +++ b/single/message/collator.go @@ -9,10 +9,9 @@ import ( // Error messages. const ( // Collate - errUnmarshalResponsePart = "failed to unmarshal response payload: %+v" - errMaxParts = "max number of parts reported by payload %d is larger than collator expected (%d)" - errPartOutOfRange = "payload part number %d greater than max number of expected parts (%d)" - errPartExists = "a payload for the part number %d already exists in the list" + errMaxParts = "max number of parts reported by payload %d is larger than collator expected (%d)" + errPartOutOfRange = "payload part number %d greater than max number of expected parts (%d)" + errPartExists = "a payload for the part number %d already exists in the list" ) // Initial value of the Collator maxNum that indicates it has yet to be set. @@ -26,6 +25,17 @@ type Collator struct { sync.Mutex } +type Part interface { + // GetNumParts returns the total number of parts in the message. + GetNumParts() uint8 + + // GetPartNum returns the index of this part in the message. + GetPartNum() uint8 + + // GetContents returns the contents of the message part. + GetContents() []byte +} + // NewCollator generates an empty list of payloads to fit the max number of // possible messages. maxNum is set to indicate that it is not yet set. func NewCollator(messageCount uint8) *Collator { @@ -38,38 +48,33 @@ func NewCollator(messageCount uint8) *Collator { // Collate collects message payload parts. Once all parts are received, the full // collated payload is returned along with true. Otherwise, returns false. -func (c *Collator) Collate(payloadBytes []byte) ([]byte, bool, error) { - payload, err := UnmarshalResponsePart(payloadBytes) - if err != nil { - return nil, false, errors.Errorf(errUnmarshalResponsePart, err) - } - +func (c *Collator) Collate(part Part) ([]byte, bool, error) { c.Lock() defer c.Unlock() // If this is the first message received, then set the max number of // messages expected to be received off its max number of parts if c.maxNum == unsetCollatorMax { - if int(payload.GetNumParts()) > len(c.payloads) { + if int(part.GetNumParts()) > len(c.payloads) { return nil, false, errors.Errorf( - errMaxParts, payload.GetNumParts(), len(c.payloads)) + errMaxParts, part.GetNumParts(), len(c.payloads)) } - c.maxNum = int(payload.GetNumParts()) + c.maxNum = int(part.GetNumParts()) } // Make sure that the part number is within the expected number of parts - if int(payload.GetPartNum()) >= c.maxNum { + if int(part.GetPartNum()) >= c.maxNum { return nil, false, - errors.Errorf(errPartOutOfRange, payload.GetPartNum(), c.maxNum) + errors.Errorf(errPartOutOfRange, part.GetPartNum(), c.maxNum) } // Make sure no payload with the same part number exists - if c.payloads[payload.GetPartNum()] != nil { - return nil, false, errors.Errorf(errPartExists, payload.GetPartNum()) + if c.payloads[part.GetPartNum()] != nil { + return nil, false, errors.Errorf(errPartExists, part.GetPartNum()) } // Add the payload to the list - c.payloads[payload.GetPartNum()] = payload.GetContents() + c.payloads[part.GetPartNum()] = part.GetContents() c.count++ // Return false if not all messages have been received diff --git a/single/message/collator_test.go b/single/message/collator_test.go index 9ad8312525a246fd14e9bd21f67701cd0125af91..617697db480b6dd6f6375757b326952db17021f9 100644 --- a/single/message/collator_test.go +++ b/single/message/collator_test.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "reflect" - "strings" "testing" ) @@ -29,16 +28,17 @@ func TestNewCollator(t *testing.T) { func TestCollator_Collate(t *testing.T) { messageCount := 16 msgPayloadSize := 2 - msgParts := map[int]ResponsePart{} + msgParts := map[int]mockPart{} expectedData := make([]byte, messageCount*msgPayloadSize) copy(expectedData, "This is the expected final data.") buff := bytes.NewBuffer(expectedData) for i := 0; i < messageCount; i++ { - msgParts[i] = NewResponsePart(msgPayloadSize + 5) - msgParts[i].SetNumParts(uint8(messageCount)) - msgParts[i].SetPartNum(uint8(i)) - msgParts[i].SetContents(buff.Next(msgPayloadSize)) + msgParts[i] = mockPart{ + numParts: uint8(messageCount), + partNum: uint8(i), + contents: buff.Next(msgPayloadSize), + } } c := NewCollator(uint8(messageCount)) @@ -51,7 +51,7 @@ func TestCollator_Collate(t *testing.T) { var err error var collated bool - fullPayload, collated, err = c.Collate(part.Marshal()) + fullPayload, collated, err = c.Collate(part) if err != nil { t.Errorf("Collate returned an error for part #%d: %+v", j, err) } @@ -71,31 +71,12 @@ func TestCollator_Collate(t *testing.T) { } } -// Error path: the byte slice cannot be unmarshaled. -func TestCollator_collate_UnmarshalError(t *testing.T) { - payloadBytes := []byte{1} - c := NewCollator(1) - payload, collated, err := c.Collate(payloadBytes) - expectedErr := strings.Split(errUnmarshalResponsePart, "%")[0] - - if err == nil || !strings.Contains(err.Error(), expectedErr) { - t.Errorf("Collate failed to return an error for failing to "+ - "unmarshal the payload.\nexpected: %s\nreceived: %+v", - expectedErr, err) - } - - if payload != nil || collated { - t.Errorf("Collate signaled the payload was collated on error."+ - "\npayload: %+v\ncollated: %+v", payload, collated) - } -} - // Error path: max reported parts by payload larger than set in Collator. func TestCollator_Collate_MaxPartsError(t *testing.T) { - payloadBytes := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + p := mockPart{0xFF, 0xFF, []byte{0xFF, 0xFF, 0xFF}} messageCount := uint8(1) c := NewCollator(messageCount) - _, _, err := c.Collate(payloadBytes) + _, _, err := c.Collate(p) expectedErr := fmt.Sprintf(errMaxParts, 0xFF, messageCount) if err == nil || err.Error() != expectedErr { @@ -107,11 +88,11 @@ func TestCollator_Collate_MaxPartsError(t *testing.T) { // Error path: the message part number is greater than the max number of parts. func TestCollator_Collate_PartNumTooLargeError(t *testing.T) { - payloadBytes := []byte{25, 5, 5, 5, 5} - partNum := uint8(5) - c := NewCollator(partNum) - _, _, err := c.Collate(payloadBytes) - expectedErr := fmt.Sprintf(errPartOutOfRange, partNum, c.maxNum) + p := mockPart{35, 5, []byte{5, 5, 5}} + messageCount := uint8(1) + c := NewCollator(messageCount) + _, _, err := c.Collate(p) + expectedErr := fmt.Sprintf(errMaxParts, p.numParts, messageCount) if err == nil || err.Error() != expectedErr { t.Errorf("Collate failed to return the expected error when the part "+ @@ -122,17 +103,28 @@ func TestCollator_Collate_PartNumTooLargeError(t *testing.T) { // Error path: a message with the part number already exists. func TestCollator_Collate_PartExistsError(t *testing.T) { - payloadBytes := []byte{0, 1, 5, 0, 1, 20} + p := mockPart{5, 1, []byte{5, 0, 1, 20}} c := NewCollator(5) - _, _, err := c.Collate(payloadBytes) + _, _, err := c.Collate(p) if err != nil { t.Fatalf("Collate returned an error: %+v", err) } - expectedErr := fmt.Sprintf(errPartExists, payloadBytes[1]) + expectedErr := fmt.Sprintf(errPartExists, p.partNum) - _, _, err = c.Collate(payloadBytes) + _, _, err = c.Collate(p) if err == nil || err.Error() != expectedErr { t.Errorf("Collate failed to return an error when the part number "+ "already exists.\nexpected: %s\nreceived: %+v", expectedErr, err) } } + +type mockPart struct { + numParts uint8 + partNum uint8 + contents []byte +} + +func (m mockPart) GetNumParts() uint8 { return m.numParts } +func (m mockPart) GetPartNum() uint8 { return m.partNum } +func (m mockPart) GetContents() []byte { return m.contents } +func (m mockPart) Marshal() []byte { return append([]byte{m.numParts, m.partNum}, m.contents...) } diff --git a/single/message/request.go b/single/message/request.go index 75e83510926c7b1d559be6068b0bde1702cae34c..3519902e75b6804df0e4ca0108b396e5af79863b 100644 --- a/single/message/request.go +++ b/single/message/request.go @@ -277,11 +277,23 @@ func (mp RequestPayload) GetNumRequestParts() uint8 { return mp.numRequestParts[0] } +// GetNumParts returns the number of messages in the request. This function +// wraps GetMaxRequestParts so that RequestPayload adheres to the Part +// interface. +func (mp RequestPayload) GetNumParts() uint8 { + return mp.GetNumRequestParts() +} + // SetNumRequestParts sets the number of messages in the request. func (mp RequestPayload) SetNumRequestParts(num uint8) { copy(mp.numRequestParts, []byte{num}) } +// GetPartNum always returns 0 since it is the first message. +func (mp RequestPayload) GetPartNum() uint8 { + return 0 +} + // GetContents returns the payload's contents. func (mp RequestPayload) GetContents() []byte { return mp.contents[:binary.BigEndian.Uint16(mp.size)] diff --git a/single/message/requestPart.go b/single/message/requestPart.go new file mode 100644 index 0000000000000000000000000000000000000000..11d9f27b663cd15d500d7bf3321427c08f70c5d9 --- /dev/null +++ b/single/message/requestPart.go @@ -0,0 +1,138 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package message + +import ( + "encoding/binary" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" +) + +// Error messages. +const ( + // NewRequestPart + errReqPartPayloadSize = "[SU] Failed to create new single-use request " + + "message part: external payload size (%d) is smaller than the " + + "minimum message size for a request part (%d)." + + // UnmarshalRequestPart + errReqPartDataSize = "size of data (%d) must be at least %d" + + // RequestPart.SetContents + errReqPartContentsSize = "[SU] Failed to set contents of single-use " + + "request message part: size of the supplied contents (%d) is larger " + + "than the max message size (%d)." +) + +// Sizes of fields. +const ( + reqPartPartNumLen = 1 + reqPartSizeLen = 2 + reqPartMinSize = reqPartPartNumLen + reqPartSizeLen +) + +/* ++------------------------------+ +| cMix Message Contents | ++---------+---------+----------+ +| partNum | size | contents | +| 1 byte | 2 bytes | variable | ++---------+---------+----------+ +*/ + +type RequestPart struct { + data []byte // Serial of all contents + partNum []byte // Index of message in a series of messages + size []byte // Size of the contents + contents []byte // The encrypted contents +} + +// NewRequestPart generates a new request message part of the specified size. +func NewRequestPart(externalPayloadSize int) RequestPart { + if externalPayloadSize < reqPartMinSize { + jww.FATAL.Panicf( + errReqPartPayloadSize, externalPayloadSize, reqPartMinSize) + } + + rmp := mapRequestPart(make([]byte, externalPayloadSize)) + return rmp +} + +// GetRequestPartContentsSize returns the size of the contents for the given +// external payload size. +func GetRequestPartContentsSize(externalPayloadSize int) int { + return externalPayloadSize - reqPartMinSize +} + +// mapRequestPart builds a message part mapped to the passed in data. +// It is mapped by reference; a copy is not made. +func mapRequestPart(data []byte) RequestPart { + return RequestPart{ + data: data, + partNum: data[:reqPartPartNumLen], + size: data[reqPartPartNumLen:reqPartMinSize], + contents: data[reqPartMinSize:], + } +} + +// UnmarshalRequestPart converts a byte buffer into a request message part. +func UnmarshalRequestPart(b []byte) (RequestPart, error) { + if len(b) < reqPartMinSize { + return RequestPart{}, errors.Errorf( + errReqPartDataSize, len(b), reqPartMinSize) + } + return mapRequestPart(b), nil +} + +// Marshal returns the bytes of the message part. +func (m RequestPart) Marshal() []byte { + return m.data +} + +// GetPartNum returns the index of this part in the message. +func (m RequestPart) GetPartNum() uint8 { + return m.partNum[0] +} + +// SetPartNum sets the part number of the message. +func (m RequestPart) SetPartNum(num uint8) { + copy(m.partNum, []byte{num}) +} + +// GetNumParts always returns 0. It is here so that RequestPart adheres to th +// Part interface. +func (m RequestPart) GetNumParts() uint8 { + return 0 +} + +// GetContents returns the contents of the message part. +func (m RequestPart) GetContents() []byte { + return m.contents[:binary.BigEndian.Uint16(m.size)] +} + +// GetContentsSize returns the length of the contents. +func (m RequestPart) GetContentsSize() int { + return int(binary.BigEndian.Uint16(m.size)) +} + +// GetMaxContentsSize returns the max capacity of the contents. +func (m RequestPart) GetMaxContentsSize() int { + return len(m.contents) +} + +// SetContents sets the contents of the message part. Does not zero out previous +// contents. +func (m RequestPart) SetContents(contents []byte) { + if len(contents) > len(m.contents) { + jww.FATAL.Panicf(errReqPartContentsSize, len(contents), len(m.contents)) + } + + binary.BigEndian.PutUint16(m.size, uint16(len(contents))) + + copy(m.contents, contents) +} diff --git a/single/message/requestPart_test.go b/single/message/requestPart_test.go new file mode 100644 index 0000000000000000000000000000000000000000..75090ffd6823c5c6844fbaa60bea64057f380fca --- /dev/null +++ b/single/message/requestPart_test.go @@ -0,0 +1,183 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package message + +import ( + "bytes" + "fmt" + "math/rand" + "reflect" + "testing" +) + +// Happy path. +func Test_NewRequestPart(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + payloadSize := prng.Intn(2000) + expected := RequestPart{ + data: make([]byte, payloadSize), + partNum: make([]byte, reqPartPartNumLen), + size: make([]byte, reqPartSizeLen), + contents: make([]byte, payloadSize-reqPartMinSize), + } + + rmp := NewRequestPart(payloadSize) + + if !reflect.DeepEqual(expected, rmp) { + t.Errorf("NewRequestPart did not return the expected "+ + "RequestPart.\nexpected: %+v\nreceived: %+v", expected, rmp) + } +} + +// Error path: provided contents size is not large enough. +func Test_NewRequestPart_PayloadSizeError(t *testing.T) { + externalPayloadSize := 1 + expectedErr := fmt.Sprintf( + errReqPartPayloadSize, externalPayloadSize, reqPartMinSize) + defer func() { + if r := recover(); r == nil || r != expectedErr { + t.Errorf("NewRequestPart did not panic with the expected error "+ + "when the size of the payload is smaller than the required "+ + "size.\nexpected: %s\nreceived: %+v", expectedErr, r) + } + }() + + _ = NewRequestPart(externalPayloadSize) +} + +// Happy path. +func Test_mapRequestPart(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + expectedPartNum := uint8(prng.Uint32()) + size := []byte{uint8(prng.Uint64()), uint8(prng.Uint64())} + expectedContents := make([]byte, prng.Intn(2000)) + prng.Read(expectedContents) + var data []byte + data = append(data, expectedPartNum) + data = append(data, size...) + data = append(data, expectedContents...) + + rmp := mapRequestPart(data) + + if expectedPartNum != rmp.partNum[0] { + t.Errorf("mapRequestPart did not correctly map partNum."+ + "\nexpected: %d\nreceived: %d", expectedPartNum, rmp.partNum[0]) + } + + if !bytes.Equal(expectedContents, rmp.contents) { + t.Errorf("mapRequestPart did not correctly map contents."+ + "\nexpected: %+v\nreceived: %+v", expectedContents, rmp.contents) + } + + if !bytes.Equal(data, rmp.data) { + t.Errorf("mapRequestPart did not save the data correctly."+ + "\nexpected: %+v\nreceived: %+v", data, rmp.data) + } +} + +// Happy path. +func TestRequestPart_Marshal_UnmarshalRequestPart(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + payload := make([]byte, prng.Intn(2000)) + prng.Read(payload) + rmp := NewRequestPart(prng.Intn(2000)) + + data := rmp.Marshal() + + newRmp, err := UnmarshalRequestPart(data) + if err != nil { + t.Errorf("UnmarshalRequestPart produced an error: %+v", err) + } + + if !reflect.DeepEqual(rmp, newRmp) { + t.Errorf("Failed to Marshal and unmarshal the RequestPart."+ + "\nexpected: %+v\nrecieved: %+v", rmp, newRmp) + } +} + +// Error path: provided bytes are too small. +func Test_UnmarshalRequestPart_Error(t *testing.T) { + data := []byte{1} + expectedErr := fmt.Sprintf(errReqPartDataSize, len(data), reqPartMinSize) + _, err := UnmarshalRequestPart([]byte{1}) + if err == nil || err.Error() != expectedErr { + t.Errorf("UnmarshalRequestPart did not produce the expected error "+ + "when the byte slice is smaller required."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Happy path. +func TestRequestPart_SetPartNum_GetPartNum(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + expectedPartNum := uint8(prng.Uint32()) + rmp := NewRequestPart(prng.Intn(2000)) + + rmp.SetPartNum(expectedPartNum) + + if expectedPartNum != rmp.GetPartNum() { + t.Errorf("GetPartNum failed to return the expected part number."+ + "\nexpected: %d\nrecieved: %d", expectedPartNum, rmp.GetPartNum()) + } +} + +// Happy path. +func TestRequestPart_GetMaxParts(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + expectedMaxParts := uint8(0) + rmp := NewRequestPart(prng.Intn(2000)) + + if expectedMaxParts != rmp.GetNumParts() { + t.Errorf("GetNumParts failed to return the expected max parts."+ + "\nexpected: %d\nrecieved: %d", expectedMaxParts, rmp.GetNumParts()) + } +} + +// Happy path. +func TestRequestPart_SetContents_GetContents_GetContentsSize_GetMaxContentsSize(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + externalPayloadSize := prng.Intn(2000) + contentSize := externalPayloadSize - reqPartMinSize - 10 + expectedContents := make([]byte, contentSize) + prng.Read(expectedContents) + rmp := NewRequestPart(externalPayloadSize) + rmp.SetContents(expectedContents) + + if !bytes.Equal(expectedContents, rmp.GetContents()) { + t.Errorf("GetContents failed to return the expected contents."+ + "\nexpected: %+v\nrecieved: %+v", expectedContents, rmp.GetContents()) + } + + if contentSize != rmp.GetContentsSize() { + t.Errorf("GetContentsSize failed to return the expected contents size."+ + "\nexpected: %d\nrecieved: %d", contentSize, rmp.GetContentsSize()) + } + + if externalPayloadSize-reqPartMinSize != rmp.GetMaxContentsSize() { + t.Errorf("GetMaxContentsSize failed to return the expected max "+ + "contents size.\nexpected: %d\nrecieved: %d", + externalPayloadSize-reqPartMinSize, rmp.GetMaxContentsSize()) + } +} + +// Error path: size of supplied contents does not match message contents size. +func TestRequestPart_SetContents_ContentsSizeError(t *testing.T) { + payloadSize, contentsLen := 255, 500 + expectedErr := fmt.Sprintf( + errReqPartContentsSize, contentsLen, payloadSize-reqPartMinSize) + defer func() { + if r := recover(); r == nil || r != expectedErr { + t.Errorf("SetContents did not panic with the expected error when "+ + "the size of the supplied bytes is larger than the content "+ + "size.\nexpected: %s\nreceived: %+v", expectedErr, r) + } + }() + + rmp := NewRequestPart(payloadSize) + rmp.SetContents(make([]byte, contentsLen)) +} diff --git a/single/receivedRequest.go b/single/receivedRequest.go index b8762674c226da6cf109e2ee223eca7aad1daecb..0cbf4adddfc3595f403c19ba663d053eb390afa8 100644 --- a/single/receivedRequest.go +++ b/single/receivedRequest.go @@ -10,6 +10,7 @@ import ( "gitlab.com/elixxir/client/single/message" ds "gitlab.com/elixxir/comms/network/dataStructures" "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/e2e/singleUse" "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/primitives/id" "sync" @@ -91,7 +92,8 @@ func (r Request) Respond(payload []byte, cmixParams cmix.CMIXParams, parts := partitionResponse(payload, r.net.GetMaxMessageLength(), r.maxParts) // Encrypt and send the partitions - cyphers := makeCyphers(r.dhKey, uint8(len(parts))) + cyphers := makeCyphers(r.dhKey, uint8(len(parts)), + singleUse.NewResponseKey, singleUse.NewResponseFingerprint) rounds := make([]id.Round, len(parts)) sendResults := make(chan ds.EventReturn, len(parts)) diff --git a/single/request.go b/single/request.go index 646608b1b2cc845006aa0be723c31c834e4b32c8..6198b1ffc46d5b8c0a6d74cec3cb6134c28291f8 100644 --- a/single/request.go +++ b/single/request.go @@ -1,8 +1,7 @@ package single import ( - "encoding/base64" - "fmt" + "bytes" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/cmix" @@ -26,7 +25,7 @@ import ( // Response interface allows for callbacks to type Response interface { Callback(payload []byte, receptionID receptionID.EphemeralIdentity, - round rounds.Round, err error) + rounds []rounds.Round, err error) } type RequestParams struct { @@ -46,11 +45,13 @@ func GetDefaultRequestParams() RequestParams { // Error messages. const ( // TransmitRequest - errNetworkHealth = "cannot send singe-use request when the network is not healthy" - errMakeDhKeys = "failed to generate DH keys (%s for %s): %+v" - errMakeIDs = "failed to generate IDs (%s for %s): %+v" - errAddFingerprint = "failed to add fingerprint %d of %d: %+v (%s for %s)" - errSendRequest = "failed to send %s request to %s: %+v" + errPayloadSize = "size of payload %d exceeds the maximum size of %d (%s for %s)" + errNetworkHealth = "cannot send singe-use request when the network is not healthy" + errMakeDhKeys = "failed to generate DH keys (%s for %s): %+v" + errMakeIDs = "failed to generate IDs (%s for %s): %+v" + errAddFingerprint = "failed to add fingerprint %d of %d: %+v (%s for %s)" + errSendRequest = "failed to send %s request to %s: %+v" + errSendRequestPart = "failed to send request part %d of %d (%s for %s): %+v" // generateDhKeys errGenerateInGroup = "failed to generate private key in group: %+v" @@ -63,11 +64,17 @@ const ( errResponseTimeout = "waiting for response to single-use request timed out after %s" ) +// Maximum number of request part cMix messages. +const maxNumRequestParts = 255 + // GetMaxRequestSize returns the maximum size of a request payload. func GetMaxRequestSize(net CMix, e2eGrp *cyclic.Group) int { payloadSize := message.GetRequestPayloadSize(net.GetMaxMessageLength(), e2eGrp.GetP().ByteLen()) - return message.GetRequestContentsSize(payloadSize) + requestSize := message.GetRequestContentsSize(payloadSize) + requestPartSize := message.GetRequestPartContentsSize( + net.GetMaxMessageLength()) + return requestSize + (maxNumRequestParts * requestPartSize) } /* Single is a system which allows for an end-to-end encrypted anonymous request @@ -83,16 +90,24 @@ func GetMaxRequestSize(net CMix, e2eGrp *cyclic.Group) int { // containing the given payload. The request is identified as coming from a new // user ID and the recipient of the request responds to that address. As a // result, this request does not reveal the identity of the sender. -// The current implementation only allows for a single cMix request payload. -// Because the request payload itself must include negotiation materials, it is -// limited to just a few thousand bits of payload, and will return an error if -// the payload is too large. GetMaxRequestSize can be used to get this max size. +// +// The current implementation allows for up to maxNumRequestParts cMix request +// payloads. GetMaxRequestSize can be used to get the max size. +// // The network follower must be running and healthy to transmit. func TransmitRequest(recipient contact.Contact, tag string, payload []byte, - callback Response, param RequestParams, net CMix, rng csprng.Source, - e2eGrp *cyclic.Group) (id.Round, receptionID.EphemeralIdentity, error) { + callback Response, param RequestParams, net cmix.Client, rng csprng.Source, + e2eGrp *cyclic.Group) ([]id.Round, receptionID.EphemeralIdentity, error) { + + if len(payload) > GetMaxRequestSize(net, e2eGrp) { + return nil, receptionID.EphemeralIdentity{}, errors.Errorf( + errPayloadSize, len(payload), GetMaxRequestSize(net, e2eGrp), tag, + recipient) + } + if !net.IsHealthy() { - return 0, receptionID.EphemeralIdentity{}, errors.New(errNetworkHealth) + return nil, receptionID.EphemeralIdentity{}, + errors.New(errNetworkHealth) } // Get address ID address space size; this blocks until the address space @@ -103,31 +118,34 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte, // Generate DH key and public key dhKey, publicKey, err := generateDhKeys(e2eGrp, recipient.DhPubKey, rng) if err != nil { - return 0, receptionID.EphemeralIdentity{}, + return nil, receptionID.EphemeralIdentity{}, errors.Errorf(errMakeDhKeys, tag, recipient, err) } // Build the message payload + payloadSize := message.GetRequestPayloadSize(net.GetMaxMessageLength(), + e2eGrp.GetP().ByteLen()) + firstPart, parts := partitionPayload( + message.GetRequestContentsSize(payloadSize), + message.GetRequestPartContentsSize(net.GetMaxMessageLength()), + payload) request := message.NewRequest( net.GetMaxMessageLength(), e2eGrp.GetP().ByteLen()) requestPayload := message.NewRequestPayload( - request.GetPayloadSize(), payload, param.MaxResponseMessages) + request.GetPayloadSize(), firstPart, param.MaxResponseMessages) // Generate new user ID and address ID var sendingID receptionID.EphemeralIdentity requestPayload, sendingID, err = makeIDs( requestPayload, publicKey, addressSize, param.Timeout, timeStart, rng) if err != nil { - return 0, receptionID.EphemeralIdentity{}, + return nil, receptionID.EphemeralIdentity{}, errors.Errorf(errMakeIDs, tag, recipient, err) } - fmt.Printf("reqPayload: %v\n", base64. - StdEncoding.EncodeToString(requestPayload.Marshal())) - - // Encrypt payload - fp := singleUse.NewTransmitFingerprint(recipient.DhPubKey) - key := singleUse.NewTransmitKey(dhKey) + // Encrypt and assemble payload + fp := singleUse.NewRequestFingerprint(recipient.DhPubKey) + key := singleUse.NewRequestKey(dhKey) encryptedPayload := auth.Crypt(key, fp[:24], requestPayload.Marshal()) fmt.Printf("ecrPayload: %v\n", base64.StdEncoding.EncodeToString(encryptedPayload)) // Generate CMix message MAC @@ -142,7 +160,7 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte, timeoutKillChan := make(chan bool) var callbackOnce sync.Once wrapper := func(payload []byte, receptionID receptionID.EphemeralIdentity, - round rounds.Round, err error) { + rounds []rounds.Round, err error) { select { case timeoutKillChan <- true: default: @@ -150,12 +168,14 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte, callbackOnce.Do(func() { net.DeleteClientFingerprints(sendingID.Source) - go callback.Callback(payload, receptionID, round, err) + go callback.Callback(payload, receptionID, rounds, err) }) } - cyphers := makeCyphers(dhKey, param.MaxResponseMessages) + cyphers := makeCyphers(dhKey, param.MaxResponseMessages, + singleUse.NewResponseKey, singleUse.NewResponseFingerprint) + roundIds := newRoundIdCollector(len(cyphers)) for i, cy := range cyphers { processor := responseProcessor{ sendingID: sendingID, @@ -164,12 +184,13 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte, cy: cy, tag: tag, recipient: &recipient, + roundIDs: roundIds, } err = net.AddFingerprint( sendingID.Source, processor.cy.GetFingerprint(), &processor) if err != nil { - return 0, receptionID.EphemeralIdentity{}, errors.Errorf( + return nil, receptionID.EphemeralIdentity{}, errors.Errorf( errAddFingerprint, i, len(cyphers), tag, recipient, err) } } @@ -189,14 +210,34 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte, rid, _, err := net.Send(recipient.ID, cmixMsg.RandomFingerprint(rng), svc, request.Marshal(), mac, param.CmixParam) if err != nil { - return 0, receptionID.EphemeralIdentity{}, + return nil, receptionID.EphemeralIdentity{}, errors.Errorf(errSendRequest, tag, recipient, err) } + roundIDs := make([]id.Round, len(parts)+1) + roundIDs[0] = rid + for i, part := range parts { + requestPart := message.NewRequestPart(net.GetMaxMessageLength()) + requestPart.SetPartNum(uint8(i + 1)) + requestPart.SetContents(part) + + key = singleUse.NewResponseKey(dhKey, uint64(i+1)) + fp = singleUse.NewResponseFingerprint(dhKey, uint64(i+1)) + encryptedPayload = auth.Crypt(key, fp[:24], requestPart.Marshal()) + mac = singleUse.MakeMAC(key, encryptedPayload) + + roundIDs[i+1], _, err = net.Send( + recipient.ID, fp, svc, encryptedPayload, mac, param.CmixParam) + if err != nil { + return nil, receptionID.EphemeralIdentity{}, errors.Errorf( + errSendRequestPart, i, len(part), tag, recipient, err) + } + } + remainingTimeout := param.Timeout - netTime.Since(timeStart) go waitForTimeout(timeoutKillChan, wrapper, remainingTimeout) - return rid, sendingID, nil + return roundIDs, sendingID, nil } // generateDhKeys generates a new public key and DH key. @@ -276,8 +317,36 @@ func waitForTimeout(kill chan bool, cb callbackWrapper, timeout time.Duration) { cb( nil, receptionID.EphemeralIdentity{}, - rounds.Round{}, + nil, errors.Errorf(errResponseTimeout, timeout), ) } } + +// partitionPayload splits the payload into its parts. The first part is of size +// firstPartSize and is shorter than the rest since it is sent in the +// message.Request, which includes extra information. It is also returned on its +// own so that it can be handled on its own. The rest of the parts are of size +// partSize and will be sent as part of message.RequestPart. +func partitionPayload(firstPartSize, partSize int, payload []byte) ( + firstPart []byte, parts [][]byte) { + + // Return just the first part if it fits in a single message + if len(payload) <= firstPartSize { + return payload, nil + } + + firstPart = payload[:firstPartSize] + + numParts := (len(payload[:firstPartSize]) + partSize - 1) / partSize + parts = make([][]byte, 0, numParts) + buff := bytes.NewBuffer(payload[firstPartSize:]) + + for n := buff.Next(partSize); len(n) > 0; n = buff.Next(partSize) { + newPart := make([]byte, partSize) + copy(newPart, n) + parts = append(parts, newPart) + } + + return firstPart, parts +} diff --git a/single/requestPartProcessor.go b/single/requestPartProcessor.go new file mode 100644 index 0000000000000000000000000000000000000000..41aa2bd8c40200b1c1d4637b69dca68089a1680c --- /dev/null +++ b/single/requestPartProcessor.go @@ -0,0 +1,57 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package single + +import ( + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/single/message" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/xx_network/primitives/id" +) + +// requestPartProcessor handles the decryption and collation of request parts. +type requestPartProcessor struct { + myId *id.ID + tag string + cb func(payloadContents []byte, rounds []rounds.Round) + c *message.Collator + cy cypher + roundIDs *roundCollector +} + +func (rpp *requestPartProcessor) Process(msg format.Message, + _ receptionID.EphemeralIdentity, round rounds.Round) { + decrypted, err := rpp.cy.Decrypt(msg.GetContents(), msg.GetMac()) + if err != nil { + jww.ERROR.Printf("[SU] Failed to decrypt single-use request payload "+ + "for %s to %s: %+v", rpp.tag, rpp.myId, err) + return + } + + requestPart, err := message.UnmarshalRequestPart(decrypted) + if err != nil { + jww.ERROR.Printf("[SU] Failed to unmarshal single-use request part "+ + "payload for %s to %s: %+v", rpp.tag, rpp.myId, err) + return + } + + payload, done, err := rpp.c.Collate(requestPart) + if err != nil { + jww.ERROR.Printf("[SU] Failed to collate single-use request payload "+ + "for %s to %s: %+v", rpp.tag, rpp.myId, err) + return + } + + rpp.roundIDs.add(round) + + if done { + rpp.cb(payload, rpp.roundIDs.getList()) + } +} diff --git a/single/responseProcessor.go b/single/responseProcessor.go index c6b503164bbdbbea64bc0f6d644f26e8c4923fc9..0f7134331051900027e3bf699218878bcdfa4f8a 100644 --- a/single/responseProcessor.go +++ b/single/responseProcessor.go @@ -12,7 +12,7 @@ import ( ) type callbackWrapper func(payload []byte, - receptionID receptionID.EphemeralIdentity, round rounds.Round, err error) + receptionID receptionID.EphemeralIdentity, rounds []rounds.Round, err error) // responseProcessor is registered for each potential fingerprint. Adheres to // the message.Processor interface registered with cmix.Client @@ -23,6 +23,7 @@ type responseProcessor struct { cy cypher tag string recipient *contact.Contact + roundIDs *roundCollector } // Process decrypts a response part and adds it to the collator - returning @@ -42,7 +43,14 @@ func (rsp *responseProcessor) Process(ecrMsg format.Message, return } - payload, done, err := rsp.c.Collate(decrypted) + responsePart, err := message.UnmarshalResponsePart(decrypted) + if err != nil { + jww.ERROR.Printf("[SU] Failed to unmarshal single-use response part "+ + "payload for %s to %s: %+v", rsp.tag, rsp.recipient.ID, err) + return + } + + payload, done, err := rsp.c.Collate(responsePart) if err != nil { jww.ERROR.Printf("[SU] Failed to collate single-use response payload "+ "for %s to %s, single use may fail: %+v", @@ -50,7 +58,9 @@ func (rsp *responseProcessor) Process(ecrMsg format.Message, return } + rsp.roundIDs.add(round) + if done { - rsp.callback(payload, receptionID, round, nil) + rsp.callback(payload, receptionID, rsp.roundIDs.getList(), nil) } } diff --git a/single/roundCollector.go b/single/roundCollector.go new file mode 100644 index 0000000000000000000000000000000000000000..4d48b8e32c0f3e458457502481dd69344c510ee1 --- /dev/null +++ b/single/roundCollector.go @@ -0,0 +1,52 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package single + +import ( + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/xx_network/primitives/id" + "sync" +) + +// roundCollector keeps track of a list of unique rounds. Multiple inserts of +// the same round are ignored. It is multi-thread safe. +type roundCollector struct { + list map[id.Round]rounds.Round + mux sync.Mutex +} + +// newRoundIdCollector initialises a new roundCollector with a list of the +// given size. Size is not necessary and can be larger or smaller than the real +// size. +func newRoundIdCollector(size int) *roundCollector { + return &roundCollector{ + list: make(map[id.Round]rounds.Round, size), + } +} + +// add inserts a new round to the list. +func (rc *roundCollector) add(round rounds.Round) { + rc.mux.Lock() + defer rc.mux.Unlock() + + rc.list[round.ID] = round +} + +// getList returns the list of round IDs. +func (rc *roundCollector) getList() []rounds.Round { + rc.mux.Lock() + defer rc.mux.Unlock() + + list := make([]rounds.Round, 0, len(rc.list)) + + for _, round := range rc.list { + list = append(list, round) + } + + return list +}