├── Makefile ├── .gitignore ├── go.mod ├── states ├── states_test.go └── state.go ├── excludedRounds ├── excludedRounds.go ├── set.go └── set_test.go ├── nicknames ├── isValid.go └── isValid_test.go ├── LICENSE ├── authorizer ├── dns.go └── dns_test.go ├── format ├── fingerprint.go ├── fingerprint_test.go ├── message.go └── message_test.go ├── fact ├── factList.go ├── type.go ├── type_test.go ├── factList_test.go ├── fact.go └── fact_test.go ├── current ├── activity_test.go └── activity.go ├── .gitlab-ci.yml ├── notifications ├── data.go └── data_test.go ├── go.sum ├── version ├── checkVersion.go └── checkVersion_test.go └── knownRounds ├── knownRounds.go ├── uint64Buff.go └── knownRounds_test.go /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: update master release update_master update_release build clean 2 | 3 | clean: 4 | go mod tidy 5 | go mod vendor -e 6 | 7 | update: 8 | -GOFLAGS="" go get all 9 | 10 | build: 11 | go build ./... 12 | 13 | update_release: 14 | GOFLAGS="" go get gitlab.com/xx_network/primitives@release 15 | 16 | update_master: 17 | GOFLAGS="" go get gitlab.com/xx_network/primitives@master 18 | 19 | master: update_master clean build 20 | 21 | release: update_release clean build 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | # Ignore glide files/folders 3 | glide.lock 4 | vendor/ 5 | # Ignore Jetbrains IDE folder 6 | .idea/* 7 | # Ignore vim .swp buffers for open files 8 | .*.swp 9 | .*.swo 10 | # Ignore local development scripts 11 | localdev_* 12 | # Ignore logs 13 | *.log 14 | # Android things 15 | *.iml 16 | /android/.gradle 17 | /android/local.properties 18 | /android/.idea/workspace.xml 19 | /android/.idea/libraries 20 | /android/.DS_Store 21 | /android/build 22 | /android/captures 23 | /android/.externalNativeBuild 24 | *.apk 25 | *.ap_ 26 | *.dex 27 | *.class 28 | *.aar 29 | *.jar 30 | # Ignore genered version file 31 | cmd/version_vars.go 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gitlab.com/elixxir/primitives 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/badoux/checkmail v1.2.1 7 | github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 8 | github.com/pkg/errors v0.9.1 9 | github.com/spf13/jwalterweatherman v1.1.0 10 | github.com/ttacon/libphonenumber v1.2.1 11 | gitlab.com/xx_network/primitives v0.0.5 12 | golang.org/x/crypto v0.16.0 13 | ) 14 | 15 | require ( 16 | github.com/golang/protobuf v1.5.2 // indirect 17 | github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect 18 | golang.org/x/sys v0.15.0 // indirect 19 | google.golang.org/protobuf v1.26.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /states/states_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package states 9 | 10 | import "testing" 11 | 12 | // Consistency test of Round.String. 13 | func TestRound_String(t *testing.T) { 14 | expected := []string{"PENDING", "PRECOMPUTING", "STANDBY", "QUEUED", 15 | "REALTIME", "COMPLETED", "FAILED", "UNKNOWN STATE: 7"} 16 | 17 | for st := PENDING; st <= NUM_STATES; st++ { 18 | if st.String() != expected[st] { 19 | t.Errorf("Incorrect string for Round state %d."+ 20 | "\nexpected: %s\nreceived: %s", st, expected[st], st.String()) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /excludedRounds/excludedRounds.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package excludedRounds 9 | 10 | import ( 11 | "gitlab.com/xx_network/primitives/id" 12 | ) 13 | 14 | // ExcludedRounds is a list of rounds that are excluded from sending on. 15 | type ExcludedRounds interface { 16 | // Has indicates if the round is in the list. 17 | Has(rid id.Round) bool 18 | 19 | // Insert adds the round to the list. Returns true if the round was added. 20 | Insert(rid id.Round) bool 21 | 22 | // Remove deletes the round from the list. 23 | Remove(rid id.Round) 24 | 25 | // Len returns the number of rounds in the list. 26 | Len() int 27 | } 28 | -------------------------------------------------------------------------------- /nicknames/isValid.go: -------------------------------------------------------------------------------- 1 | package nicknames 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | jww "github.com/spf13/jwalterweatherman" 6 | ) 7 | 8 | const ( 9 | MinNicknameLength = 3 10 | MaxNicknameLength = 24 11 | ) 12 | 13 | var ErrNicknameTooShort = errors.Errorf("nicknames must be at least "+ 14 | "%d characters in length", MinNicknameLength) 15 | var ErrNicknameTooLong = errors.Errorf("nicknames must be %d "+ 16 | "characters in length or less", MaxNicknameLength) 17 | 18 | // IsValid checks if a nickname is valid. 19 | // 20 | // Rules: 21 | // - A nickname must not be longer than 24 characters. 22 | // - A nickname must not be shorter than 1 character. 23 | // - If a nickname is blank (empty string), then it will be treated by the 24 | // system as no nickname. 25 | // 26 | // TODO: Add character filtering. 27 | func IsValid(nick string) error { 28 | if nick == "" { 29 | jww.INFO.Printf( 30 | "Empty nickname passed; treating it as if no nickname was set.") 31 | return nil 32 | } 33 | 34 | runeNick := []rune(nick) 35 | if len(runeNick) < MinNicknameLength { 36 | return errors.WithStack(ErrNicknameTooShort) 37 | } 38 | 39 | if len(runeNick) > MaxNicknameLength { 40 | return errors.WithStack(ErrNicknameTooLong) 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2024 xx foundation 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright notice, 9 | this list of conditions and the following disclaimer in the documentation and/or 10 | other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | -------------------------------------------------------------------------------- /authorizer/dns.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package authorizer 9 | 10 | import ( 11 | "encoding/hex" 12 | ) 13 | 14 | const ( 15 | // DomainName is the registered domain to be used for calculation of 16 | // unique Gateway DNS addresses by authorizer, gateway, and client. 17 | DomainName = "xxnode.io" 18 | 19 | // Maximum length of DNS name. Determined by third party service. 20 | maxLength = 64 21 | 22 | // Maximum number of characters of gateway ID to use. Subtract length of 23 | // domain plus the additional period from maxLength. 24 | maxGwIdLength = maxLength - len(DomainName) - 1 25 | ) 26 | 27 | // GetGatewayDns returns the DNS name for the given marshalled gateway ID. 28 | // Strips the domain name, if it exists, from the encoded ID. 29 | func GetGatewayDns(gwID []byte) string { 30 | encoded := hex.EncodeToString(gwID) 31 | if len(encoded) > maxGwIdLength { 32 | encoded = encoded[:maxGwIdLength] 33 | } 34 | return encoded + "." + DomainName 35 | } 36 | -------------------------------------------------------------------------------- /excludedRounds/set.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | // package Set contains a wrapper for the set object which is thread-safe 9 | 10 | package excludedRounds 11 | 12 | import ( 13 | "sync" 14 | 15 | "github.com/golang-collections/collections/set" 16 | 17 | "gitlab.com/xx_network/primitives/id" 18 | ) 19 | 20 | // Set struct contains a set of rounds to be excluded from cmix 21 | type Set struct { 22 | xr *set.Set 23 | sync.RWMutex 24 | } 25 | 26 | func NewSet() *Set { 27 | return &Set{xr: set.New(nil)} 28 | } 29 | 30 | func (s *Set) Has(rid id.Round) bool { 31 | s.RLock() 32 | defer s.RUnlock() 33 | 34 | return s.xr.Has(rid) 35 | } 36 | 37 | func (s *Set) Insert(rid id.Round) bool { 38 | s.Lock() 39 | defer s.Unlock() 40 | 41 | if s.xr.Has(rid) { 42 | return false 43 | } 44 | 45 | s.xr.Insert(rid) 46 | return true 47 | } 48 | 49 | func (s *Set) Remove(rid id.Round) { 50 | s.Lock() 51 | defer s.Unlock() 52 | 53 | s.xr.Remove(rid) 54 | } 55 | 56 | func (s *Set) Len() int { 57 | s.RLock() 58 | defer s.RUnlock() 59 | 60 | return s.xr.Len() 61 | } 62 | -------------------------------------------------------------------------------- /excludedRounds/set_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package excludedRounds 9 | 10 | import ( 11 | "testing" 12 | 13 | "gitlab.com/xx_network/primitives/id" 14 | ) 15 | 16 | func TestSet(t *testing.T) { 17 | s := NewSet() 18 | if s.Len() != 1 { 19 | t.Errorf("Unexpected length.\nexpected: %d\nreceived: %d", 1, s.Len()) 20 | } 21 | rid1 := id.Round(400) 22 | if s.Has(rid1) { 23 | t.Errorf("NewSet excluded rounds set should not have anything in it") 24 | } 25 | if !s.Insert(rid1) { 26 | t.Errorf("Insert failed.") 27 | } 28 | if s.Insert(rid1) { 29 | t.Errorf("Insert did not fail for already inserted item.") 30 | } 31 | if !s.Has(rid1) { 32 | t.Errorf("Should have found inserted round in excluded round set") 33 | } 34 | if s.Len() != 2 { 35 | t.Errorf("Unexpected length.\nexpected: %d\nreceived: %d", 2, s.Len()) 36 | } 37 | s.Remove(rid1) 38 | if s.Has(rid1) { 39 | t.Errorf("Should not have found round in excluded round set") 40 | } 41 | if s.Len() != 1 { 42 | t.Errorf("Unexpected length.\nexpected: %d\nreceived: %d", 1, s.Len()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /states/state.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package states 9 | 10 | import "strconv" 11 | 12 | // This holds the enum for the states of a round. It is in primitives so 13 | // other repos such as registration/permissioning, gateway, and client can 14 | // access it 15 | 16 | // Round describes the state of the round. 17 | type Round uint32 18 | 19 | // List of round states. 20 | const ( 21 | PENDING = Round(iota) 22 | PRECOMPUTING 23 | STANDBY 24 | QUEUED 25 | REALTIME 26 | COMPLETED 27 | FAILED 28 | NUM_STATES 29 | ) 30 | 31 | // String returns the string representation of the Round state. This functions 32 | // adheres to the fmt.Stringer interface. 33 | func (r Round) String() string { 34 | switch r { 35 | case PENDING: 36 | return "PENDING" 37 | case PRECOMPUTING: 38 | return "PRECOMPUTING" 39 | case STANDBY: 40 | return "STANDBY" 41 | case QUEUED: 42 | return "QUEUED" 43 | case REALTIME: 44 | return "REALTIME" 45 | case COMPLETED: 46 | return "COMPLETED" 47 | case FAILED: 48 | return "FAILED" 49 | default: 50 | return "UNKNOWN STATE: " + strconv.FormatUint(uint64(r), 10) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /format/fingerprint.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package format 9 | 10 | import ( 11 | "encoding/base64" 12 | "encoding/json" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | type Fingerprint [KeyFPLen]byte 18 | 19 | // NewFingerprint generates a new Fingerprint with the provided bytes. 20 | func NewFingerprint(b []byte) Fingerprint { 21 | var fp Fingerprint 22 | copy(fp[:], b[:]) 23 | return fp 24 | } 25 | 26 | // Bytes returns the fingerprint as a byte slice. 27 | func (fp Fingerprint) Bytes() []byte { 28 | return fp[:] 29 | } 30 | 31 | // String returns the fingerprint as a base 64 encoded string. This functions 32 | // satisfies the fmt.Stringer interface. 33 | func (fp Fingerprint) String() string { 34 | return base64.StdEncoding.EncodeToString(fp.Bytes()) 35 | } 36 | 37 | // MarshalJSON adheres to the json.Marshaler interface. 38 | func (fp Fingerprint) MarshalJSON() ([]byte, error) { 39 | return json.Marshal(fp[:]) 40 | } 41 | 42 | // UnmarshalJSON adheres to the json.Unmarshaler interface. 43 | func (fp *Fingerprint) UnmarshalJSON(data []byte) error { 44 | var fpBytes []byte 45 | err := json.Unmarshal(data, &fpBytes) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | if len(fpBytes) != KeyFPLen { 51 | return errors.Errorf("length of fingerprint must be %d", KeyFPLen) 52 | } 53 | 54 | copy(fp[:], fpBytes[:]) 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /fact/factList.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package fact 9 | 10 | import ( 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | jww "github.com/spf13/jwalterweatherman" 15 | ) 16 | 17 | // FactList is a list of Fact objects. This type can be JSON marshalled and 18 | // unmarshalled. 19 | type FactList []Fact 20 | 21 | const factDelimiter = "," 22 | const factBreak = ";" 23 | 24 | // Stringify marshals the FactList into a portable string. 25 | func (fl FactList) Stringify() string { 26 | stringList := make([]string, len(fl)) 27 | for index, f := range fl { 28 | stringList[index] = f.Stringify() 29 | } 30 | 31 | return strings.Join(stringList, factDelimiter) + factBreak 32 | } 33 | 34 | // UnstringifyFactList unmarshalls the stringified FactList, which consists of 35 | // the fact list and optional arbitrary data, delimited by the factBreak. 36 | func UnstringifyFactList(s string) (FactList, string, error) { 37 | parts := strings.SplitN(s, factBreak, 2) 38 | if len(parts) != 2 { 39 | return nil, "", errors.New("Invalid fact string passed") 40 | } else if parts[0] == "" { 41 | return nil, parts[1], nil 42 | } 43 | factStrings := strings.Split(parts[0], factDelimiter) 44 | 45 | factList := make([]Fact, 0, len(factStrings)) 46 | for _, fString := range factStrings { 47 | fact, err := UnstringifyFact(fString) 48 | if err != nil { 49 | jww.WARN.Printf( 50 | "Fact %q failed to unstringify, dropped: %s", fString, err) 51 | } else { 52 | factList = append(factList, fact) 53 | } 54 | 55 | } 56 | return factList, parts[1], nil 57 | } 58 | -------------------------------------------------------------------------------- /nicknames/isValid_test.go: -------------------------------------------------------------------------------- 1 | package nicknames 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | const nicknameSource = "Sodium, atomic number 11, was first isolated by " + 10 | "Humphry Davy in 1807. A chemical component of salt, he named it Na " + 11 | "in honor of the saltiest region on earth, North America." 12 | 13 | // Tests that IsValid returns true for all usernames within the correct lengths. 14 | func TestIsValid(t *testing.T) { 15 | for i := MinNicknameLength; i <= MaxNicknameLength; i++ { 16 | nick := nicknameSource[:i] 17 | if err := IsValid(nick); err != nil { 18 | t.Errorf("Error returned from nicknames.IsValid with valid "+ 19 | "nickname %q of input of length %d: %+v", nick, i, err) 20 | } 21 | } 22 | } 23 | 24 | // Tests that IsValid return nil for an empty nickname. 25 | func TestIsValid_Empty(t *testing.T) { 26 | if err := IsValid(""); err != nil { 27 | t.Errorf("Empty nickname should be valid, received: %+v", err) 28 | } 29 | } 30 | 31 | // Error path: Tests that IsValid returns the error ErrNicknameTooLong when the 32 | // nickname is too long. 33 | func TestIsValid_MaxLengthError(t *testing.T) { 34 | for i := MaxNicknameLength + 1; i < MaxNicknameLength*5; i++ { 35 | nick := nicknameSource[:i] 36 | err := IsValid(nick) 37 | if err == nil || !errors.Is(err, ErrNicknameTooLong) { 38 | t.Errorf("Wrong error returned from nicknames.IsValid with too "+ 39 | "long input of length %d.\nexpected: %v\nreceived: %+v", 40 | i, ErrNicknameTooLong, err) 41 | } 42 | } 43 | } 44 | 45 | // Error path: Tests that IsValid returns the error ErrNicknameTooShort when the 46 | // nickname is too short. 47 | func TestIsValid_MinLengthError(t *testing.T) { 48 | for i := 1; i < MinNicknameLength; i++ { 49 | nick := nicknameSource[:i] 50 | 51 | err := IsValid(nick) 52 | if err == nil || !errors.Is(err, ErrNicknameTooShort) { 53 | t.Errorf("Wrong error returned from nicknames.IsValid with too "+ 54 | "short input of length %d.\nexpected: %v\nreceived: %+v", 55 | i, ErrNicknameTooShort, err) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /fact/type.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package fact 9 | 10 | import ( 11 | "strconv" 12 | 13 | "github.com/pkg/errors" 14 | jww "github.com/spf13/jwalterweatherman" 15 | ) 16 | 17 | type FactType uint8 18 | 19 | const ( 20 | Username FactType = 0 21 | Email FactType = 1 22 | Phone FactType = 2 23 | Nickname FactType = 3 24 | ) 25 | 26 | // String returns the string representation of the FactType. This functions 27 | // adheres to the fmt.Stringer interface. 28 | func (t FactType) String() string { 29 | switch t { 30 | case Username: 31 | return "Username" 32 | case Email: 33 | return "Email" 34 | case Phone: 35 | return "Phone" 36 | case Nickname: 37 | return "Nickname" 38 | default: 39 | return "Unknown Fact FactType: " + strconv.FormatUint(uint64(t), 10) 40 | } 41 | } 42 | 43 | // Stringify marshals the FactType into a portable string. 44 | func (t FactType) Stringify() string { 45 | switch t { 46 | case Username: 47 | return "U" 48 | case Email: 49 | return "E" 50 | case Phone: 51 | return "P" 52 | case Nickname: 53 | return "N" 54 | } 55 | jww.FATAL.Panicf("Unknown Fact FactType: %d", t) 56 | return "error" 57 | } 58 | 59 | // UnstringifyFactType unmarshalls the stringified FactType. 60 | func UnstringifyFactType(s string) (FactType, error) { 61 | switch s { 62 | case "U": 63 | return Username, nil 64 | case "E": 65 | return Email, nil 66 | case "P": 67 | return Phone, nil 68 | case "N": 69 | return Nickname, nil 70 | } 71 | return 99, errors.Errorf("Unknown Fact FactType: %s", s) 72 | } 73 | 74 | // IsValid determines if the FactType is one of the defined types. 75 | func (t FactType) IsValid() bool { 76 | switch t { 77 | case Username, Email, Phone, Nickname: 78 | return true 79 | default: 80 | return false 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /current/activity_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package current 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/pkg/errors" 14 | 15 | "gitlab.com/elixxir/primitives/states" 16 | ) 17 | 18 | // Consistency test of Activity.String. 19 | func TestActivity_String(t *testing.T) { 20 | expected := []string{"NOT_STARTED", "WAITING", "PRECOMPUTING", "STANDBY", 21 | "REALTIME", "COMPLETED", "ERROR", "CRASH", "UNKNOWN ACTIVITY: 8"} 22 | 23 | for st := NOT_STARTED; st <= NUM_STATES; st++ { 24 | if st.String() != expected[st] { 25 | t.Errorf("Incorrect string for Activity %d."+ 26 | "\nexpected: %s\nreceived: %s", st, expected[st], st.String()) 27 | } 28 | } 29 | } 30 | 31 | // Test proper happy path of Activity.ConvertToRoundState. 32 | func TestActivity_ConvertToRoundState(t *testing.T) { 33 | tests := []struct { 34 | activity Activity 35 | state states.Round 36 | err error 37 | }{ 38 | {NOT_STARTED, 99, errors.Errorf("unable to convert activity %s (%d) "+ 39 | "to valid state", NOT_STARTED, NOT_STARTED)}, 40 | {WAITING, states.PENDING, nil}, 41 | {PRECOMPUTING, states.PRECOMPUTING, nil}, 42 | {STANDBY, states.STANDBY, nil}, 43 | {REALTIME, states.REALTIME, nil}, 44 | {COMPLETED, states.COMPLETED, nil}, 45 | {ERROR, states.FAILED, nil}, 46 | {CRASH, 99, errors.Errorf("unable to convert activity %s (%d) to "+ 47 | "a valid state", CRASH, CRASH)}, 48 | } 49 | 50 | for i, tt := range tests { 51 | state, err := tt.activity.ConvertToRoundState() 52 | if err != nil && tt.err == nil { 53 | if err.Error() != tt.err.Error() { 54 | t.Errorf( 55 | "Unexpected error for %s (%d).\nexpected: %s\nreceived: %s", 56 | tt.activity, i, tt.err, err) 57 | } 58 | } else if state != tt.state { 59 | t.Errorf("Unexpected conversation of %s (%d)."+ 60 | "\nexpected: %s\nreceived: %s", tt.activity, i, tt.state, state) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | cache: 2 | untracked: true 3 | key: "$CI_BUILD_REF_NAME" 4 | paths: 5 | - vendor/ 6 | 7 | before_script: 8 | - go version || echo "Go executable not found." 9 | - echo $CI_BUILD_REF 10 | - echo $CI_PROJECT_DIR 11 | - echo $PWD 12 | - eval $(ssh-agent -s) 13 | - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null 14 | - mkdir -p ~/.ssh 15 | - chmod 700 ~/.ssh 16 | - ssh-keyscan -t rsa $GITLAB_SERVER > ~/.ssh/known_hosts 17 | - rm -rf ~/.gitconfig 18 | - git config --global url."git@$GITLAB_SERVER:".insteadOf "https://gitlab.com/" 19 | - git config --global url."git@$GITLAB_SERVER:".insteadOf "https://git.xx.network/" --add 20 | - export PATH=$HOME/go/bin:$PATH 21 | 22 | stages: 23 | - test 24 | - build 25 | - trigger_integration 26 | 27 | test: 28 | stage: test 29 | image: $DOCKER_IMAGE 30 | script: 31 | - git clean -ffdx 32 | - go mod vendor -v 33 | - go mod tidy 34 | - go build ./... 35 | - mkdir -p testdata 36 | 37 | # Test coverage 38 | - go-acc --covermode atomic --output testdata/coverage.out ./... -- -v 39 | # Exclude some specific packages and files 40 | - cat testdata/coverage.out | grep -v pb[.]go > testdata/coverage-real.out 41 | - go tool cover -func=testdata/coverage-real.out 42 | - go tool cover -html=testdata/coverage-real.out -o testdata/coverage.html 43 | 44 | # Test Coverage Check 45 | - go tool cover -func=testdata/coverage-real.out | grep "total:" | awk '{print $3}' | sed 's/\%//g' > testdata/coverage-percentage.txt 46 | - export CODE_CHECK=$(echo "$(cat testdata/coverage-percentage.txt) >= $MIN_CODE_COVERAGE" | bc -l) 47 | - (if [ "$CODE_CHECK" == "1" ]; then echo "Minimum coverage of $MIN_CODE_COVERAGE succeeded"; else echo "Minimum coverage of $MIN_CODE_COVERAGE failed"; exit 1; fi); 48 | artifacts: 49 | paths: 50 | - vendor/ 51 | - testdata/ 52 | 53 | build: 54 | stage: build 55 | image: $DOCKER_IMAGE 56 | script: 57 | - go mod vendor -v 58 | - go build ./... 59 | - mkdir -p release 60 | - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' ./... 61 | - cd release 62 | artifacts: 63 | paths: 64 | - release/ 65 | 66 | 67 | trigger-integration: 68 | stage: trigger_integration 69 | trigger: 70 | project: elixxir/integration 71 | branch: $CI_COMMIT_REF_NAME 72 | only: 73 | - master 74 | -------------------------------------------------------------------------------- /current/activity.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package current 9 | 10 | import ( 11 | "strconv" 12 | 13 | "github.com/pkg/errors" 14 | 15 | "gitlab.com/elixxir/primitives/states" 16 | ) 17 | 18 | // This holds the enum for the activity of the server. It is in primitives so 19 | // that other repos such as registration/permissioning can access it. 20 | 21 | // Activity describes the activity a server has be doing. 22 | type Activity uint32 23 | 24 | // List of Activities. 25 | const ( 26 | NOT_STARTED = Activity(iota) 27 | WAITING 28 | PRECOMPUTING 29 | STANDBY 30 | REALTIME 31 | COMPLETED 32 | ERROR 33 | CRASH 34 | NUM_STATES 35 | ) 36 | 37 | // String returns the string representation of the Activity. This functions 38 | // adheres to the fmt.Stringer interface. 39 | func (a Activity) String() string { 40 | switch a { 41 | case NOT_STARTED: 42 | return "NOT_STARTED" 43 | case WAITING: 44 | return "WAITING" 45 | case PRECOMPUTING: 46 | return "PRECOMPUTING" 47 | case STANDBY: 48 | return "STANDBY" 49 | case REALTIME: 50 | return "REALTIME" 51 | case COMPLETED: 52 | return "COMPLETED" 53 | case ERROR: 54 | return "ERROR" 55 | case CRASH: 56 | return "CRASH" 57 | default: 58 | return "UNKNOWN ACTIVITY: " + strconv.FormatUint(uint64(a), 10) 59 | } 60 | } 61 | 62 | // ConvertToRoundState converts an Activity to a valid round state or returns an 63 | // error if it is invalid. 64 | func (a Activity) ConvertToRoundState() (states.Round, error) { 65 | switch a { 66 | case WAITING: 67 | return states.PENDING, nil 68 | case PRECOMPUTING: 69 | return states.PRECOMPUTING, nil 70 | case STANDBY: 71 | return states.STANDBY, nil 72 | case REALTIME: 73 | return states.REALTIME, nil 74 | case COMPLETED: 75 | return states.COMPLETED, nil 76 | case ERROR: 77 | return states.FAILED, nil 78 | default: 79 | // Unsupported conversion. Return an arbitrary round and error 80 | return states.Round(99), errors.Errorf( 81 | "unable to convert activity %s (%d) to a valid state", a, a) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /authorizer/dns_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package authorizer 9 | 10 | import ( 11 | "math/rand" 12 | "testing" 13 | ) 14 | 15 | // Consistency test of GetGatewayDns. 16 | func TestGetGatewayDns(t *testing.T) { 17 | prng := rand.New(rand.NewSource(389257)) 18 | 19 | expectedDnsNames := []string{ 20 | "883968c2360cb4.xxnode.io", 21 | "72a1a31943d4282c700b614ab911769b316e064bf001081d3484aa.xxnode.io", 22 | "3f346ab53235279738c9355b97adde74f8703b7f8a55f100617ad0.xxnode.io", 23 | "df75c745cd8f0e3226cb8b48368a7eefac708b63da8b793e747d6e.xxnode.io", 24 | "de5e5e0ddfaa873b04d1a6e38d8b000edb.xxnode.io", 25 | "ebfb75cd082cacc0a24106ff35f818d5e1355a9cf2c57541c8515f.xxnode.io", 26 | "265d6ed02d478816e9455572a0a292d8240d1bdb4db854e508cdef.xxnode.io", 27 | "847d2b5077f2afb1cb519e0297cc8b66d287f29020d68d312853e7.xxnode.io", 28 | "19441cb0394816046e7d76cd410dd75944255f79a37f0677a7b125.xxnode.io", 29 | "b09d84ed9bf54d0e8831e6c11def92bceb25eb3bd4166106ea23de.xxnode.io", 30 | "d81e9c84aa04d9fb848ec5de6333a0b4818735e5f7604b60b848f2.xxnode.io", 31 | "abed7c32d04c12100092856e2f524ed5cb9f2764ece34f1c15f512.xxnode.io", 32 | "936acb388ff7ab670ab83c68bf0fbb6e6abc4121edff348a11ea56.xxnode.io", 33 | "082de3dea0097c7bec94e0f91990a2f1b4f17a973873bd6819c076.xxnode.io", 34 | "5515a6895c3c129fdb7d36925fb164c882d378cd92d672424bec5c.xxnode.io", 35 | "541d092e3000108fbf5f24cdde601c30a04d9014bc070ae5f2c26f.xxnode.io", 36 | "5d244632decacbc4f08d2da5fae3a8f2854865d6e1f0380cd5ae2e.xxnode.io", 37 | "1d2e6ff5023016518c59b7d0b71b77924033fb624cd22a97b2d97c.xxnode.io", 38 | "57a8402f3ac28f1a0901c2d23819356ec6c0c9b38b52484e5e2327.xxnode.io", 39 | "3ece16fbf31224624fd657df55bb9f363048dcbaffa379df2ecd03.xxnode.io", 40 | "2649315e.xxnode.io", 41 | "39f0119b3114b56dd810183063fe1bc92157efb046e5bde0b98673.xxnode.io", 42 | "777e17f50624354acf1cb6527f9dc3e1bc87d2e8162424ddb5d5fa.xxnode.io", 43 | "b95e85de4ea45f6a9e4dacf16a58686d9632764da4043715f51254.xxnode.io", 44 | "43a01ed5093752390e7c4cf17a222d2ee321f38889516c6970ed8a.xxnode.io", 45 | } 46 | 47 | for i, expected := range expectedDnsNames { 48 | gwID := make([]byte, prng.Intn(maxGwIdLength*2)) 49 | prng.Read(gwID) 50 | dnsName := GetGatewayDns(gwID) 51 | 52 | if expected != dnsName { 53 | t.Errorf("Unexpected DNS name for gateway ID %X (%d)."+ 54 | "\nexpected: %q\nreceived: %q", gwID, i, expected, dnsName) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /fact/type_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package fact 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | // Consistency test of FactType.String. 15 | func TestFactType_String(t *testing.T) { 16 | tests := map[FactType]string{ 17 | Username: "Username", 18 | Email: "Email", 19 | Phone: "Phone", 20 | Nickname: "Nickname", 21 | FactType(200): "Unknown Fact FactType: 200", 22 | } 23 | 24 | for ft, expected := range tests { 25 | str := ft.String() 26 | if expected != str { 27 | t.Errorf("Unexpected FactType string.\nexpected: %q\nreceived: %q", 28 | expected, str) 29 | } 30 | } 31 | } 32 | 33 | // Tests that a FactType marshalled by FactType.Stringify and unmarshalled by 34 | // UnstringifyFactType matches the original. 35 | func TestFactType_Stringify_UnstringifyFactType(t *testing.T) { 36 | factTypes := []FactType{ 37 | Username, 38 | Email, 39 | Phone, 40 | Nickname, 41 | } 42 | 43 | for _, expected := range factTypes { 44 | str := expected.Stringify() 45 | 46 | ft, err := UnstringifyFactType(str) 47 | if err != nil { 48 | t.Fatalf("Failed to unstringify fact type %q: %+v", str, err) 49 | } 50 | if expected != ft { 51 | t.Errorf("Unexpected unstringified FactType."+ 52 | "\nexpected: %s\nreceived: %s", expected, str) 53 | } 54 | } 55 | } 56 | 57 | // Panic path: Tests that FactType.Stringify panics for an invalid FactType 58 | func TestFactType_Stringify_InvalidFactTypePanic(t *testing.T) { 59 | defer func() { 60 | if r := recover(); r == nil { 61 | t.Errorf("Failed to panic for invalid FactType") 62 | } 63 | }() 64 | 65 | FactType(99).Stringify() 66 | } 67 | 68 | // Error path: Tests that FactType.UnstringifyFactType returns an error for an 69 | // invalid FactType. 70 | func TestFactType_Unstringify_UnknownFactTypeError(t *testing.T) { 71 | _, err := UnstringifyFactType("invalid") 72 | if err == nil { 73 | t.Errorf("Failed to get error for invalid FactType.") 74 | } 75 | } 76 | 77 | func TestFactType_IsValid(t *testing.T) { 78 | tests := map[FactType]bool{ 79 | Username: true, 80 | Email: true, 81 | Phone: true, 82 | Nickname: true, 83 | 99: false, 84 | } 85 | 86 | for ft, expected := range tests { 87 | if ft.IsValid() != expected { 88 | t.Errorf("Unexpected IsValid result for %s."+ 89 | "\nexpected: %t\nreceived: %t", ft, expected, ft.IsValid()) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /format/fingerprint_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package format 9 | 10 | import ( 11 | "bytes" 12 | "encoding/base64" 13 | "encoding/json" 14 | "math/rand" 15 | "testing" 16 | ) 17 | 18 | // Happy path. 19 | func TestNewFingerprint(t *testing.T) { 20 | prng := rand.New(rand.NewSource(42)) 21 | fpBytes := make([]byte, KeyFPLen) 22 | prng.Read(fpBytes) 23 | 24 | fp := NewFingerprint(fpBytes) 25 | if !bytes.Equal(fpBytes, fp[:]) { 26 | t.Errorf("NewFingerprint failed to copy the correct bytes into the "+ 27 | "Fingerprint.\nexpected: %+v\nreceived: %+v", fpBytes, fp) 28 | } 29 | 30 | // Ensure that the data is copied 31 | fpBytes[2]++ 32 | if fp[2] == fpBytes[2] { 33 | t.Errorf("NewFingerprint failed to create a copy of the data.") 34 | } 35 | } 36 | 37 | // Happy path. 38 | func TestFingerprint_Bytes(t *testing.T) { 39 | prng := rand.New(rand.NewSource(42)) 40 | fpBytes := make([]byte, KeyFPLen) 41 | prng.Read(fpBytes) 42 | 43 | fp := NewFingerprint(fpBytes) 44 | testFpBytes := fp.Bytes() 45 | if !bytes.Equal(fpBytes, testFpBytes) { 46 | t.Errorf("Bytes failed to return the expected bytes."+ 47 | "\nexpected: %+v\nreceived: %+v", fpBytes, testFpBytes) 48 | } 49 | 50 | // Ensure that the data is copied 51 | testFpBytes[2]++ 52 | if fp[2] == testFpBytes[2] { 53 | t.Errorf("Bytes failed to create a copy of the data.") 54 | } 55 | } 56 | 57 | // Happy path. 58 | func TestFingerprint_String(t *testing.T) { 59 | prng := rand.New(rand.NewSource(42)) 60 | fpBytes := make([]byte, KeyFPLen) 61 | prng.Read(fpBytes) 62 | fp := NewFingerprint(fpBytes) 63 | 64 | expectedString := base64.StdEncoding.EncodeToString(fpBytes) 65 | if expectedString != fp.String() { 66 | t.Errorf("String failed to return the expected string."+ 67 | "\nexpected: %s\nreceived: %s", expectedString, fp.String()) 68 | } 69 | } 70 | 71 | func Test_Fingerprint_JSON_Marshal_Unmarshal(t *testing.T) { 72 | prng := rand.New(rand.NewSource(74043)) 73 | fpBytes := make([]byte, KeyFPLen) 74 | prng.Read(fpBytes) 75 | 76 | expected := NewFingerprint(fpBytes) 77 | 78 | data, err := json.Marshal(expected) 79 | if err != nil { 80 | t.Errorf("Failed to marshal %T: %+v", expected, err) 81 | } 82 | 83 | var fp Fingerprint 84 | if err = json.Unmarshal(data, &fp); err != nil { 85 | t.Errorf("Failed to unmarshal %T: %+v", fp, err) 86 | } 87 | 88 | if expected != fp { 89 | t.Errorf("Unexpected fingerprint.\nexpected: %s\nreceived: %s", 90 | expected, fp) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /fact/factList_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package fact 9 | 10 | import ( 11 | "encoding/json" 12 | "reflect" 13 | "testing" 14 | ) 15 | 16 | // Tests that a FactList marshalled by FactList.Stringify and unmarshalled by 17 | // UnstringifyFactList matches the original. 18 | func TestFactList_Stringify_UnstringifyFactList(t *testing.T) { 19 | expected := FactList{ 20 | Fact{"vivian@elixxir.io", Email}, 21 | Fact{"(270) 301-5797US", Phone}, 22 | Fact{"invalidFact", Phone}, 23 | } 24 | 25 | flString := expected.Stringify() 26 | factList, _, err := UnstringifyFactList(flString) 27 | if err != nil { 28 | t.Fatalf("Failed to unstringify %q: %+v", flString, err) 29 | } 30 | 31 | expected = expected[:2] 32 | if !reflect.DeepEqual(factList, expected) { 33 | t.Errorf("Unexpected unstringified FactList."+ 34 | "\nexpected: %v\nreceived: %v", expected, factList) 35 | } 36 | } 37 | 38 | // Tests that a nil FactList marshalled by FactList.Stringify and unmarshalled 39 | // by UnstringifyFactList matches the original. 40 | func TestUnstringifyFactList_NilFactList(t *testing.T) { 41 | var expected FactList 42 | 43 | flString := expected.Stringify() 44 | factList, _, err := UnstringifyFactList(flString) 45 | if err != nil { 46 | t.Fatalf("Failed to unstringify %q: %+v", flString, err) 47 | } 48 | 49 | if !reflect.DeepEqual(factList, expected) { 50 | t.Errorf("Unexpected unstringified FactList."+ 51 | "\nexpected: %v\nreceived: %v", expected, factList) 52 | } 53 | } 54 | 55 | // Error path: Tests that UnstringifyFactList returns an error for a malformed 56 | // stringified FactList. 57 | func Test_UnstringifyFactList_MissingFactBreakError(t *testing.T) { 58 | _, _, err := UnstringifyFactList("hi") 59 | if err == nil { 60 | t.Errorf("Expected error for invalid stringified list.") 61 | } 62 | } 63 | 64 | // Tests that a FactList JSON marshalled and unmarshalled matches the original. 65 | func TestFactList_JsonMarshalUnmarshal(t *testing.T) { 66 | expected := FactList{ 67 | {"devUsername", Username}, 68 | {"devinputvalidation@elixxir.io", Email}, 69 | {"6502530000US", Phone}, 70 | {"name", Nickname}, 71 | } 72 | 73 | data, err := json.Marshal(expected) 74 | if err != nil { 75 | t.Fatalf("Failed to JSON marshal FactList: %+v", err) 76 | } 77 | 78 | var factList FactList 79 | err = json.Unmarshal(data, &factList) 80 | if err != nil { 81 | t.Errorf("Failed to JSON unmarshal FactList: %+v", err) 82 | } 83 | 84 | if !reflect.DeepEqual(expected, factList) { 85 | t.Errorf("Marshalled and unmarshalled FactList does not match original."+ 86 | "\nexpected: %+v\nreceived: %+v", expected, factList) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /notifications/data.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package notifications 9 | 10 | import ( 11 | "bytes" 12 | "encoding/base64" 13 | "encoding/csv" 14 | "strconv" 15 | "strings" 16 | 17 | "github.com/pkg/errors" 18 | jww "github.com/spf13/jwalterweatherman" 19 | ) 20 | 21 | type Data struct { 22 | EphemeralID int64 23 | RoundID uint64 24 | IdentityFP []byte 25 | MessageHash []byte 26 | } 27 | 28 | func (d *Data) String() string { 29 | fields := []string{ 30 | strconv.FormatInt(d.EphemeralID, 10), 31 | strconv.FormatUint(d.RoundID, 10), 32 | base64.StdEncoding.EncodeToString(d.IdentityFP), 33 | base64.StdEncoding.EncodeToString(d.MessageHash), 34 | } 35 | return "{" + strings.Join(fields, " ") + "}" 36 | } 37 | 38 | // BuildNotificationCSV converts the [Data] list into a CSV of the specified max 39 | // size and return it along with the included [Data] entries. Any [Data] entries 40 | // over that size are excluded. 41 | // 42 | // The CSV contains each [Data] entry on its own row with column one the 43 | // [Data.MessageHash] and column two having the [Data.IdentityFP], but base 64 44 | // encoded 45 | func BuildNotificationCSV(ndList []*Data, maxSize int) ([]byte, []*Data) { 46 | var buf bytes.Buffer 47 | var numWritten int 48 | 49 | for i, nd := range ndList { 50 | var line bytes.Buffer 51 | w := csv.NewWriter(&line) 52 | output := []string{ 53 | base64.StdEncoding.EncodeToString(nd.MessageHash), 54 | base64.StdEncoding.EncodeToString(nd.IdentityFP)} 55 | 56 | if err := w.Write(output); err != nil { 57 | jww.FATAL.Printf("Failed to write record %d of %d to "+ 58 | "notifications CSV line buffer: %+v", i, len(ndList), err) 59 | } 60 | w.Flush() 61 | 62 | if buf.Len()+line.Len() > maxSize { 63 | break 64 | } 65 | 66 | if _, err := buf.Write(line.Bytes()); err != nil { 67 | jww.FATAL.Printf("Failed to write record %d of %d to "+ 68 | "notifications CSV: %+v", i, len(ndList), err) 69 | } 70 | 71 | numWritten++ 72 | } 73 | 74 | return buf.Bytes(), ndList[numWritten:] 75 | } 76 | 77 | // DecodeNotificationsCSV decodes the Data list CSV into a slice of Data. 78 | func DecodeNotificationsCSV(data string) ([]*Data, error) { 79 | r := csv.NewReader(strings.NewReader(data)) 80 | records, err := r.ReadAll() 81 | if err != nil { 82 | return nil, errors.Wrapf(err, "Failed to read notifications CSV records.") 83 | } 84 | 85 | list := make([]*Data, len(records)) 86 | for i, tuple := range records { 87 | messageHash, err := base64.StdEncoding.DecodeString(tuple[0]) 88 | if err != nil { 89 | return nil, errors.Wrapf(err, 90 | "Failed to decode MessageHash for record %d of %d", 91 | i, len(records)) 92 | } 93 | 94 | identityFP, err := base64.StdEncoding.DecodeString(tuple[1]) 95 | if err != nil { 96 | return nil, errors.Wrapf(err, 97 | "Failed to decode IdentityFP for record %d of %d", 98 | i, len(records)) 99 | } 100 | list[i] = &Data{ 101 | IdentityFP: identityFP, 102 | MessageHash: messageHash, 103 | } 104 | } 105 | 106 | return list, nil 107 | } 108 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/badoux/checkmail v1.2.1 h1:TzwYx5pnsV6anJweMx2auXdekBwGr/yt1GgalIx9nBQ= 2 | github.com/badoux/checkmail v1.2.1/go.mod h1:XroCOBU5zzZJcLvgwU15I+2xXyCdTWXyR9MGfRhBYy0= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4= 6 | github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU= 7 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 8 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 9 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 10 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 11 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 12 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 13 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 17 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 18 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 19 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 20 | github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0= 21 | github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= 22 | github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4= 23 | github.com/ttacon/libphonenumber v1.2.1/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M= 24 | gitlab.com/xx_network/primitives v0.0.4-0.20230724185812-bc6fc6e5341b h1:oymmRpA+5/SeBp+MgFeYyuB8S9agfetGDnxBXXq9utE= 25 | gitlab.com/xx_network/primitives v0.0.4-0.20230724185812-bc6fc6e5341b/go.mod h1:vI6JXexgqihcIVFGsZAwGlHWGT14XC24NwEB2c6q9nc= 26 | gitlab.com/xx_network/primitives v0.0.5 h1:jPq3EnoghvrfcZixnYSWXyk9n8IU1XYXizQjlqdABmY= 27 | gitlab.com/xx_network/primitives v0.0.5/go.mod h1:yB8Sk1aqB8KJTq6SASA+XeA2gqWxvkcnGbShY3ISLVk= 28 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 29 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 30 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= 31 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 32 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 33 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 35 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 36 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 37 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 38 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 39 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 40 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 41 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 | -------------------------------------------------------------------------------- /version/checkVersion.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | // Package version describes a version used for a repository. 9 | // 10 | // The version for an entity is composed of a major version, minor version, and 11 | // patch. The major and minor version numbers are both integers and dictate the 12 | // compatibility between two versions. The patch provides extra information that 13 | // is not part of the compatibility check, but still must be present. 14 | package version 15 | 16 | import ( 17 | "strconv" 18 | "strings" 19 | 20 | "github.com/pkg/errors" 21 | ) 22 | 23 | // String version delimiter 24 | const delimiter = "." 25 | 26 | // Version structure contains the major, minor, and patch information. 27 | type Version struct { 28 | major int 29 | minor int 30 | patch string 31 | } 32 | 33 | // New returns a new Version with the given major, minor and patch. 34 | func New(major, minor int, patch string) Version { 35 | return Version{ 36 | major: major, 37 | minor: minor, 38 | patch: patch, 39 | } 40 | } 41 | 42 | // Major returns the major integer of the Version. 43 | func (v Version) Major() int { 44 | return v.major 45 | } 46 | 47 | // Minor returns the minor integer of the Version. 48 | func (v Version) Minor() int { 49 | return v.minor 50 | } 51 | 52 | // Patch returns the patch string of the Version. 53 | func (v Version) Patch() string { 54 | return v.patch 55 | } 56 | 57 | // String prints the Version in a string format of the form "major.minor.path". 58 | func (v Version) String() string { 59 | return strconv.Itoa(v.major) + delimiter + strconv.Itoa(v.minor) + 60 | delimiter + v.patch 61 | } 62 | 63 | // ParseVersion parses a string into a Version object. An error is returned for 64 | // invalid version string. To be valid, a string must contain a major integer, 65 | // a minor integer, and a patch string separated by a period. 66 | func ParseVersion(versionString string) (Version, error) { 67 | // Separate string into the three parts 68 | versionParts := strings.SplitN(versionString, delimiter, 3) 69 | 70 | // Check that the string has three parts 71 | if len(versionParts) != 3 { 72 | return Version{}, errors.Errorf("version string must contain 3 parts: "+ 73 | "a major, minor, and patch separated by \"%s\". Received string "+ 74 | "with %d part(s).", delimiter, len(versionParts)) 75 | } 76 | 77 | version := Version{} 78 | var err error 79 | 80 | // Check that the major version is an integer 81 | version.major, err = strconv.Atoi(versionParts[0]) 82 | if err != nil { 83 | return Version{}, 84 | errors.Errorf("expected integer for major version: %+v", err) 85 | } 86 | 87 | // Check that the minor version is an integer 88 | version.minor, err = strconv.Atoi(versionParts[1]) 89 | if err != nil { 90 | return Version{}, 91 | errors.Errorf("expected integer for minor version: %+v", err) 92 | } 93 | 94 | // Check that the patch is not empty 95 | if versionParts[2] == "" { 96 | return Version{}, errors.New("patch cannot be empty.") 97 | } 98 | version.patch = versionParts[2] 99 | 100 | return version, nil 101 | } 102 | 103 | // IsCompatible determines if the current Version is compatible with the 104 | // required Version. Version are compatible when the major versions are equal 105 | // and the current minor version is greater than or equal to the required 106 | // version. The patches do not need to match. 107 | func IsCompatible(required, current Version) bool { 108 | // Compare major versions 109 | if required.major != current.major { 110 | return false 111 | } 112 | 113 | // Compare minor versions 114 | if required.minor > current.minor { 115 | return false 116 | } 117 | 118 | return true 119 | } 120 | 121 | // Equal determines if two Version objects are equal. They are equal when major, 122 | // minor, and patch are all the same 123 | func Equal(a, b Version) bool { 124 | return a.major == b.major && a.minor == b.minor && a.patch == b.patch 125 | } 126 | 127 | // Cmp compares two Versions. Return 1 if a is greater than b, -1 if a is less 128 | // than b, and 0 if they are equal. A Version 129 | func Cmp(a, b Version) int { 130 | if a.major > b.major { 131 | return 1 132 | } else if a.major < b.major { 133 | return -1 134 | } 135 | 136 | if a.minor > b.minor { 137 | return 1 138 | } else if a.minor < b.minor { 139 | return -1 140 | } 141 | 142 | return 0 143 | } 144 | -------------------------------------------------------------------------------- /fact/fact.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package fact 9 | 10 | import ( 11 | "strings" 12 | 13 | "github.com/badoux/checkmail" 14 | "github.com/pkg/errors" 15 | "github.com/ttacon/libphonenumber" 16 | ) 17 | 18 | const ( 19 | // The maximum character length of a fact. 20 | maxFactLen = 64 21 | 22 | // The minimum character length of a nickname. 23 | minNicknameLen = 3 24 | ) 25 | 26 | // Fact represents a piece of user-identifying information. This structure can 27 | // be JSON marshalled and unmarshalled. 28 | // 29 | // JSON example: 30 | // 31 | // { 32 | // "Fact": "john@example.com", 33 | // "T": 1 34 | // } 35 | type Fact struct { 36 | Fact string `json:"Fact"` 37 | T FactType `json:"T"` 38 | } 39 | 40 | // NewFact checks if the inputted information is a valid fact on the 41 | // fact type. If so, it returns a new fact object. If not, it returns a 42 | // validation error. 43 | func NewFact(ft FactType, fact string) (Fact, error) { 44 | if len(fact) > maxFactLen { 45 | return Fact{}, errors.Errorf("Fact (%s) exceeds maximum character limit "+ 46 | "for a fact (%d characters)", fact, maxFactLen) 47 | } 48 | 49 | f := Fact{ 50 | Fact: fact, 51 | T: ft, 52 | } 53 | if err := ValidateFact(f); err != nil { 54 | return Fact{}, err 55 | } 56 | 57 | return f, nil 58 | } 59 | 60 | // Stringify marshals the Fact for transmission for UDB. It is not a part of the 61 | // fact interface. 62 | func (f Fact) Stringify() string { 63 | return f.T.Stringify() + f.Fact 64 | } 65 | 66 | // UnstringifyFact unmarshalls the stringified fact into a Fact. 67 | func UnstringifyFact(s string) (Fact, error) { 68 | if len(s) < 1 { 69 | return Fact{}, errors.New("stringified facts must at least " + 70 | "have a type at the start") 71 | } 72 | 73 | if len(s) > maxFactLen { 74 | return Fact{}, errors.Errorf("Fact (%s) exceeds maximum character limit "+ 75 | "for a fact (%d characters)", s, maxFactLen) 76 | } 77 | 78 | T := s[:1] 79 | fact := s[1:] 80 | if len(fact) == 0 { 81 | return Fact{}, errors.New( 82 | "stringified facts must be at least 1 character long") 83 | } 84 | ft, err := UnstringifyFactType(T) 85 | if err != nil { 86 | return Fact{}, errors.WithMessagef(err, 87 | "Failed to unstringify fact type for %q", s) 88 | } 89 | 90 | return NewFact(ft, fact) 91 | } 92 | 93 | // Normalized returns the fact in all uppercase letters. 94 | func (f Fact) Normalized() string { 95 | return strings.ToUpper(f.Fact) 96 | } 97 | 98 | // ValidateFact checks the fact to see if it valid based on its type. 99 | func ValidateFact(fact Fact) error { 100 | switch fact.T { 101 | case Username: 102 | return nil 103 | case Phone: 104 | // Extract specific information for validating a number 105 | // TODO: removes phone validation entirely. It is not used right now anyhow 106 | number, code := extractNumberInfo(fact.Fact) 107 | return validateNumber(number, code) 108 | case Email: 109 | // Check input of email inputted 110 | return validateEmail(fact.Fact) 111 | case Nickname: 112 | return validateNickname(fact.Fact) 113 | default: 114 | return errors.Errorf("Unknown fact type: %d", fact.T) 115 | } 116 | } 117 | 118 | // Numbers are assumed to have the 2-letter country code appended 119 | // to the fact, with the rest of the information being a phone number 120 | // Example: 6502530000US is a valid US number with the country code 121 | // that would be the fact information for a phone number 122 | func extractNumberInfo(fact string) (number, countryCode string) { 123 | factLen := len(fact) 124 | number = fact[:factLen-2] 125 | countryCode = fact[factLen-2:] 126 | return 127 | } 128 | 129 | // Validate the email input and check if the host is contact-able 130 | func validateEmail(email string) error { 131 | // Check that the input is validly formatted 132 | if err := checkmail.ValidateFormat(email); err != nil { 133 | return errors.Wrapf(err, "Could not validate format for email %q", email) 134 | } 135 | 136 | return nil 137 | } 138 | 139 | // Checks if the number and country code passed in is parse-able 140 | // and is a valid phone number with that information 141 | func validateNumber(number, countryCode string) error { 142 | catchPanic := func(number, countryCode string) (err error) { 143 | defer func() { 144 | if r := recover(); r != nil { 145 | err = errors.Errorf("Crash occured on phone validation of: "+ 146 | "number: %s, country code: %s: %+v", number, countryCode, r) 147 | } 148 | }() 149 | 150 | if len(number) == 0 || len(countryCode) == 0 { 151 | err = errors.New("Number or input are of length 0") 152 | return err 153 | } 154 | num, err := libphonenumber.Parse(number, countryCode) 155 | if err != nil || num == nil { 156 | err = errors.Wrapf(err, "Could not parse number %q", number) 157 | return err 158 | } 159 | if !libphonenumber.IsValidNumber(num) { 160 | err = errors.Errorf("Could not validate number %q", number) 161 | return err 162 | } 163 | 164 | return nil 165 | } 166 | 167 | return catchPanic(number, countryCode) 168 | } 169 | 170 | func validateNickname(nickname string) error { 171 | if len(nickname) < minNicknameLen { 172 | return errors.Errorf("Could not validate nickname %s: "+ 173 | "too short (< %d characters)", nickname, minNicknameLen) 174 | } 175 | return nil 176 | } 177 | -------------------------------------------------------------------------------- /version/checkVersion_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package version 9 | 10 | import ( 11 | "reflect" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | // Happy path. 17 | func TestNew(t *testing.T) { 18 | expected := Version{1, 2, "3"} 19 | 20 | test := New(expected.major, expected.minor, expected.patch) 21 | 22 | if !reflect.DeepEqual(expected, test) { 23 | t.Errorf("New did not create the expected Version."+ 24 | "\nexpected: %+v\nreceived: %+v", expected, test) 25 | } 26 | } 27 | 28 | // Happy path. 29 | func TestVersion_Major(t *testing.T) { 30 | v := Version{1, 2, "3"} 31 | 32 | if v.Major() != v.major { 33 | t.Errorf("Major did not return the expected value."+ 34 | "\nexpected: %d\nreceived: %d", v.major, v.Major()) 35 | } 36 | } 37 | 38 | // Happy path. 39 | func TestVersion_Minor(t *testing.T) { 40 | v := Version{1, 2, "3"} 41 | 42 | if v.Minor() != v.minor { 43 | t.Errorf("Minor did not return the expected value."+ 44 | "\nexpected: %d\nreceived: %d", v.minor, v.Minor()) 45 | } 46 | } 47 | 48 | // Happy path. 49 | func TestVersion_Patch(t *testing.T) { 50 | v := Version{1, 2, "3"} 51 | 52 | if v.Patch() != v.patch { 53 | t.Errorf("Patch did not return the expected value."+ 54 | "\nexpected: %s\nreceived: %s", v.patch, v.Patch()) 55 | } 56 | } 57 | 58 | // Happy path. 59 | func TestVersion_String(t *testing.T) { 60 | // Test values 61 | testValues := map[string]Version{ 62 | "0.0.": {}, 63 | "0.0.0": {0, 0, "0"}, 64 | "5.10.test": {5, 10, "test"}, 65 | "5.10.test.test": {5, 10, "test.test"}, 66 | } 67 | 68 | for expected, ver := range testValues { 69 | test := ver.String() 70 | if expected != test { 71 | t.Errorf("String did not return the expected string."+ 72 | "\nexpected: %s\nreceived: %s", expected, test) 73 | } 74 | } 75 | } 76 | 77 | // Happy path. 78 | func TestParseVersion(t *testing.T) { 79 | // Test values 80 | testValues := map[string]Version{ 81 | "0.0.0": {0, 0, "0"}, 82 | "5.10.test": {5, 10, "test"}, 83 | "5.10.test.test": {5, 10, "test.test"}, 84 | } 85 | 86 | for versionString, expected := range testValues { 87 | test, err := ParseVersion(versionString) 88 | if err != nil { 89 | t.Errorf("ParseVersion produced an unexpected error: %+v", err) 90 | } 91 | 92 | if expected != test { 93 | t.Errorf("ParseVersion did not return the expected Version."+ 94 | "\nexpected: %+v\nreceived: %+v", expected, test) 95 | } 96 | } 97 | } 98 | 99 | // Error path: tests various invalid Version strings return the expected error. 100 | func TestParseVersion_Error(t *testing.T) { 101 | // Test values 102 | testStrings := map[string]string{ 103 | "invalid version.": "3 parts", 104 | "": "3 parts", 105 | "0": "3 parts", 106 | "0.": "3 parts", 107 | "0.0": "3 parts", 108 | "0.0.": "patch cannot be empty", 109 | "a.0.0": "major version", 110 | "0.a.0": "minor version", 111 | "a.a.a": "major version", 112 | ".": "3 parts", 113 | "..": "major version", 114 | "...": "major version", 115 | "18446744073709551615.0.0": "major", 116 | } 117 | 118 | // Check that all the test strings produce the expected errors 119 | for str, expectedErr := range testStrings { 120 | _, err := ParseVersion(str) 121 | if err == nil || !strings.Contains(err.Error(), expectedErr) { 122 | t.Errorf("ParseVersion did not produce the expected error for \"%s\"."+ 123 | "\nexpected: %s\nreceived: %+v", str, expectedErr, err) 124 | } 125 | } 126 | } 127 | 128 | // Happy path. 129 | func TestIsCompatible(t *testing.T) { 130 | // Test values 131 | testValues := []struct{ required, current Version }{ 132 | {Version{1, 17, "51"}, Version{1, 17, "51"}}, 133 | {Version{5, 42, "51"}, Version{5, 42, "0"}}, 134 | {Version{0, 14, "51"}, Version{0, 15, ""}}, 135 | {Version{9, 72, ""}, Version{9, 73, ""}}, 136 | } 137 | 138 | for i, v := range testValues { 139 | if !IsCompatible(v.required, v.current) { 140 | t.Errorf("IsCompatible incorectly determined the current version"+ 141 | "is not comptabile with the required version (%d)."+ 142 | "\nrequired: %s\ncurrent: %s", i, v.required, v.current) 143 | } 144 | } 145 | } 146 | 147 | // Error path: tests multiple cases of invalid Version objects. 148 | func TestIsCompatible_Failure(t *testing.T) { 149 | // Test values 150 | testValues := []struct{ required, current Version }{ 151 | {Version{5, 41, "patch5"}, Version{5, 40, "patch5"}}, 152 | {Version{4, 15, "patch0"}, Version{5, 15, "patch0"}}, 153 | {Version{0, 14, "patch9"}, Version{0, 13, ""}}, 154 | } 155 | 156 | for i, v := range testValues { 157 | if IsCompatible(v.required, v.current) { 158 | t.Errorf("IsCompatible incorectly determined the current version"+ 159 | "is comptabile with the required version (%d)."+ 160 | "\nrequired: %s\ncurrent: %s", i, v.required, v.current) 161 | } 162 | } 163 | } 164 | 165 | // Happy path: tests that the same Version objects are equal. 166 | func TestEqual_Same(t *testing.T) { 167 | // Test values 168 | testValues := []struct{ a, b Version }{ 169 | {Version{}, Version{}}, 170 | {Version{1, 1, "1"}, Version{1, 1, "1"}}, 171 | {Version{4, 15, "patch0"}, Version{4, 15, "patch0"}}, 172 | } 173 | 174 | for i, v := range testValues { 175 | if !Equal(v.a, v.b) { 176 | t.Errorf("Equal determined the versions are not equal (%d)."+ 177 | "\na: %s\nb: %s", i, v.a, v.b) 178 | } 179 | } 180 | } 181 | 182 | // Happy path: tests that differing Version objects are not equal. 183 | func TestEqual_Different(t *testing.T) { 184 | // Test values 185 | testValues := []struct{ a, b Version }{ 186 | {Version{major: 1}, Version{minor: 1}}, 187 | {Version{1, 0, "1"}, Version{1, 2, "1"}}, 188 | {Version{1, 1, "1"}, Version{1, 1, "2"}}, 189 | {Version{1, 1, "1"}, Version{2, 1, "1"}}, 190 | } 191 | 192 | for i, v := range testValues { 193 | if Equal(v.a, v.b) { 194 | t.Errorf("Equal determined the versions are equal (%d)."+ 195 | "\na: %s\nb: %s", i, v.a, v.b) 196 | } 197 | } 198 | } 199 | 200 | // Happy path: tests that the same Version objects are equal. 201 | func TestCmp(t *testing.T) { 202 | // Test values 203 | testValues := []struct { 204 | expected int 205 | a, b Version 206 | }{ 207 | {0, Version{}, Version{}}, 208 | {0, Version{1, 1, ""}, Version{1, 1, ""}}, 209 | {0, Version{4, 15, ""}, Version{4, 15, ""}}, 210 | {1, Version{4, 15, ""}, Version{3, 15, ""}}, 211 | {1, Version{4, 15, ""}, Version{4, 14, ""}}, 212 | {-1, Version{4, 15, ""}, Version{5, 15, ""}}, 213 | {-1, Version{4, 15, ""}, Version{4, 16, ""}}, 214 | } 215 | 216 | for i, v := range testValues { 217 | test := Cmp(v.a, v.b) 218 | if v.expected != test { 219 | t.Errorf("Cmp did not return the expected value for %s and %s (%d)."+ 220 | "\nexpected: %d\nreceived: %d", v.a, v.b, i, v.expected, test) 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /fact/fact_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package fact 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "reflect" 14 | "strings" 15 | "testing" 16 | ) 17 | 18 | // Tests that NewFact returns a correctly formatted Fact. 19 | func TestNewFact(t *testing.T) { 20 | tests := []struct { 21 | ft FactType 22 | fact string 23 | expected Fact 24 | }{ 25 | {Username, "myUsername", Fact{"myUsername", Username}}, 26 | {Email, "email@example.com", Fact{"email@example.com", Email}}, 27 | {Phone, "8005559486US", Fact{"8005559486US", Phone}}, 28 | {Nickname, "myNickname", Fact{"myNickname", Nickname}}, 29 | } 30 | 31 | for i, tt := range tests { 32 | fact, err := NewFact(tt.ft, tt.fact) 33 | if err != nil { 34 | t.Errorf("Failed to make new fact (%d): %+v", i, err) 35 | } else if !reflect.DeepEqual(tt.expected, fact) { 36 | t.Errorf("Unexpected new Fact (%d).\nexpected: %s\nreceived: %s", 37 | i, tt.expected, fact) 38 | } 39 | } 40 | } 41 | 42 | // Error path: Tests that NewFact returns error when a fact exceeds the 43 | // maxFactLen. 44 | func TestNewFact_ExceedMaxFactError(t *testing.T) { 45 | _, err := NewFact(Email, 46 | "devinputvalidation_devinputvalidation_devinputvalidation@elixxir.io") 47 | if err == nil { 48 | t.Fatal("Expected error when the fact is longer than the maximum " + 49 | "character length.") 50 | } 51 | 52 | } 53 | 54 | // Error path: Tests that NewFact returns error when the fact is not valid. 55 | func TestNewFact_InvalidFactError(t *testing.T) { 56 | _, err := NewFact(Nickname, "hi") 57 | if err == nil { 58 | t.Fatal("Expected error when the fact is invalid.") 59 | } 60 | } 61 | 62 | // Tests that a Fact marshalled by Fact.Stringify and unmarshalled by 63 | // UnstringifyFact matches the original. 64 | func TestFact_Stringify_UnstringifyFact(t *testing.T) { 65 | facts := []Fact{ 66 | {"myUsername", Username}, 67 | {"email@example.com", Email}, 68 | {"8005559486US", Phone}, 69 | {"myNickname", Nickname}, 70 | } 71 | 72 | for i, expected := range facts { 73 | factString := expected.Stringify() 74 | fact, err := UnstringifyFact(factString) 75 | if err != nil { 76 | t.Errorf( 77 | "Failed to unstringify fact %s (%d): %+v", expected, i, err) 78 | } else if !reflect.DeepEqual(expected, fact) { 79 | t.Errorf("Unexpected unstringified Fact %s (%d)."+ 80 | "\nexpected: %s\nreceived: %s", 81 | factString, i, expected, fact) 82 | } 83 | } 84 | } 85 | 86 | // Consistency test of Fact.Stringify. 87 | func TestFact_Stringify(t *testing.T) { 88 | tests := []struct { 89 | fact Fact 90 | expected string 91 | }{ 92 | {Fact{"myUsername", Username}, "UmyUsername"}, 93 | {Fact{"email@example.com", Email}, "Eemail@example.com"}, 94 | {Fact{"8005559486US", Phone}, "P8005559486US"}, 95 | {Fact{"myNickname", Nickname}, "NmyNickname"}, 96 | } 97 | 98 | for i, tt := range tests { 99 | factString := tt.fact.Stringify() 100 | 101 | if factString != tt.expected { 102 | t.Errorf("Unexpected strified Fact %s (%d)."+ 103 | "\nexpected: %s\nreceived: %s", 104 | tt.fact, i, tt.expected, factString) 105 | } 106 | } 107 | } 108 | 109 | // Consistency test of UnstringifyFact 110 | func TestUnstringifyFact(t *testing.T) { 111 | tests := []struct { 112 | factString string 113 | expected Fact 114 | }{ 115 | {"UmyUsername", Fact{"myUsername", Username}}, 116 | {"Eemail@example.com", Fact{"email@example.com", Email}}, 117 | {"P8005559486US", Fact{"8005559486US", Phone}}, 118 | {"NmyNickname", Fact{"myNickname", Nickname}}, 119 | } 120 | 121 | for i, tt := range tests { 122 | fact, err := UnstringifyFact(tt.factString) 123 | if err != nil { 124 | t.Errorf( 125 | "Failed to unstringify fact %s (%d): %+v", tt.factString, i, err) 126 | } else if !reflect.DeepEqual(tt.expected, fact) { 127 | t.Errorf("Unexpected unstringified Fact %s (%d)."+ 128 | "\nexpected: %s\nreceived: %s", 129 | tt.factString, i, tt.expected, fact) 130 | } 131 | } 132 | } 133 | 134 | // Error path: Tests all error paths of UnstringifyFact. 135 | func TestUnstringifyFact_Error(t *testing.T) { 136 | longFact := strings.Repeat("A", maxFactLen+1) 137 | tests := []struct { 138 | factString string 139 | expectedErr string 140 | }{ 141 | {"", "stringified facts must at least have a type at the start"}, 142 | {longFact, fmt.Sprintf("Fact (%s) exceeds maximum character limit for "+ 143 | "a fact (%d characters)", longFact, maxFactLen)}, 144 | {"P", "stringified facts must be at least 1 character long"}, 145 | {"QA", `Failed to unstringify fact type for "QA"`}, 146 | } 147 | 148 | for i, tt := range tests { 149 | _, err := UnstringifyFact(tt.factString) 150 | if err == nil || !strings.Contains(err.Error(), tt.expectedErr) { 151 | t.Errorf("Unexpected error when Unstringifying fact %q (%d)."+ 152 | "\nexpected: %s\nreceived: %+v", 153 | tt.factString, i, tt.expectedErr, err) 154 | } 155 | } 156 | } 157 | 158 | // Consistency test of Fact.Normalized. 159 | func TestFact_Normalized(t *testing.T) { 160 | tests := []struct { 161 | fact Fact 162 | expected string 163 | }{ 164 | {Fact{"myUsername", Username}, "MYUSERNAME"}, 165 | {Fact{"email@example.com", Email}, "EMAIL@EXAMPLE.COM"}, 166 | {Fact{"8005559486US", Phone}, "8005559486US"}, 167 | {Fact{"myNickname", Nickname}, "MYNICKNAME"}, 168 | } 169 | 170 | for i, tt := range tests { 171 | normal := tt.fact.Normalized() 172 | if normal != tt.expected { 173 | t.Errorf("Unexpected new normalized Fact %v (%d)."+ 174 | "\nexpected: %q\nreceived: %q", tt.fact, i, tt.expected, normal) 175 | } 176 | } 177 | } 178 | 179 | // Tests that ValidateFact correctly validates various facts. 180 | func TestValidateFact(t *testing.T) { 181 | facts := []Fact{ 182 | {"myUsername", Username}, 183 | {"email@example.com", Email}, 184 | {"8005559486US", Phone}, 185 | {"myNickname", Nickname}, 186 | } 187 | 188 | for i, fact := range facts { 189 | err := ValidateFact(fact) 190 | if err != nil { 191 | t.Errorf( 192 | "Failed to validate fact %s (%d): %+v", fact, i, err) 193 | } 194 | } 195 | } 196 | 197 | // Error path: Tests that ValidateFact does not validate invalid facts 198 | func TestValidateFact_InvalidFactsError(t *testing.T) { 199 | facts := []Fact{ 200 | {"test@gmail@gmail.com", Email}, 201 | {"US8005559486", Phone}, 202 | {"020 8743 8000135UK", Phone}, 203 | {"me", Nickname}, 204 | {"me", 99}, 205 | } 206 | 207 | for i, fact := range facts { 208 | err := ValidateFact(fact) 209 | if err == nil { 210 | t.Errorf("Did not error on invalid fact %s (%d)", fact, i) 211 | } 212 | } 213 | } 214 | 215 | // Error path: Tests all error paths of validateNumber. 216 | func Test_validateNumber_Error(t *testing.T) { 217 | tests := []struct { 218 | number, countryCode string 219 | expectedErr string 220 | }{ 221 | {"5", "", "Number or input are of length 0"}, 222 | {"", "US", "Number or input are of length 0"}, 223 | // {"020 8743 8000135", "UK", `Could not parse number "020 8743 8000135"`}, 224 | {"8005559486", "UK", `Could not parse number "8005559486"`}, 225 | {"+343511234567", "ES", `Could not validate number "+343511234567"`}, 226 | } 227 | 228 | for i, tt := range tests { 229 | err := validateNumber(tt.number, tt.countryCode) 230 | if err == nil || !strings.Contains(err.Error(), tt.expectedErr) { 231 | t.Errorf("Unexpected error when validating number %q with country "+ 232 | "code %q (%d).\nexpected: %s\nreceived: %+v", 233 | tt.number, tt.countryCode, i, tt.expectedErr, err) 234 | } 235 | } 236 | } 237 | 238 | // Tests that a Fact JSON marshalled and unmarshalled matches the original. 239 | func TestFact_JsonMarshalUnmarshal(t *testing.T) { 240 | facts := []Fact{ 241 | {"myUsername", Username}, 242 | {"email@example.com", Email}, 243 | {"8005559486US", Phone}, 244 | {"myNickname", Nickname}, 245 | } 246 | 247 | for i, expected := range facts { 248 | data, err := json.Marshal(expected) 249 | if err != nil { 250 | t.Errorf("Failed to JSON marshal %s (%d): %+v", expected, i, err) 251 | } 252 | 253 | var fact Fact 254 | if err = json.Unmarshal(data, &fact); err != nil { 255 | t.Errorf("Failed to JSON unmarshal %s (%d): %+v", expected, i, err) 256 | } 257 | 258 | if !reflect.DeepEqual(expected, fact) { 259 | t.Errorf("Unexpected unmarshalled fact (%d)."+ 260 | "\nexpected: %+v\nreceived: %+v", i, expected, fact) 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /notifications/data_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package notifications 9 | 10 | import ( 11 | "math/rand" 12 | "reflect" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | // Tests that a list of Data CSV encoded by BuildNotificationCSV and decoded bu 18 | // DecodeNotificationsCSV matches the original. 19 | func TestBuildNotificationCSV_DecodeNotificationsCSV(t *testing.T) { 20 | rng := rand.New(rand.NewSource(186745)) 21 | expected := make([]*Data, 50) 22 | for i := range expected { 23 | identityFP, messageHash := make([]byte, 25), make([]byte, 32) 24 | rng.Read(messageHash) 25 | rng.Read(identityFP) 26 | expected[i] = &Data{IdentityFP: identityFP, MessageHash: messageHash} 27 | } 28 | 29 | csvData, _ := BuildNotificationCSV(expected, 9999) 30 | dataList, err := DecodeNotificationsCSV(string(csvData)) 31 | if err != nil { 32 | t.Errorf("Failed to decode notifications CSV: %+v", err) 33 | } 34 | 35 | if !reflect.DeepEqual(expected, dataList) { 36 | t.Errorf("The generated Data list does not match the original."+ 37 | "\nexpected: %v\nreceived: %v", expected, dataList) 38 | } 39 | } 40 | 41 | // Consistency test of BuildNotificationCSV. 42 | func TestBuildNotificationCSV(t *testing.T) { 43 | expected := `U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=,39ebTXZCm2F6DJ+fDTulWwzA1hRMiIU1hA== 44 | GsvgcJsHWAg/YdN1vAK0HfT5GSnhj9qeb4LlTnSOgec=,nku9b+NM3LqEPujWPoxP/hzr6lRtj6wT3Q== 45 | GqwEzi6ih3xVec+ix44bC6+uiBuCp1EQikLtPJA8qkM=,Rlp4YgYWl4rtDOPGxPOue8PgSVtXEv79vg== 46 | DBAoh+EA2s0tiF9pLLYH2gChHBxwceeWotwtwlpbdLI=,4SlwXic/BckjJoKOKwVuOBdljhBhSYlH/Q== 47 | 80RBDtTBFgI/qONXa2/tJ/+JdLrAyv2a0FaSsTYZ5zg=,lk39x56NU0NzZhz9ZtdP7B4biUkatyNuSw== 48 | dSFikM8r60LDyicyhWDxqsBnzqbov0bUqytGgEAsX7I=,gg6IXTJg8d6XgoPUoJo2+WwglBdG4+1Npg== 49 | Rqmui0+ntPw6ILr6GnXtMnqGuLDDmvHP0rO1EhnqeVM=,Or9EjSxHnTJgdTOQWRTIzBzwnaOeDpKdAg== 50 | Sry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQiR9ZdeKSo=,oriqBHxhzbMzc+vnLCegmMAhl9rmtzLDUQ== 51 | 32aPh04snxzgnKhgF+fiF0gwP/QcGyPhHEjtF1OdaF8=,dvKnmLxk3g5dsoZLKtPCbOY4I0J2WhPWlg== 52 | 5S33YPbDRl4poNykasOg1XATO8IVcfX1SmQxBVE/2EI=,mxlK4bqfKoOGrnKzZh/oLCrGTb9GFRgk4g== 53 | MFMSY3yZwrh9bfDdXvKDZxkHLWcvYfqgvob0V5Iew3w=,DkYM8NcD0H3F9WYaRQEzQJpxK2pmq9e6ZQ== 54 | IkyiaXjZpc5i/rEag48WYi61TO4+Z1UinBg8GTOpFlg=,Xhg7twkZLbDmyNcJudc4O5k8aUmZRbCwzw== 55 | 49wuwfyWENfusZ0JFqJ0I8KeRC8OMcLJU5Zg8F+zfkU=,zRvwvPwaNGxDTxHPAEFvphaVuSAuaDY6HA== 56 | eH9HhOCu2ceFZBhOEx8efIEfvYhbzGc06JM/PLLyXVI=,+fjHVHrX4dYnjJ98hy+ED52U2f3trpPbJA== 57 | lXGPWAuMjyvsxqp2w7D5SK++YSelz9VrwRs8Lqg3ocY=,aagi92hk7CrgzWv93yGxFER0v9N80ga1Gg== 58 | zgUKthmex7OW1hj94OGimZpvPZ+LergUn3Leulxs1P0=,TTkskrSyGsgSA0Bi38MGOnpoYrD+8QUpGQ== 59 | wqh6SZT8HkAeEcWknH6OqeZdbMQEZf01LyxC7D0+9g0=,tpdAUX3HZSue7/UWU1qhyfM9sT7R964b4w== 60 | hBMjKNED+HGvm80VIzw5OXj1wXCJ6PMmegzMfjm/ysc=,rEK+LBcsYkPRBjMDbT1GuBkWrkb/E9amsg== 61 | +tkHnW3zRAWQKWZ7LrQaQAEXVW/ly0BbMXCsrKXHW68=,f3tw6GFE07oDazsfWP5CeVDn0E9MJuvhLw== 62 | 1eEjcZgIogS4Ubps8spsRu2IFi9dRc21oHY65+GDP7c=,rfmJNvUeTdqKKE7xmoW7h0N7QQMwWjs4bA== 63 | fTbZLSJUmWCnFPKoKeHCAhZzvzDFC2edUFaJVcnBmAg=,nZX2A5fSr1+PyREL46nhJelEhJeXCNaqfA== 64 | /GKejZFHzy9ftqBVkauGhzoerQWkpmcdaVFcg53Yrzo=,Otd0AsX9OoOgRgipiTMAIWLdTB/1VH9XUg== 65 | Ax8hIeFBCKpaV0VsrpHBcymtWs5h6um2Ut8zALTCq1g=,J3bYW2jKMtXDc8JkeFg7xI+ja+SNZZw/4Q== 66 | c0EBx+SP03+5+uPwu06bbfR1Ki6RZM8F9WjSyJ6k1l0=,dgYOZIeQWTJLt1rbFBovfC/eeBH0gc8Iag== 67 | PsPYs3cAEv0npLZbAq6FJW9zbt4+TdhXIJV1pIjVdA0=,L3JpWlcNvyZH8pXiM5Xu2s/2NuGwzyDeag== 68 | EP+ZQ+3Kb5a/TdrwC51PzWrL27P2MZRQNYaopliuYLU=,7lOata0Z8roj3KZn36ZVE0xZSiyAa9+k5w== 69 | VuRYtIuuSQ0ELgejVels+4nMq/KBnXlNnhKC/QpyVPE=,s5T26rxmpki639tH01CKaTgLpg1f9LQyew== 70 | 9lDgExuPV3WthpenNGPNKAbmru75K16b/+QOlGaZD6M=,rsEeSny2rrsXt/7SlRPTHtT/HRbm1ZlWGQ== 71 | UV9fU4dpAO0PetHyOLszRAnjwWSVc6VvQ6jh0hNyRvw=,psYzJNQ/g+wNTS/WUG/f7uIeJDI9gOfLhA== 72 | X0PSIyKapCEUSifbt8RAwceY+aJNLIXxLCSIv4fS2Sk=,oKR5pVt7c+TvskFDTjbUT315OI2hnlz+gw== 73 | IWN1mCbfOfgzaVyiKqZRlUiQvNzPZq09c6jhq5+Dh30=,Jju7J/W8SXvWVEdNy4YqtN1om6BNDa5ooA== 74 | g9al6HTEHOSudp3dtiHBZDI5vTeKLpGprOJ38sCNcUs=,ydnmLAViyiluEqd2F0TduCOoLxm6fQpSSw== 75 | VJK79yrDTvy5Cl7fbbwhn78w7PJfpmbJJGsIHV0sV44=,7wAIsI1hoJdkBPQuqCpIc/sNZId3faZHBg== 76 | t0hXpZ8dKn82F6O81VqVn9GSBMLjv6zg5gMLfABpuXw=,aQyYNMIoKbqb1P+pr1gZb3deMPPJO0nsLw== 77 | Hoo35EiId/9sfiQlH+FQ7OfMWvss7XprvKzj7qAb11k=,QA2HuYCzU8EVy8Fip3jdnqBCNZ1MIP4hig== 78 | Rm/cqgfzRclH5aCWoj+JZ89P4Si96pz8xljy7bEkkpw=,3M9Yj0lOvjNGwZrteHuXxXcN/t6EXPWwQA== 79 | 3LYIlEhmP8MyF8HyL7TKpWBOFiDDl7Oo40e3k0PkPl4=,lPyl5AhHBG352IgCviQSoTRntmVWLzKHSA== 80 | 5IPF6phRI8xCLk96jOl0B1OPYfZ+ga42GtW89w8iiDE=,aw4ukENMK3yiyg2KICMlx7gMtjXoXb0jNw== 81 | QNWTeKISlTt5F8x/RdbsAU0fC1kNaLRRMzwAisvlEjE=,+4CfIcugABlRkeMY0PNJ84IlHeA7NfV9zw== 82 | UrloJgqUXJGcj7n7jfqEfWb7oCNK27w240akwcvimRg=,FGu6CxanGNbECj5ZdsoEaGR0nEgEx5zJrQ== 83 | ZLZ2Bw9hP9+WSKJW3DwiOkvOiRWUK9lrAHMdrZWDfD8=,r/8aTMECHltCu3V4nHCj932lPCXgSLkLqg== 84 | HrARGizMUEcrKECJa840U6mtBJct5H/GZEahdvtaE8I=,Xcu6Vrv2NV4bKvhmVDH3RyqWYYFmnxAfWg== 85 | Vyy0GiAUFyBexvVbintbSsYQjuBFVTHkOGRH9fTJGdw=,S77jKfBIvvwO5ArLSmxuEHLQQwBQjdXzWw== 86 | LPwGgdnQAZaEWYyCdG1Zk/AB99k9z/INedKtTv1e5Ow=,qyjyubYZBFj+NsS3dayvYMFUI5W2jO9WjQ== 87 | OWA4Tr2KTqoq6+xmTlY4cNuAPSgOPmJwo7D+A4vILZw=,gw/oRNJWsLXpYvMxM58T2FKXOynKoD6QFA== 88 | qIfiAe4BFutxC8au4sJOXZBExUpNymRkA2w2FMafnII=,PFvyIccm6amL8jQBONIh2lPeVMi1Bvk/fg== 89 | AcsU15TF3uaMZzKcHTyptNP7EBq5eBYhI2vBK/rFKCQ=,Gcam+D1Hzebx9Zs8AHd3yAALcOHAyJAiuQ== 90 | 2xNm0x0FAN2fAkPW6rUP0gFhx0hJw94sUaubeM+WWRA=,iC3H9TvHMgsc9IRy9ks2Qd/TaY9zTNkOXA== 91 | A3hMWMAcrvqWoVNZPxQqYFWLMoCUCnrl2NArseYXnTk=,WsPBzNwVH8QF0fcpHDoq7po6JHhgL9Zcew== 92 | ` 93 | extra := "Zq3/Nor7+NgAzkvg7LxVOYyRMMnAEDxkHpGnKpeHltc=,wGc+G+CLk/qEIoGMQ0XBZlyHkiYS3r7nkw==\n" 94 | 95 | rng := rand.New(rand.NewSource(42)) 96 | dataList := make([]*Data, 50) 97 | for i := range dataList { 98 | identityFP, messageHash := make([]byte, 25), make([]byte, 32) 99 | rng.Read(messageHash) 100 | rng.Read(identityFP) 101 | dataList[i] = &Data{IdentityFP: identityFP, MessageHash: messageHash} 102 | } 103 | 104 | csv, rest := BuildNotificationCSV(dataList, 4096) 105 | second, _ := BuildNotificationCSV(rest, 4096) 106 | if expected != string(csv) { 107 | t.Errorf("First pass mismatch.\nexpected:\n[%s]\n\nreceived:\n[%s]", 108 | expected, string(csv)) 109 | } 110 | if extra != string(second) { 111 | t.Errorf("Second pass mismatch.\nexpected:\n[%s]\n\nreceived:\n[%s]", 112 | extra, string(second)) 113 | } 114 | } 115 | 116 | func TestBuildNotificationCSV_small(t *testing.T) { 117 | expected := `U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=,39ebTXZCm2F6DJ+fDTulWwzA1hRMiIU1hA== 118 | GsvgcJsHWAg/YdN1vAK0HfT5GSnhj9qeb4LlTnSOgec=,nku9b+NM3LqEPujWPoxP/hzr6lRtj6wT3Q== 119 | ` 120 | rng := rand.New(rand.NewSource(42)) 121 | dataList := make([]*Data, 2) 122 | for i := range dataList { 123 | identityFP, messageHash := make([]byte, 25), make([]byte, 32) 124 | rng.Read(messageHash) 125 | rng.Read(identityFP) 126 | dataList[i] = &Data{IdentityFP: identityFP, MessageHash: messageHash} 127 | } 128 | 129 | csv, rest := BuildNotificationCSV(dataList, 4096) 130 | if expected != string(csv) { 131 | t.Errorf("First pass mismatch.\nexpected:\n[%s]\n\nreceived:\n[%s]", 132 | expected, string(csv)) 133 | } 134 | if len(rest) != 0 { 135 | t.Errorf("Should not have been any overflow, but got %+v", rest) 136 | } 137 | } 138 | 139 | // Error path: Tests that DecodeNotificationsCSV returns the expected error for 140 | // an invalid MessageHash. 141 | func TestDecodeNotificationsCSV_InvalidMessageHashError(t *testing.T) { 142 | invalidCSV := `U4x/lrFkvxuXu59LtHLonnZND6SugndnVI=,39ebTXZCm2F6DJ+fDTulWwzA1hRMiIU1hA== 143 | ` 144 | expectedErr := "Failed to decode MessageHash for record 0 of 1" 145 | _, err := DecodeNotificationsCSV(invalidCSV) 146 | if err == nil || !strings.Contains(err.Error(), expectedErr) { 147 | t.Errorf("Unexpected error for invalid MessageHash."+ 148 | "\nexpected: %s\nreceived: %+v", expectedErr, err) 149 | } 150 | } 151 | 152 | // Error path: Tests that DecodeNotificationsCSV returns the expected error for 153 | // an invalid identityFP. 154 | func TestDecodeNotificationsCSV_InvalididentityFPError(t *testing.T) { 155 | invalidCSV := `U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=,39ebTXZCm2F6DJ1hRMiIU1hA== 156 | ` 157 | expectedErr := "Failed to decode IdentityFP for record 0 of 1" 158 | _, err := DecodeNotificationsCSV(invalidCSV) 159 | if err == nil || !strings.Contains(err.Error(), expectedErr) { 160 | t.Errorf("Unexpected error for invalid identityFP."+ 161 | "\nexpected: %s\nreceived: %+v", expectedErr, err) 162 | } 163 | } 164 | 165 | // Error path: Tests that DecodeNotificationsCSV returns the expected error for 166 | // an invalid identityFP. 167 | func TestDecodeNotificationsCSV_NoEofError(t *testing.T) { 168 | invalidCSV := `U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=,39ebTXZCm2F6DJ+fDTulWwzA1hRMiIU1hA==,"` 169 | expectedErr := "Failed to read notifications CSV records." 170 | _, err := DecodeNotificationsCSV(invalidCSV) 171 | if err == nil || !strings.Contains(err.Error(), expectedErr) { 172 | t.Errorf("Unexpected error for invalid identityFP."+ 173 | "\nexpected: %s\nreceived: %+v", expectedErr, err) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /format/message.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package format 9 | 10 | import ( 11 | "encoding/base64" 12 | "encoding/binary" 13 | "fmt" 14 | "strconv" 15 | 16 | "golang.org/x/crypto/blake2b" 17 | 18 | jww "github.com/spf13/jwalterweatherman" 19 | ) 20 | 21 | const ( 22 | KeyFPLen = 32 23 | MacLen = 32 24 | EphemeralRIDLen = 8 25 | SIHLen = 25 26 | RecipientIDLen = EphemeralRIDLen + SIHLen 27 | 28 | MinimumPrimeSize = 2*MacLen + RecipientIDLen 29 | 30 | AssociatedDataSize = KeyFPLen + MacLen + RecipientIDLen 31 | 32 | messagePayloadVersion = 0 33 | ) 34 | 35 | /* 36 | Message Structure (not to scale) 37 | +----------------------------------------------------------------------------------------------------+ 38 | | Message | 39 | | 2*primeSize bits | 40 | +------------------------------------------+---------------------------------------------------------+ 41 | | payloadA | payloadB | 42 | | primeSize bits | primeSize bits | 43 | +---------+----------+---------------------+---------+-------+-----------+--------------+------------+ 44 | | grpBitA | keyFP |version| Contents1 | grpBitB | MAC | Contents2 | ephemeralRID | SIH | 45 | | 1 bit | 255 bits |1 byte | *below* | 1 bit | 255 b | *below* | 64 bits | 200 bits | 46 | + --------+----------+---------------------+---------+-------+-----------+--------------+------------+ 47 | | Raw Contents | 48 | | 2*primeSize - recipientID bits | 49 | +------------------------------------------------------------------------+ 50 | 51 | - size: size in bits of the data which is stored 52 | - Contents1 size = primeSize - grpBitASize - KeyFPLen - sizeSize - 1 53 | - Contents2 size = primeSize - grpBitBSize - MacLen - RecipientIDLen - timestampSize 54 | - the size of the data in the two contents fields is stored within the "size" field 55 | 56 | /////Adherence to the group///////////////////////////////////////////////////// 57 | The first bits of keyFingerprint and MAC are enforced to be 0, thus ensuring 58 | PayloadA and PayloadB are within the group 59 | */ 60 | 61 | // Message structure stores all the data serially. Subsequent fields point to 62 | // subsections of the serialised data. 63 | type Message struct { 64 | data []byte 65 | 66 | // Note: These are mapped to locations in the data object 67 | payloadA []byte 68 | payloadB []byte 69 | 70 | keyFP []byte 71 | version []byte 72 | contents1 []byte 73 | mac []byte 74 | contents2 []byte 75 | ephemeralRID []byte // Ephemeral reception ID 76 | sih []byte // Service Identification Hash 77 | 78 | rawContents []byte 79 | } 80 | 81 | // NewMessage creates a new empty message based upon the size of the encryption 82 | // primes. All subcomponents point to locations in the internal data buffer. 83 | // Panics if the prime size to too small. 84 | func NewMessage(numPrimeBytes int) Message { 85 | if numPrimeBytes < MinimumPrimeSize { 86 | jww.FATAL.Panicf("Failed to create new Message: minimum prime length "+ 87 | "is %d, received prime size is %d.", MinimumPrimeSize, numPrimeBytes) 88 | } 89 | 90 | data := make([]byte, 2*numPrimeBytes) 91 | 92 | return Message{ 93 | data: data, 94 | 95 | payloadA: data[:numPrimeBytes], 96 | payloadB: data[numPrimeBytes:], 97 | 98 | keyFP: data[:KeyFPLen], 99 | version: data[KeyFPLen : KeyFPLen+1], 100 | contents1: data[1+KeyFPLen : numPrimeBytes], 101 | 102 | mac: data[numPrimeBytes : numPrimeBytes+MacLen], 103 | contents2: data[numPrimeBytes+MacLen : 2*numPrimeBytes-RecipientIDLen], 104 | ephemeralRID: data[2*numPrimeBytes-RecipientIDLen : 2*numPrimeBytes-SIHLen], 105 | sih: data[2*numPrimeBytes-SIHLen:], 106 | 107 | rawContents: data[:2*numPrimeBytes-RecipientIDLen], 108 | } 109 | } 110 | 111 | // Marshal marshals the message into a byte slice. Use this when 112 | // sending over the wire or other socket connection. Do not use this 113 | // if you ever want to compare a marshalled message with itself, because 114 | // both the Ephemeral ID and SIH are modified on each send attempt. 115 | func (m *Message) Marshal() []byte { 116 | return copyByteSlice(m.data) 117 | } 118 | 119 | // MarshalImmutable marshals the message into a byte slice. Note that the 120 | // Ephemeral ID and the SIH both change every time a message is 121 | // sent. This function 0's those fields to guarantee that the same 122 | // message will be byte identical with itself when Marshalled. 123 | func (m *Message) MarshalImmutable() []byte { 124 | newM := m.Copy() 125 | newM.SetEphemeralRID(make([]byte, EphemeralRIDLen)) 126 | newM.SetSIH(make([]byte, SIHLen)) 127 | return newM.data 128 | } 129 | 130 | // Unmarshal unmarshalls a byte slice into a new Message. 131 | func Unmarshal(b []byte) (Message, error) { 132 | m := NewMessage(len(b) / 2) 133 | copy(m.data, b) 134 | 135 | // if m.Version() != messagePayloadVersion { 136 | // return Message{}, fmt.Errorf( 137 | // "message encoding version mismatch, got %d expected %d", 138 | // m.Version(), messagePayloadVersion) 139 | // } 140 | 141 | return m, nil 142 | } 143 | 144 | // Version returns the encoding version. 145 | func (m *Message) Version() uint8 { 146 | return m.version[0] 147 | } 148 | 149 | // Copy returns a copy of the message. 150 | func (m Message) Copy() Message { 151 | m2 := NewMessage(len(m.data) / 2) 152 | copy(m2.data, m.data) 153 | return m2 154 | } 155 | 156 | // GetPrimeByteLen returns the size of the prime used. 157 | func (m Message) GetPrimeByteLen() int { 158 | return len(m.data) / 2 159 | } 160 | 161 | // GetPayloadA returns payload A, which is the first half of the message. 162 | func (m Message) GetPayloadA() []byte { 163 | return copyByteSlice(m.payloadA) 164 | } 165 | 166 | // SetPayloadA copies the passed byte slice into payload A. If the specified 167 | // byte slice is not exactly the same size as payload A, then it panics. 168 | func (m Message) SetPayloadA(payload []byte) { 169 | if len(payload) != len(m.payloadB) { 170 | jww.ERROR.Panicf("Failed to set Message payload A: length must be %d, "+ 171 | "length of received data is %d.", len(m.payloadA), len(payload)) 172 | } 173 | 174 | copy(m.payloadA, payload) 175 | } 176 | 177 | // GetPayloadB returns payload B, which is the last half of the message. 178 | func (m Message) GetPayloadB() []byte { 179 | return copyByteSlice(m.payloadB) 180 | } 181 | 182 | // SetPayloadB copies the passed byte slice into payload B. If the specified 183 | // byte slice is not exactly the same size as payload B, then it panics. 184 | func (m Message) SetPayloadB(payload []byte) { 185 | if len(payload) != len(m.payloadB) { 186 | jww.ERROR.Panicf("Failed to set Message payload B: length must be %d, "+ 187 | "length of received data is %d.", len(m.payloadB), len(payload)) 188 | } 189 | 190 | copy(m.payloadB, payload) 191 | } 192 | 193 | // ContentsSize returns the maximum size of the contents. 194 | func (m Message) ContentsSize() int { 195 | return len(m.data) - AssociatedDataSize - 1 196 | } 197 | 198 | // GetContents returns the exact contents of the message. This size of the 199 | // return is based on the size of the contents actually stored. 200 | func (m Message) GetContents() []byte { 201 | c := make([]byte, len(m.contents1)+len(m.contents2)) 202 | 203 | copy(c[:len(m.contents1)], m.contents1) 204 | copy(c[len(m.contents1):], m.contents2) 205 | 206 | return c 207 | } 208 | 209 | // SetContents sets the contents of the message. This overwrites any storage 210 | // already in the message but will not clear bits beyond the size of the passed 211 | // contents. Panics if the passed contents is larger than the maximum contents 212 | // size. 213 | func (m Message) SetContents(c []byte) { 214 | if len(c) > len(m.contents1)+len(m.contents2) { 215 | jww.ERROR.Panicf("Failed to set Message contents: length must be "+ 216 | "equal to or less than %d, length of received data is %d.", 217 | len(m.contents1)+len(m.contents2), len(c)) 218 | } 219 | 220 | if len(c) <= len(m.contents1) { 221 | copy(m.contents1, c) 222 | } else { 223 | copy(m.contents1, c[:len(m.contents1)]) 224 | copy(m.contents2, c[len(m.contents1):]) 225 | } 226 | } 227 | 228 | // GetRawContentsSize returns the exact contents of the message. 229 | func (m Message) GetRawContentsSize() int { 230 | return len(m.rawContents) 231 | } 232 | 233 | // GetRawContents returns the exact contents of the message. This field crosses 234 | // over the group barrier and the setter of this is responsible for ensuring the 235 | // underlying payloads are within the group. 236 | // flips the first bit to 0 on return 237 | func (m Message) GetRawContents() []byte { 238 | newRaw := copyByteSlice(m.rawContents) 239 | clearFirstBit(newRaw) 240 | newRaw[m.GetPrimeByteLen()] &= 0b01111111 241 | return newRaw 242 | } 243 | 244 | // SetRawContents sets the raw contents of the message. This field crosses over 245 | // the group barrier and the setter of this is responsible for ensuring the 246 | // underlying payloads are within the group. This will panic if the payload is 247 | // greater than the maximum size. This overwrites any storage already in the 248 | // message. If the passed contents is larger than the maximum contents size this 249 | // will panic. 250 | func (m Message) SetRawContents(c []byte) { 251 | if len(c) != len(m.rawContents) { 252 | jww.ERROR.Panicf("Failed to set Message raw contents: length must be %d, "+ 253 | "length of received data is %d.", len(m.rawContents), len(c)) 254 | } 255 | 256 | copy(m.rawContents, c) 257 | } 258 | 259 | // GetKeyFP gets the key Fingerprint 260 | // flips the first bit to 0 on return 261 | func (m Message) GetKeyFP() Fingerprint { 262 | newFP := NewFingerprint(m.keyFP) 263 | clearFirstBit(newFP[:]) 264 | return newFP 265 | } 266 | 267 | // SetKeyFP sets the key Fingerprint. Checks that the first bit of the Key 268 | // Fingerprint is 0, otherwise it panics. 269 | func (m Message) SetKeyFP(fp Fingerprint) { 270 | if fp[0]>>7 != 0 { 271 | jww.ERROR.Panicf("Failed to set Message key fingerprint: first bit " + 272 | "of provided data must be zero.") 273 | } 274 | 275 | copy(m.keyFP, fp.Bytes()) 276 | } 277 | 278 | // GetMac gets the MAC. 279 | // flips the first bit to 0 on return 280 | func (m Message) GetMac() []byte { 281 | newMac := copyByteSlice(m.mac) 282 | clearFirstBit(newMac) 283 | return newMac 284 | } 285 | 286 | // SetMac sets the MAC. Checks that the first bit of the MAC is 0, otherwise it 287 | // panics. 288 | func (m Message) SetMac(mac []byte) { 289 | if len(mac) != MacLen { 290 | jww.ERROR.Panicf("Failed to set Message MAC: length must be %d, "+ 291 | "length of received data is %d.", MacLen, len(mac)) 292 | } 293 | 294 | if mac[0]>>7 != 0 { 295 | jww.ERROR.Panicf("Failed to set Message MAC: first bit of provided " + 296 | "data must be zero.") 297 | } 298 | 299 | copy(m.mac, mac) 300 | } 301 | 302 | // GetEphemeralRID returns the ephemeral recipient ID. 303 | func (m Message) GetEphemeralRID() []byte { 304 | return copyByteSlice(m.ephemeralRID) 305 | } 306 | 307 | // SetEphemeralRID copies the ephemeral recipient ID bytes into the message. 308 | func (m Message) SetEphemeralRID(ephemeralRID []byte) { 309 | if len(ephemeralRID) != EphemeralRIDLen { 310 | jww.ERROR.Panicf("Failed to set Message ephemeral recipient ID: "+ 311 | "length must be %d, length of received data is %d.", 312 | EphemeralRIDLen, len(ephemeralRID)) 313 | } 314 | copy(m.ephemeralRID, ephemeralRID) 315 | } 316 | 317 | // GetSIH return the Service Identification Hash. 318 | func (m Message) GetSIH() []byte { 319 | return copyByteSlice(m.sih) 320 | } 321 | 322 | // SetSIH sets the Service Identification Hash, which should be generated via 323 | // fingerprint.IdentityFP. 324 | func (m Message) SetSIH(identityFP []byte) { 325 | if len(identityFP) != SIHLen { 326 | jww.ERROR.Panicf("Failed to set Service Identification Hash: length "+ 327 | "must be %d, length of received data is %d.", 328 | SIHLen, len(identityFP)) 329 | } 330 | copy(m.sih, identityFP) 331 | } 332 | 333 | // Digest gets a digest of the message contents, primarily used for debugging 334 | func (m Message) Digest() string { 335 | return DigestContents(m.GetContents()) 336 | } 337 | 338 | // DigestContents - message.Digest that works without the message format 339 | func DigestContents(c []byte) string { 340 | h, _ := blake2b.New256(nil) 341 | h.Write(c) 342 | d := h.Sum(nil) 343 | digest := base64.StdEncoding.EncodeToString(d[:15]) 344 | return digest[:20] 345 | } 346 | 347 | // copyByteSlice is a helper function to make a copy of a byte slice. 348 | func copyByteSlice(s []byte) []byte { 349 | c := make([]byte, len(s)) 350 | copy(c, s) 351 | return c 352 | } 353 | 354 | // GoString returns the Message key fingerprint, MAC, ephemeral recipient ID, 355 | // identity fingerprint, and contents as a string. This functions satisfies the 356 | // fmt.GoStringer interface. 357 | func (m Message) GoString() string { 358 | mac := "" 359 | if len(m.mac) > 0 { 360 | mac = base64.StdEncoding.EncodeToString(m.GetMac()) 361 | } 362 | keyFP := "" 363 | if len(m.keyFP) > 0 { 364 | keyFP = m.GetKeyFP().String() 365 | } 366 | ephID := "" 367 | if len(m.ephemeralRID) > 0 { 368 | ephID = strconv.FormatUint(binary.BigEndian.Uint64(m.GetEphemeralRID()), 10) 369 | } 370 | sih := "" 371 | if len(m.sih) > 0 { 372 | sih = base64.StdEncoding.EncodeToString(m.GetSIH()) 373 | } 374 | 375 | return "format.Message{" + 376 | "keyFP:" + keyFP + 377 | ", MAC:" + mac + 378 | ", ephemeralRID:" + ephID + 379 | ", sih:" + sih + 380 | ", contents:" + fmt.Sprintf("%q", m.GetContents()) + "}" 381 | } 382 | 383 | // SetGroupBits allows the first and second bits to be set in the payload. 384 | // This should be used with code which determines if the bit can be set 385 | // to 1 before proceeding. 386 | func (m Message) SetGroupBits(bitA, bitB bool) { 387 | setFirstBit(m.payloadA, bitA) 388 | setFirstBit(m.payloadB, bitB) 389 | } 390 | 391 | func setFirstBit(b []byte, bit bool) { 392 | if bit { 393 | b[0] |= 0b10000000 394 | } else { 395 | b[0] &= 0b01111111 396 | } 397 | } 398 | 399 | func clearFirstBit(b []byte) { 400 | b[0] = 0b01111111 & b[0] 401 | } 402 | -------------------------------------------------------------------------------- /knownRounds/knownRounds.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | // Package knownRounds tracks which rounds have been checked and which are 9 | // unchecked using a bit stream. 10 | package knownRounds 11 | 12 | import ( 13 | "bytes" 14 | "encoding/binary" 15 | "math" 16 | 17 | "github.com/pkg/errors" 18 | jww "github.com/spf13/jwalterweatherman" 19 | 20 | "gitlab.com/xx_network/primitives/id" 21 | ) 22 | 23 | type RoundCheckFunc func(id id.Round) bool 24 | 25 | // KnownRounds structure tracks which rounds are known and which are unknown. 26 | // Each bit in bitStream corresponds to a round ID and if it is set, it means 27 | // the round has been checked. All rounds before firstUnchecked are known to be 28 | // checked. All rounds after lastChecked are unknown. 29 | type KnownRounds struct { 30 | bitStream uint64Buff // Buffer of check/unchecked rounds 31 | firstUnchecked id.Round // ID of the first round that us unchecked 32 | lastChecked id.Round // ID of the last round that is checked 33 | fuPos int // The bit position of firstUnchecked in bitStream 34 | } 35 | 36 | // DiskKnownRounds structure is used to as an intermediary to marshal and 37 | // unmarshal KnownRounds. 38 | type DiskKnownRounds struct { 39 | BitStream []byte 40 | FirstUnchecked, LastChecked uint64 41 | } 42 | 43 | // NewKnownRound creates a new empty KnownRounds in the default state with a 44 | // bit stream that can hold the given number of rounds. 45 | func NewKnownRound(roundCapacity int) *KnownRounds { 46 | return &KnownRounds{ 47 | bitStream: make(uint64Buff, (roundCapacity+63)/64), 48 | firstUnchecked: 0, 49 | lastChecked: 0, 50 | fuPos: 0, 51 | } 52 | } 53 | 54 | // NewFromParts creates a new KnownRounds from the given firstUnchecked, 55 | // lastChecked, fuPos, and uint64 buffer. 56 | func NewFromParts( 57 | buff []uint64, firstUnchecked, lastChecked id.Round, fuPos int) *KnownRounds { 58 | return &KnownRounds{ 59 | bitStream: buff, 60 | firstUnchecked: firstUnchecked, 61 | lastChecked: lastChecked, 62 | fuPos: fuPos, 63 | } 64 | } 65 | 66 | // Marshal returns the JSON encoding of DiskKnownRounds, which contains the 67 | // compressed information from KnownRounds. The bit stream is compressed such 68 | // that the firstUnchecked occurs in the first block of the bit stream. 69 | func (kr *KnownRounds) Marshal() []byte { 70 | // Calculate length of compressed bit stream. 71 | startPos := kr.getBitStreamPos(kr.firstUnchecked) 72 | endPos := kr.getBitStreamPos(kr.lastChecked) 73 | length := kr.bitStream.delta(startPos, endPos) 74 | 75 | // Copy only the blocks between firstUnchecked and lastChecked to the stream 76 | startBlock, _ := kr.bitStream.convertLoc(startPos) 77 | bitStream := make(uint64Buff, length) 78 | for i := 0; i < length; i++ { 79 | bitStream[i] = kr.bitStream[(i+startBlock)%len(kr.bitStream)] 80 | } 81 | 82 | // Create new buffer 83 | buf := bytes.Buffer{} 84 | 85 | // Add firstUnchecked to buffer 86 | b := make([]byte, 8) 87 | binary.LittleEndian.PutUint64(b, uint64(kr.firstUnchecked)) 88 | buf.Write(b) 89 | 90 | // Add lastChecked to buffer 91 | b = make([]byte, 8) 92 | binary.LittleEndian.PutUint64(b, uint64(kr.lastChecked)) 93 | buf.Write(b) 94 | 95 | // Add marshaled bitStream to buffer 96 | buf.Write(bitStream.marshal()) 97 | 98 | return buf.Bytes() 99 | } 100 | 101 | // Unmarshal parses the JSON-encoded data and stores it in the KnownRounds. An 102 | // error is returned if the bit stream data is larger than the KnownRounds bit 103 | // stream. 104 | func (kr *KnownRounds) Unmarshal(data []byte) error { 105 | buf := bytes.NewBuffer(data) 106 | 107 | if buf.Len() < 16 { 108 | return errors.Errorf("KnownRounds Unmarshal: "+ 109 | "size of data %d < %d expected", buf.Len(), 16) 110 | } 111 | 112 | // Get firstUnchecked and lastChecked and calculate fuPos 113 | kr.firstUnchecked = id.Round(binary.LittleEndian.Uint64(buf.Next(8))) 114 | kr.lastChecked = id.Round(binary.LittleEndian.Uint64(buf.Next(8))) 115 | kr.fuPos = int(kr.firstUnchecked % 64) 116 | 117 | // Unmarshal the bitStream from the rest of the bytes 118 | bitStream, err := unmarshal(buf.Bytes()) 119 | if err != nil { 120 | return errors.Errorf("Failed to unmarshal bitstream: %+v", err) 121 | } 122 | 123 | // Handle the copying in of the bit stream 124 | if len(kr.bitStream) == 0 { 125 | // If there is no bitstream, like in the wire representations, then make 126 | // the size equal to what is coming in 127 | kr.bitStream = bitStream 128 | } else if len(kr.bitStream) >= len(bitStream) { 129 | // If a size already exists and the data fits within it, then copy it 130 | // into the beginning of the buffer 131 | copy(kr.bitStream, bitStream) 132 | } else { 133 | // If the passed in data is larger than the internal buffer, then return 134 | // an error 135 | return errors.Errorf("KnownRounds bitStream size of %d is too small "+ 136 | "for passed in bit stream of size %d.", 137 | len(kr.bitStream), len(bitStream)) 138 | } 139 | 140 | return nil 141 | } 142 | 143 | // KrChanges map contains a list of changes between two KnownRounds bit streams. 144 | // The key is the index of the changed word and the value contains the change. 145 | type KrChanges map[int]uint64 146 | 147 | // OutputBuffChanges returns the current KnownRounds' firstUnchecked, 148 | // lastChecked, fuPos, and a list of changes between the given uint64 buffer and 149 | // the current KnownRounds bit stream. An error is returned if the two buffers 150 | // are not of the same length. 151 | func (kr *KnownRounds) OutputBuffChanges( 152 | old []uint64) (KrChanges, id.Round, id.Round, int, error) { 153 | 154 | // Return an error if they are not the same length 155 | if len(old) != len(kr.bitStream) { 156 | return nil, 0, 0, 0, errors.Errorf("length of old buffer %d is "+ 157 | "not the same as length of the current buffer %d", 158 | len(old), len(kr.bitStream)) 159 | } 160 | 161 | // Create list of changes 162 | changes := make(KrChanges) 163 | for i, word := range kr.bitStream { 164 | if word != old[i] { 165 | changes[i] = word 166 | } 167 | } 168 | 169 | return changes, kr.firstUnchecked, kr.lastChecked, kr.fuPos, nil 170 | } 171 | 172 | func (kr KnownRounds) GetFirstUnchecked() id.Round { return kr.firstUnchecked } 173 | func (kr KnownRounds) GetLastChecked() id.Round { return kr.lastChecked } 174 | func (kr KnownRounds) GetFuPos() int { return kr.fuPos } 175 | func (kr KnownRounds) GetBitStream() []uint64 { return kr.bitStream.deepCopy() } 176 | func (kr KnownRounds) MarshalBitStream1Byte() []byte { return kr.bitStream.marshal1ByteVer2() } 177 | func (kr KnownRounds) MarshalBitStream2Byte() []byte { return kr.bitStream.marshal2BytesVer2() } 178 | func (kr KnownRounds) MarshalBitStream4Byte() []byte { return kr.bitStream.marshal4BytesVer2() } 179 | func (kr KnownRounds) MarshalBitStream8Byte() []byte { return kr.bitStream.marshal8BytesVer2() } 180 | 181 | // Checked determines if the round has been checked. 182 | func (kr *KnownRounds) Checked(rid id.Round) bool { 183 | if rid < kr.firstUnchecked { 184 | return true 185 | } else if rid > kr.lastChecked { 186 | return false 187 | } 188 | 189 | pos := kr.getBitStreamPos(rid) 190 | 191 | return kr.bitStream.get(pos) 192 | } 193 | 194 | // Check denotes a round has been checked. If the passed in round occurred after 195 | // the last checked round, then every round between them is set as unchecked and 196 | // the passed in round becomes the last checked round. Will panic if the buffer 197 | // is not large enough to hold the current data and the new data 198 | func (kr *KnownRounds) Check(rid id.Round) { 199 | if abs(int(kr.lastChecked-rid))/(len(kr.bitStream)*64) > 0 { 200 | jww.FATAL.Panicf("Cannot check a round outside the current scope. " + 201 | "Scope is KnownRounds size more rounds than last checked. A call " + 202 | "to Forward can be used to fix the scope.") 203 | } 204 | kr.check(rid) 205 | } 206 | 207 | func (kr *KnownRounds) ForceCheck(rid id.Round) { 208 | if rid < kr.firstUnchecked { 209 | return 210 | } else if kr.lastChecked < rid && 211 | int(rid-kr.firstUnchecked) > (len(kr.bitStream)*64) { 212 | kr.Forward(rid - id.Round(len(kr.bitStream)*64)) 213 | } 214 | 215 | kr.check(rid) 216 | } 217 | 218 | // Check denotes a round has been checked. If the passed in round occurred after 219 | // the last checked round, then every round between them is set as unchecked and 220 | // the passed in round becomes the last checked round. Will shift the buffer 221 | // forward, erasing old data, if the buffer is not large enough to hold the new 222 | // checked input 223 | func (kr *KnownRounds) check(rid id.Round) { 224 | if rid < kr.firstUnchecked { 225 | return 226 | } 227 | pos := kr.getBitStreamPos(rid) 228 | 229 | // Set round as checked 230 | kr.bitStream.set(pos) 231 | 232 | // If the round ID is newer, then set it as the last checked ID and uncheck 233 | // all the newly added rounds in the buffer 234 | if rid > kr.lastChecked { 235 | kr.bitStream.clearRange(kr.getBitStreamPos(kr.lastChecked+1), pos) 236 | kr.lastChecked = rid 237 | } 238 | 239 | if kr.getBitStreamPos(kr.firstUnchecked) == pos { 240 | if kr.getBitStreamPos(kr.lastChecked) == pos { 241 | kr.fuPos = kr.getBitStreamPos(rid + 1) 242 | kr.firstUnchecked = rid + 1 243 | kr.lastChecked = rid + 1 244 | kr.bitStream.clear(kr.fuPos) 245 | } else { 246 | kr.migrateFirstUnchecked(rid) 247 | } 248 | } 249 | 250 | // Handle cases where rid lapse firstUnchecked one or more times. 251 | if rid > kr.firstUnchecked && (rid-kr.firstUnchecked) >= id.Round(kr.Len()) { 252 | newFu := rid + 1 - id.Round(kr.Len()) 253 | kr.fuPos = kr.getBitStreamPos(newFu) 254 | kr.firstUnchecked = rid + 1 - id.Round(kr.Len()) 255 | kr.migrateFirstUnchecked(rid) 256 | } 257 | 258 | // Set round as checked 259 | kr.bitStream.set(pos) 260 | } 261 | 262 | // abs returns the absolute value of the passed in integer. 263 | func abs(n int) int { 264 | if n < 0 { 265 | return -n 266 | } 267 | return n 268 | } 269 | 270 | // migrateFirstUnchecked moves firstUnchecked to the next unchecked round or 271 | // sets it to lastUnchecked if all rounds are checked. 272 | func (kr *KnownRounds) migrateFirstUnchecked(rid id.Round) { 273 | for ; kr.bitStream.get(kr.getBitStreamPos(rid)) && rid <= kr.lastChecked; rid++ { 274 | } 275 | kr.fuPos = kr.getBitStreamPos(rid) 276 | kr.firstUnchecked = rid 277 | } 278 | 279 | // Forward sets all rounds before the given round ID as checked. 280 | func (kr *KnownRounds) Forward(rid id.Round) { 281 | if rid > kr.lastChecked { 282 | kr.firstUnchecked = rid 283 | kr.lastChecked = rid 284 | kr.fuPos = int(rid % 64) 285 | } else if rid > kr.firstUnchecked { 286 | kr.migrateFirstUnchecked(rid) 287 | } 288 | } 289 | 290 | // RangeUnchecked runs the passed function over all rounds starting with oldest 291 | // unknown and ending with 292 | func (kr *KnownRounds) RangeUnchecked(oldestUnknown id.Round, threshold uint, 293 | roundCheck func(id id.Round) bool, maxPickups int) ( 294 | earliestRound id.Round, has, unknown []id.Round) { 295 | 296 | newestRound := kr.lastChecked 297 | 298 | // Calculate how far back we should go back to check rounds 299 | // If the newest round is smaller than the threshold, then our oldest round 300 | // is zero. But the earliest round ID is 1. 301 | oldestPossibleEarliestRound := id.Round(1) 302 | if newestRound > id.Round(threshold) { 303 | oldestPossibleEarliestRound = newestRound - id.Round(threshold) 304 | } 305 | 306 | earliestRound = kr.lastChecked + 1 307 | has = make([]id.Round, 0, maxPickups) 308 | 309 | // If the oldest unknown round is outside the range we are attempting to 310 | // check, then skip checking 311 | if oldestUnknown > kr.lastChecked { 312 | jww.TRACE.Printf( 313 | "RangeUnchecked: oldestUnknown (%d) > kr.lastChecked (%d)", 314 | oldestUnknown, kr.lastChecked) 315 | return oldestUnknown, nil, nil 316 | } 317 | 318 | // Loop through all rounds from the oldest unknown to the last checked round 319 | // and check them, if possible 320 | for i := oldestUnknown; i <= kr.lastChecked; i++ { 321 | 322 | // If the source does not know about the round, set that round as 323 | // unknown and don't check it 324 | if !kr.Checked(i) { 325 | if i < oldestPossibleEarliestRound { 326 | unknown = append(unknown, i) 327 | } else if i < earliestRound { 328 | earliestRound = i 329 | } 330 | continue 331 | } 332 | 333 | // check the round 334 | hasRound := roundCheck(i) 335 | 336 | // If checking is not complete and the round is earlier than the 337 | // earliest round, then set it to the earliest round 338 | if hasRound { 339 | has = append(has, i) 340 | // Do not pick up too many messages at once 341 | if len(has) >= maxPickups { 342 | nextRound := i + 1 343 | if (nextRound) < earliestRound { 344 | earliestRound = nextRound 345 | } 346 | break 347 | } 348 | } 349 | } 350 | 351 | // Return the next round 352 | return earliestRound, has, unknown 353 | } 354 | 355 | // RangeUncheckedMasked masks the bit stream with the provided mask. 356 | func (kr *KnownRounds) RangeUncheckedMasked(mask *KnownRounds, 357 | roundCheck RoundCheckFunc, maxChecked int) { 358 | 359 | kr.RangeUncheckedMaskedRange(mask, roundCheck, 0, math.MaxUint64, maxChecked) 360 | } 361 | 362 | // RangeUncheckedMaskedRange masks the bit stream with the provided mask. 363 | func (kr *KnownRounds) RangeUncheckedMaskedRange(mask *KnownRounds, 364 | roundCheck RoundCheckFunc, start, end id.Round, maxChecked int) { 365 | 366 | numChecked := 0 367 | 368 | if mask.firstUnchecked != mask.lastChecked { 369 | mask.Forward(kr.firstUnchecked) 370 | subSample, delta := kr.subSample(mask.firstUnchecked, mask.lastChecked) 371 | // FIXME: it is inefficient to make a copy of the mask here. 372 | result := subSample.implies(mask.bitStream) 373 | 374 | for i := mask.firstUnchecked + id.Round(delta) - 1; i >= mask.firstUnchecked && numChecked < maxChecked; i, numChecked = i-1, numChecked+1 { 375 | if !result.get(int(i-mask.firstUnchecked)) && roundCheck(i) { 376 | kr.Check(i) 377 | } 378 | } 379 | } 380 | 381 | if start < kr.firstUnchecked { 382 | start = kr.firstUnchecked 383 | } 384 | 385 | if end > mask.firstUnchecked { 386 | end = mask.firstUnchecked 387 | } 388 | 389 | for i := start; i < end && numChecked < maxChecked; i, numChecked = i+1, numChecked+1 { 390 | if !kr.Checked(i) && roundCheck(i) { 391 | kr.Check(i) 392 | } 393 | } 394 | } 395 | 396 | // subSample returns a sub sample of the KnownRounds buffer from the start to 397 | // end round and its length. 398 | func (kr *KnownRounds) subSample(start, end id.Round) (uint64Buff, int) { 399 | // Get the number of blocks spanned by the range 400 | numBlocks := kr.bitStream.delta(kr.getBitStreamPos(start), 401 | kr.getBitStreamPos(end)) 402 | 403 | if start > kr.lastChecked { 404 | return make(uint64Buff, numBlocks), numBlocks 405 | } 406 | 407 | copyEnd := end 408 | if kr.lastChecked < end { 409 | copyEnd = kr.lastChecked 410 | } 411 | 412 | // Create a sub sample of the buffer 413 | buff := kr.bitStream.copy(kr.getBitStreamPos(start), 414 | kr.getBitStreamPos(copyEnd+1)) 415 | 416 | // Return a buffer of the correct size and its length 417 | return buff.extend(numBlocks), abs(int(end - start)) 418 | } 419 | 420 | // Truncate returns a subs ample of the KnownRounds buffer from last checked. 421 | func (kr *KnownRounds) Truncate(start id.Round) *KnownRounds { 422 | if start <= kr.firstUnchecked { 423 | return kr 424 | } 425 | 426 | // Return a buffer of the correct size and its length 427 | newKr := &KnownRounds{ 428 | bitStream: kr.bitStream.deepCopy(), 429 | firstUnchecked: kr.firstUnchecked, 430 | lastChecked: kr.lastChecked, 431 | fuPos: kr.fuPos, 432 | } 433 | 434 | newKr.migrateFirstUnchecked(start) 435 | 436 | return newKr 437 | } 438 | 439 | // Get the position of the bit in the bit stream for the given round ID. 440 | func (kr *KnownRounds) getBitStreamPos(rid id.Round) int { 441 | var delta int 442 | if rid < kr.firstUnchecked { 443 | delta = -int(kr.firstUnchecked - rid) 444 | } else { 445 | delta = int(rid - kr.firstUnchecked) 446 | } 447 | 448 | pos := (kr.fuPos + delta) % kr.Len() 449 | if pos < 0 { 450 | return kr.Len() + pos 451 | } 452 | return pos 453 | 454 | } 455 | 456 | // Len returns the max number of round IDs the buffer can hold. 457 | func (kr *KnownRounds) Len() int { 458 | return len(kr.bitStream) * 64 459 | } 460 | -------------------------------------------------------------------------------- /knownRounds/uint64Buff.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package knownRounds 9 | 10 | import ( 11 | "bytes" 12 | "encoding/binary" 13 | "io" 14 | "math" 15 | 16 | "github.com/pkg/errors" 17 | jww "github.com/spf13/jwalterweatherman" 18 | ) 19 | 20 | const ( 21 | ones = math.MaxUint64 22 | ) 23 | 24 | type uint64Buff []uint64 25 | 26 | // Get returns the value of the bit at the given position. 27 | func (u64b uint64Buff) get(pos int) bool { 28 | bin, offset := u64b.convertLoc(pos) 29 | 30 | return u64b[bin]>>(63-offset)&1 == 1 31 | } 32 | 33 | // set modifies the bit at the specified position to be 1. 34 | func (u64b uint64Buff) set(pos int) { 35 | bin, offset := u64b.convertLoc(pos) 36 | u64b[bin] |= 1 << (63 - offset) 37 | } 38 | 39 | // set modifies the bit at the specified position to be 1. 40 | func (u64b uint64Buff) clear(pos int) { 41 | bin, offset := u64b.convertLoc(pos) 42 | u64b[bin] &= ^(1 << (63 - offset)) 43 | } 44 | 45 | // clearRange clears all the bits in the buffer between the given range 46 | // (including the start and end bits). 47 | // 48 | // If start is greater than end, then the selection is inverted. 49 | func (u64b uint64Buff) clearRange(start, end int) { 50 | 51 | // Determine the starting positions the buffer 52 | numBlocks := u64b.delta(start, end) 53 | firstBlock, firstBit := u64b.convertLoc(start) 54 | 55 | // Loop over every the blocks in u64b that are in the range 56 | for blockIndex := 0; blockIndex < numBlocks; blockIndex++ { 57 | // Get index where the block appears in the buffer 58 | buffBlock := u64b.getBin(firstBlock + blockIndex) 59 | 60 | // Get the position of the last bit in the current block 61 | lastBit := 64 62 | if blockIndex == numBlocks-1 { 63 | _, lastBit = u64b.convertEnd(end) 64 | } 65 | 66 | // Generate bit mask for the range and apply it 67 | bm := bitMaskRange(firstBit, lastBit) 68 | u64b[buffBlock] &= bm 69 | 70 | // Set position to the first bit in the next block 71 | firstBit = 0 72 | } 73 | } 74 | 75 | func (u64b uint64Buff) clearAll() { 76 | for i := range u64b { 77 | u64b[i] = 0 78 | } 79 | } 80 | 81 | // copy returns a copy of the bits from start to end (inclusive) from u64b. 82 | func (u64b uint64Buff) copy(start, end int) uint64Buff { 83 | startBlock, startPos := u64b.convertLoc(start) 84 | 85 | numBlocks := u64b.delta(start, end) 86 | copied := make(uint64Buff, numBlocks) 87 | 88 | // Copy all blocks in range 89 | for i := 0; i < numBlocks; i++ { 90 | realBlock := u64b.getBin(startBlock + i) 91 | copied[i] = u64b[realBlock] 92 | } 93 | 94 | // Set all bits before the start 95 | copied[0] |= ^bitMaskRange(0, startPos) 96 | 97 | // Clear all bits after end 98 | _, endPos := u64b.convertEnd(end) 99 | copied[numBlocks-1] &= ^bitMaskRange(0, endPos) 100 | 101 | return copied 102 | } 103 | 104 | // implies applies the material implication of mask and u64b in the given range 105 | // (including the start and end bits) and places the result in masked starting 106 | // at position maskedStart. An error is returned if the range is larger than the 107 | // length of masked. 108 | // 109 | // If u64bStart is greater than u64bEnd, then the selection is inverted. 110 | // 111 | // More info on material implication: 112 | // https://en.wikipedia.org/wiki/Material_conditional 113 | func (u64b uint64Buff) implies(mask uint64Buff) uint64Buff { 114 | if len(u64b) != len(mask) { 115 | jww.FATAL.Panicf("Cannot imply two buffers of different lengths "+ 116 | "(%v and %v).\nu64b: %064b\nmask: %064b", len(u64b), len(mask), u64b, mask) 117 | } 118 | result := make(uint64Buff, len(u64b)) 119 | 120 | for i := 0; i < len(u64b); i++ { 121 | result[i] = ^mask[i] | u64b[i] 122 | } 123 | return result 124 | } 125 | 126 | // extend increases the length of the buffer to the given size and fills in the 127 | // values with zeros. 128 | func (u64b uint64Buff) extend(numBlocks int) uint64Buff { 129 | // The created buffer is all zeroes per go spec 130 | ext := make(uint64Buff, numBlocks) 131 | copy(ext[:], u64b[:]) 132 | return ext 133 | } 134 | 135 | // convertLoc returns the block index and the position of the bit in that block 136 | // for the given position in the buffer. 137 | func (u64b uint64Buff) convertLoc(pos int) (int, int) { 138 | // Block index in buffer (position / 64) 139 | bin := (pos / 64) % len(u64b) 140 | 141 | // Position of bit in block 142 | offset := pos % 64 143 | 144 | return bin, offset 145 | } 146 | 147 | func (u64b uint64Buff) convertEnd(pos int) (int, int) { 148 | bin := (pos - 1) / 64 149 | 150 | offset := (pos-1)%64 + 1 151 | 152 | return bin, offset 153 | } 154 | 155 | // getBin returns the block index in the buffer for the given absolute index. 156 | func (u64b uint64Buff) getBin(block int) int { 157 | return block % len(u64b) 158 | } 159 | 160 | // delta calculates the number of blocks or parts of blocks contained within the 161 | // range between start and end. If the start and end appear in the same block, 162 | // then delta returns 1. 163 | func (u64b uint64Buff) delta(start, end int) int { 164 | if end == start { 165 | return 1 166 | } 167 | end-- 168 | if end < start { 169 | return len(u64b) - start/64 + end/64 + 1 170 | } else { 171 | return end/64 - start/64 + 1 172 | } 173 | } 174 | 175 | // deepCopy returns a copy of the buffer. 176 | func (u64b uint64Buff) deepCopy() uint64Buff { 177 | buff := make(uint64Buff, len(u64b)) 178 | copy(buff, u64b) 179 | return buff 180 | } 181 | 182 | // bitMaskRange generates a bit mask that targets the bits in the provided 183 | // range. The resulting value has 0s in that range and 1s everywhere else. 184 | func bitMaskRange(start, end int) uint64 { 185 | s := uint64(math.MaxUint64 << uint(64-start)) 186 | e := uint64(math.MaxUint64 >> uint(end)) 187 | return (s | e) & (getInvert(end < start) ^ (s ^ e)) 188 | } 189 | 190 | func getInvert(b bool) uint64 { 191 | switch b { 192 | case true: 193 | return math.MaxUint64 194 | default: 195 | return 0 196 | } 197 | } 198 | 199 | // Word sizes for each marshal/unmarshal function. 200 | const ( 201 | u8bLen = 1 202 | u16bLen = 2 203 | u32bLen = 4 204 | u64bLen = 8 205 | ) 206 | 207 | // Current encoding version written to the marshaled buffer. Increment this on 208 | // changes. 209 | const currentVersion = 2 210 | 211 | // Map used to select correct unmarshal for the data version. 212 | var u64bUnmarshalVersion = map[uint8]map[uint8]func(b []byte) (uint64Buff, error){ 213 | currentVersion: { 214 | u8bLen: unmarshal1ByteVer2, 215 | u16bLen: unmarshal2BytesVer2, 216 | u32bLen: unmarshal4BytesVer2, 217 | u64bLen: unmarshal8BytesVer2, 218 | }, 219 | } 220 | 221 | // marshal encodes the buffer into a byte slice and compresses the data using 222 | // run-length encoding on the integer level. For this implementation, run 223 | // lengths are only included after one or more consecutive words of all 1s or 224 | // all 0s. All other data is kept in its original form. 225 | // 226 | // The data is encoded along with the word size used and the version in the 227 | // following structure: 228 | // +---------+-----------+----------+ 229 | // | version | word size | data | 230 | // | 1 byte | 1 byte | variable | 231 | // +---------+-----------+----------+ 232 | func (u64b uint64Buff) marshal() []byte { 233 | return append([]byte{currentVersion, u8bLen}, u64b.marshal1ByteVer2()...) 234 | } 235 | 236 | // unmarshal decodes the run-length encoded buffer. 237 | func unmarshal(b []byte) (uint64Buff, error) { 238 | if len(b) < 3 { 239 | return nil, errors.Errorf("marshaled bytes length %d smaller than "+ 240 | "minimum %d", len(b), 3) 241 | } 242 | 243 | unmarshalWordMap, exists := u64bUnmarshalVersion[b[0]] 244 | if !exists { 245 | return nil, errors.Errorf("encoding version %d unrecognized", b[0]) 246 | } 247 | 248 | unmarshal, exists := unmarshalWordMap[b[1]] 249 | if !exists { 250 | return nil, errors.Errorf("encoding word size %d unrecognized", b[1]) 251 | } 252 | 253 | return unmarshal(b[2:]) 254 | } 255 | 256 | func (u64b uint64Buff) marshal1ByteVer2() []byte { 257 | if len(u64b) == 0 { 258 | return nil 259 | } 260 | 261 | u8b := make([]uint8, 0, len(u64b)*8) 262 | for _, u64 := range u64b { 263 | u8b = append(u8b, 264 | uint8(u64>>56), 265 | uint8(u64>>48), 266 | uint8(u64>>40), 267 | uint8(u64>>32), 268 | uint8(u64>>24), 269 | uint8(u64>>16), 270 | uint8(u64>>8), 271 | uint8(u64), 272 | ) 273 | } 274 | 275 | var buf bytes.Buffer 276 | var cur = u8b[0] 277 | var run uint8 278 | 279 | if cur == 0 || cur == math.MaxUint8 { 280 | run = 1 281 | } 282 | for _, next := range u8b[1:] { 283 | if cur != next || run == 0 { 284 | buf.WriteByte(cur) 285 | if run > 0 { 286 | buf.WriteByte(run) 287 | run = 0 288 | } 289 | } 290 | if next == 0 || next == math.MaxUint8 { 291 | if run == math.MaxUint8 { 292 | buf.WriteByte(cur) 293 | buf.WriteByte(run) 294 | run = 0 295 | } 296 | run++ 297 | } 298 | cur = next 299 | } 300 | 301 | buf.WriteByte(cur) 302 | if run > 0 { 303 | buf.WriteByte(run) 304 | } 305 | 306 | return buf.Bytes() 307 | } 308 | 309 | func unmarshal1ByteVer2(b []byte) (uint64Buff, error) { 310 | buf := bytes.NewBuffer(b) 311 | var u8b []uint8 312 | var err error 313 | var num uint8 314 | 315 | // Reach each uint out of the buffer 316 | for num, err = buf.ReadByte(); err == nil; num, err = buf.ReadByte() { 317 | if num == 0 || num == 0xFF { 318 | run, err := buf.ReadByte() 319 | if err != nil { 320 | jww.FATAL.Panicf("Failed to read next byte: %+v", err) 321 | } 322 | runBuf := make([]uint8, run) 323 | for i := range runBuf { 324 | runBuf[i] = num 325 | } 326 | u8b = append(u8b, runBuf...) 327 | } else { 328 | u8b = append(u8b, num) 329 | } 330 | } 331 | 332 | if err != io.EOF { 333 | return nil, errors.Errorf("failed to get next uint8 from buffer: "+ 334 | "%+v", err) 335 | } 336 | 337 | if len(u8b)%8 != 0 { 338 | return nil, errors.Errorf("length of uncompressed data (%d) must be "+ 339 | "divisible by 8", len(u8b)) 340 | } 341 | 342 | var u64b uint64Buff 343 | 344 | for i := 0; i < len(u8b); i += 8 { 345 | u8P0 := uint64(u8b[i]) << 56 346 | u8P1 := uint64(u8b[i+1]) << 48 347 | u8P2 := uint64(u8b[i+2]) << 40 348 | u8P3 := uint64(u8b[i+3]) << 32 349 | u8P4 := uint64(u8b[i+4]) << 24 350 | u8P5 := uint64(u8b[i+5]) << 16 351 | u8P6 := uint64(u8b[i+6]) << 8 352 | u8P7 := uint64(u8b[i+7]) 353 | 354 | u64b = append(u64b, u8P0|u8P1|u8P2|u8P3|u8P4|u8P5|u8P6|u8P7) 355 | } 356 | 357 | return u64b, nil 358 | } 359 | 360 | func write2Bytes(i uint16) []byte { 361 | b := make([]byte, u16bLen) 362 | binary.BigEndian.PutUint16(b, i) 363 | return b 364 | } 365 | 366 | func (u64b uint64Buff) marshal2BytesVer2() []byte { 367 | if len(u64b) == 0 { 368 | return nil 369 | } 370 | 371 | u16b := make([]uint16, 0, len(u64b)*4) 372 | for _, u64 := range u64b { 373 | u16b = append(u16b, 374 | uint16(u64>>48), 375 | uint16(u64>>32), 376 | uint16(u64>>16), 377 | uint16(u64), 378 | ) 379 | } 380 | 381 | var buf bytes.Buffer 382 | var cur = u16b[0] 383 | var run uint16 384 | if cur == 0 || cur == math.MaxUint16 { 385 | run = 1 386 | } 387 | for _, next := range u16b[1:] { 388 | if cur != next || run == 0 { 389 | buf.Write(write2Bytes(cur)) 390 | if run > 0 { 391 | buf.Write(write2Bytes(run)) 392 | run = 0 393 | } 394 | } 395 | if next == 0 || next == math.MaxUint16 { 396 | if run == math.MaxUint16 { 397 | buf.Write(write2Bytes(cur)) 398 | buf.Write(write2Bytes(run)) 399 | run = 0 400 | } 401 | run++ 402 | } 403 | cur = next 404 | } 405 | 406 | buf.Write(write2Bytes(cur)) 407 | if run > 0 { 408 | buf.Write(write2Bytes(run)) 409 | } 410 | 411 | return buf.Bytes() 412 | } 413 | 414 | func unmarshal2BytesVer2(b []byte) (uint64Buff, error) { 415 | buf := bytes.NewBuffer(b) 416 | var u16b []uint16 417 | 418 | // Reach each uint out of the buffer 419 | bb := buf.Next(u16bLen) 420 | for ; len(bb) == u16bLen; bb = buf.Next(u16bLen) { 421 | num := binary.BigEndian.Uint16(bb) 422 | if num == 0 || num == math.MaxUint16 { 423 | run := binary.BigEndian.Uint16(buf.Next(u16bLen)) 424 | runBuf := make([]uint16, run) 425 | for i := range runBuf { 426 | runBuf[i] = num 427 | } 428 | u16b = append(u16b, runBuf...) 429 | } else { 430 | u16b = append(u16b, num) 431 | } 432 | } 433 | 434 | if len(bb) != 0 { 435 | return nil, errors.Errorf("extraneous data of length %d found at end "+ 436 | "of buffer", len(bb)) 437 | } else if len(u16b)%4 != 0 { 438 | return nil, errors.Errorf("length of uncompressed data (%d) must be "+ 439 | "divisible by 4", len(u16b)) 440 | } 441 | 442 | var u64b uint64Buff 443 | 444 | for i := 0; i < len(u16b); i += 4 { 445 | u16P0 := uint64(u16b[i]) << 48 446 | u16P1 := uint64(u16b[i+1]) << 32 447 | u16P2 := uint64(u16b[i+2]) << 16 448 | u16P3 := uint64(u16b[i+3]) 449 | 450 | u64b = append(u64b, u16P0|u16P1|u16P2|u16P3) 451 | } 452 | 453 | return u64b, nil 454 | } 455 | 456 | func write4Bytes(i uint32) []byte { 457 | b := make([]byte, u32bLen) 458 | binary.BigEndian.PutUint32(b, i) 459 | return b 460 | } 461 | 462 | func (u64b uint64Buff) marshal4BytesVer2() []byte { 463 | if len(u64b) == 0 { 464 | return nil 465 | } 466 | 467 | u32b := make([]uint32, 0, len(u64b)*2) 468 | for _, u64 := range u64b { 469 | u32b = append(u32b, 470 | uint32(u64>>32), 471 | uint32(u64), 472 | ) 473 | } 474 | 475 | var buf bytes.Buffer 476 | var cur = u32b[0] 477 | var run uint32 478 | if cur == 0 || cur == math.MaxUint32 { 479 | run = 1 480 | } 481 | for _, next := range u32b[1:] { 482 | if cur != next || run == 0 { 483 | buf.Write(write4Bytes(cur)) 484 | if run > 0 { 485 | buf.Write(write4Bytes(run)) 486 | run = 0 487 | } 488 | } 489 | if next == 0 || next == math.MaxUint32 { 490 | if run == math.MaxUint32 { 491 | buf.Write(write4Bytes(cur)) 492 | buf.Write(write4Bytes(run)) 493 | run = 0 494 | } 495 | run++ 496 | } 497 | cur = next 498 | } 499 | 500 | buf.Write(write4Bytes(cur)) 501 | if run > 0 { 502 | buf.Write(write4Bytes(run)) 503 | } 504 | 505 | return buf.Bytes() 506 | } 507 | 508 | func unmarshal4BytesVer2(b []byte) (uint64Buff, error) { 509 | buf := bytes.NewBuffer(b) 510 | var u32b []uint32 511 | 512 | // Reach each uint out of the buffer 513 | bb := buf.Next(u32bLen) 514 | for ; len(bb) == u32bLen; bb = buf.Next(u32bLen) { 515 | num := binary.BigEndian.Uint32(bb) 516 | if num == 0 || num == math.MaxUint32 { 517 | run := binary.BigEndian.Uint32(buf.Next(u32bLen)) 518 | runBuf := make([]uint32, run) 519 | for i := range runBuf { 520 | runBuf[i] = num 521 | } 522 | u32b = append(u32b, runBuf...) 523 | } else { 524 | u32b = append(u32b, num) 525 | } 526 | } 527 | 528 | if len(bb) != 0 { 529 | return nil, errors.Errorf("extraneous data of length %d found at end "+ 530 | "of buffer", len(bb)) 531 | } else if len(u32b)%2 != 0 { 532 | return nil, errors.Errorf("length of uncompressed data (%d) must be "+ 533 | "divisible by 2", len(u32b)) 534 | } 535 | 536 | var u64b uint64Buff 537 | 538 | for i := 0; i < len(u32b); i += 2 { 539 | u16P0 := uint64(u32b[i]) << 32 540 | u16P1 := uint64(u32b[i+1]) 541 | 542 | u64b = append(u64b, u16P0|u16P1) 543 | } 544 | 545 | return u64b, nil 546 | } 547 | 548 | func write8Bytes(i uint64) []byte { 549 | b := make([]byte, u64bLen) 550 | binary.LittleEndian.PutUint64(b, i) 551 | return b 552 | } 553 | 554 | func (u64b uint64Buff) marshal8BytesVer2() []byte { 555 | if len(u64b) == 0 { 556 | return nil 557 | } 558 | 559 | var buf bytes.Buffer 560 | var cur = u64b[0] 561 | var run uint64 562 | if cur == 0 || cur == math.MaxUint64 { 563 | run = 1 564 | } 565 | for _, next := range u64b[1:] { 566 | if cur != next || run == 0 { 567 | buf.Write(write8Bytes(cur)) 568 | if run > 0 { 569 | buf.Write(write8Bytes(run)) 570 | run = 0 571 | } 572 | } 573 | if next == 0 || next == math.MaxUint64 { 574 | if run == math.MaxUint64 { 575 | buf.Write(write8Bytes(cur)) 576 | buf.Write(write8Bytes(run)) 577 | run = 0 578 | } 579 | run++ 580 | } 581 | cur = next 582 | } 583 | 584 | buf.Write(write8Bytes(cur)) 585 | if run > 0 { 586 | buf.Write(write8Bytes(run)) 587 | } 588 | 589 | return buf.Bytes() 590 | } 591 | 592 | func unmarshal8BytesVer2(b []byte) (uint64Buff, error) { 593 | buf := bytes.NewBuffer(b) 594 | buff := uint64Buff{} 595 | 596 | bb := buf.Next(u64bLen) 597 | for ; len(bb) == u64bLen; bb = buf.Next(u64bLen) { 598 | num := binary.LittleEndian.Uint64(bb) 599 | if num == 0 || num == math.MaxUint64 { 600 | bb = buf.Next(u64bLen) 601 | if len(bb) != u64bLen { 602 | return nil, errors.New("failed to get run") 603 | } 604 | run := binary.LittleEndian.Uint64(bb) 605 | runBuf := make(uint64Buff, run) 606 | for i := range runBuf { 607 | runBuf[i] = num 608 | } 609 | buff = append(buff, runBuf...) 610 | } else { 611 | buff = append(buff, num) 612 | } 613 | } 614 | 615 | if len(bb) != 0 { 616 | return nil, errors.Errorf("extraneous data of length %d found at end "+ 617 | "of buffer", len(bb)) 618 | } 619 | 620 | return buff, nil 621 | } 622 | -------------------------------------------------------------------------------- /format/message_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package format 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "math/rand" 14 | "reflect" 15 | "testing" 16 | "time" 17 | ) 18 | 19 | func TestMessage_VersionDetection(t *testing.T) { 20 | msg := NewMessage(MinimumPrimeSize) 21 | 22 | // Generate message parts 23 | fp := NewFingerprint(makeAndFillSlice(KeyFPLen, 'c')) 24 | mac := makeAndFillSlice(MacLen, 'd') 25 | ephemeralRID := makeAndFillSlice(EphemeralRIDLen, 'e') 26 | identityFP := makeAndFillSlice(SIHLen, 'f') 27 | contents := makeAndFillSlice(MinimumPrimeSize*2-AssociatedDataSize-1, 'g') 28 | 29 | // Set message parts 30 | msg.SetKeyFP(fp) 31 | msg.SetMac(mac) 32 | msg.SetEphemeralRID(ephemeralRID) 33 | msg.SetSIH(identityFP) 34 | msg.SetContents(contents) 35 | 36 | copy(msg.version, []byte{123}) 37 | 38 | msgBytes := msg.Marshal() 39 | _, err := Unmarshal(msgBytes) 40 | if err == nil { 41 | // t.Error("version detection fail") 42 | t.Logf("Version detection disabled") 43 | } 44 | } 45 | 46 | func TestMessage_Smoke(t *testing.T) { 47 | msg := NewMessage(MinimumPrimeSize) 48 | 49 | // Generate message parts 50 | fp := NewFingerprint(makeAndFillSlice(KeyFPLen, 'c')) 51 | mac := makeAndFillSlice(MacLen, 'd') 52 | ephemeralRID := makeAndFillSlice(EphemeralRIDLen, 'e') 53 | identityFP := makeAndFillSlice(SIHLen, 'f') 54 | contents := makeAndFillSlice(MinimumPrimeSize*2-AssociatedDataSize-1, 'g') 55 | 56 | // Set message parts 57 | msg.SetKeyFP(fp) 58 | msg.SetMac(mac) 59 | msg.SetEphemeralRID(ephemeralRID) 60 | msg.SetSIH(identityFP) 61 | msg.SetContents(contents) 62 | 63 | if !bytes.Equal(fp.Bytes(), msg.keyFP) { 64 | t.Errorf("keyFp data was corrupted.\nexpected: %+v\nreceived: %+v", 65 | fp.Bytes(), msg.keyFP) 66 | } 67 | 68 | if !bytes.Equal(mac, msg.mac) { 69 | t.Errorf("MAC data was corrupted.\nexpected: %+v\nreceived: %+v", 70 | mac, msg.mac) 71 | } 72 | 73 | if !bytes.Equal(ephemeralRID, msg.ephemeralRID) { 74 | t.Errorf("ephemeralRID data was corrupted.\nexpected: %+v\nreceived: %+v", 75 | ephemeralRID, msg.ephemeralRID) 76 | } 77 | 78 | if !bytes.Equal(identityFP, msg.sih) { 79 | t.Errorf("sih data was corrupted.\nexpected: %+v\nreceived: %+v", 80 | identityFP, msg.sih) 81 | } 82 | 83 | if !bytes.Equal(contents, append(msg.contents1, msg.contents2...)) { 84 | t.Errorf("contents data was corrupted.\nexpected: %+v\nreceived: %+v", 85 | contents, append(msg.contents1, msg.contents2...)) 86 | } 87 | } 88 | 89 | // Happy path. 90 | func TestNewMessage(t *testing.T) { 91 | numPrimeBytes := MinimumPrimeSize 92 | expectedMsg := Message{ 93 | data: make([]byte, 2*numPrimeBytes), 94 | payloadA: make([]byte, numPrimeBytes), 95 | payloadB: make([]byte, numPrimeBytes), 96 | keyFP: make([]byte, KeyFPLen), 97 | version: make([]byte, 1), 98 | contents1: make([]byte, numPrimeBytes-KeyFPLen-1), 99 | mac: make([]byte, MacLen), 100 | contents2: make([]byte, numPrimeBytes-MacLen-RecipientIDLen), 101 | ephemeralRID: make([]byte, EphemeralRIDLen), 102 | sih: make([]byte, SIHLen), 103 | rawContents: make([]byte, 2*numPrimeBytes-RecipientIDLen), 104 | } 105 | 106 | msg := NewMessage(MinimumPrimeSize) 107 | 108 | if !reflect.DeepEqual(expectedMsg, msg) { 109 | t.Errorf("NewMessage did not return the expected Message."+ 110 | "\nexpected: %+v\nreceived: %+v", expectedMsg, msg) 111 | } 112 | } 113 | 114 | // Error path: panics if provided prime size is too small. 115 | func TestNewMessage_NumPrimeBytesError(t *testing.T) { 116 | // Defer to an error when NewMessage does not panic 117 | defer func() { 118 | if r := recover(); r == nil { 119 | t.Error("NewMessage did not panic when the minimum prime size " + 120 | "is too small.") 121 | } 122 | }() 123 | 124 | _ = NewMessage(MinimumPrimeSize - 1) 125 | } 126 | 127 | // Happy path. 128 | func TestMessage_Marshal_Unmarshal(t *testing.T) { 129 | m := NewMessage(256) 130 | prng := rand.New(rand.NewSource(time.Now().UnixNano())) 131 | payload := make([]byte, 256) 132 | prng.Read(payload) 133 | m.SetPayloadA(payload) 134 | prng.Read(payload) 135 | m.SetPayloadB(payload) 136 | copy(m.version, []byte{messagePayloadVersion}) 137 | 138 | messageData := m.Marshal() 139 | newMsg, err := Unmarshal(messageData) 140 | 141 | if err != nil { 142 | t.Errorf("Unmarshal failure: %#v", err) 143 | } 144 | 145 | if !reflect.DeepEqual(m, newMsg) { 146 | t.Errorf("Failed to Marshal and Unmarshal message."+ 147 | "\nexpected: %#v\nreceived: %#v", m, newMsg) 148 | } 149 | } 150 | 151 | func TestMessage_Marshal_UnmarshalImmutable(t *testing.T) { 152 | m := NewMessage(256) 153 | prng := rand.New(rand.NewSource(time.Now().UnixNano())) 154 | payload := make([]byte, 256) 155 | prng.Read(payload) 156 | m.SetPayloadA(payload) 157 | prng.Read(payload) 158 | m.SetPayloadB(payload) 159 | copy(m.version, []byte{messagePayloadVersion}) 160 | 161 | m.ephemeralRID[0] = 42 162 | m.sih[0] = 42 163 | 164 | messageData := m.MarshalImmutable() 165 | newMsg, err := Unmarshal(messageData) 166 | 167 | if err != nil { 168 | t.Errorf("Unmarshal failure: %#v", err) 169 | } 170 | 171 | if newMsg.ephemeralRID[0] != 0 { 172 | t.Errorf("MarshalImmutable did not clear EphemeralRID: %v != 0", 173 | newMsg.ephemeralRID) 174 | } 175 | if newMsg.sih[0] != 0 { 176 | t.Errorf("MarshalImmutable did not clear SIH: %v != 0", 177 | newMsg.sih) 178 | } 179 | } 180 | 181 | // Happy path. 182 | func TestMessage_Version(t *testing.T) { 183 | msg := NewMessage(MinimumPrimeSize) 184 | 185 | if msg.Version() != 0 { 186 | t.Errorf("Unexpected version for new Message."+ 187 | "\nexpected: %d\nreceived: %d", 0, msg.Version()) 188 | } 189 | 190 | copy(msg.version, []byte{123}) 191 | 192 | if msg.Version() != 123 { 193 | t.Errorf("Unexpected version."+ 194 | "\nexpected: %d\nreceived: %d", 123, msg.Version()) 195 | } 196 | } 197 | 198 | // Happy path. 199 | func TestMessage_Copy(t *testing.T) { 200 | msg := NewMessage(MinimumPrimeSize) 201 | 202 | msgCopy := msg.Copy() 203 | 204 | contents := make([]byte, MinimumPrimeSize*2-AssociatedDataSize-1) 205 | copy(contents, "test") 206 | msgCopy.SetContents(contents) 207 | 208 | if bytes.Equal(msg.GetContents(), contents) { 209 | t.Errorf("Copy failed to make a copy of the message; modifications " + 210 | "to copy reflected in original.") 211 | } 212 | } 213 | 214 | // Happy path. 215 | func TestMessage_GetPrimeByteLen(t *testing.T) { 216 | primeSize := 250 217 | m := NewMessage(primeSize) 218 | 219 | if m.GetPrimeByteLen() != primeSize { 220 | t.Errorf("GetPrimeByteLen returned incorrect prime size."+ 221 | "\nexpected: %d\nreceived: %d", primeSize, m.GetPrimeByteLen()) 222 | } 223 | } 224 | 225 | // Happy path. 226 | func TestMessage_GetPayloadA(t *testing.T) { 227 | msg := NewMessage(MinimumPrimeSize) 228 | 229 | testData := []byte("test") 230 | copy(msg.payloadA, testData) 231 | payload := msg.GetPayloadA() 232 | if !bytes.Equal(testData, payload[:len(testData)]) { 233 | t.Errorf("GetPayloadA did not properly retrieve payload A."+ 234 | "\nexpected: %s\nreceived: %s", testData, payload[:len(testData)]) 235 | } 236 | 237 | // Ensure that the data is copied 238 | payload[14] = 'x' 239 | if msg.payloadA[14] == payload[14] { 240 | t.Error("GetPayloadA did not make a copy; modifications to copy " + 241 | "reflected in original.") 242 | } 243 | } 244 | 245 | // Happy path. 246 | func TestMessage_SetPayloadA(t *testing.T) { 247 | msg := NewMessage(MinimumPrimeSize) 248 | payload := make([]byte, len(msg.payloadA)) 249 | copy(payload, "test") 250 | msg.SetPayloadA(payload) 251 | 252 | if !bytes.Equal(payload, msg.payloadA) { 253 | t.Errorf("SetPayloadA failed to set payload A correctly."+ 254 | "\nexpected: %s\nreceived: %s", payload, msg.payloadA) 255 | } 256 | } 257 | 258 | // Error path: length of provided payload incorrect. 259 | func TestMessage_SetPayloadA_LengthError(t *testing.T) { 260 | payload := make([]byte, MinimumPrimeSize/4) 261 | msg := NewMessage(MinimumPrimeSize) 262 | defer func() { 263 | if r := recover(); r == nil { 264 | t.Errorf("SetPayloadA failed to panic when the length of the "+ 265 | "provided payload (%d) is not the same as the message payload "+ 266 | "length (%d).", len(payload), len(msg.GetPayloadA())) 267 | } 268 | }() 269 | 270 | msg.SetPayloadA(payload) 271 | } 272 | 273 | // Happy path. 274 | func TestMessage_GetPayloadB(t *testing.T) { 275 | msg := NewMessage(MinimumPrimeSize) 276 | 277 | testData := []byte("test") 278 | copy(msg.payloadB, testData) 279 | payload := msg.GetPayloadB() 280 | if !bytes.Equal(testData, payload[:len(testData)]) { 281 | t.Errorf("GetPayloadB did not properly retrieve payload B."+ 282 | "\nexpected: %s\nreceived: %s", testData, payload[:len(testData)]) 283 | } 284 | 285 | // Ensure that the data is copied 286 | payload[14] = 'x' 287 | if msg.payloadB[14] == payload[14] { 288 | t.Error("GetPayloadB did not make a copy; modifications to copy " + 289 | "reflected in original.") 290 | } 291 | } 292 | 293 | // Happy path. 294 | func TestMessage_SetPayloadB(t *testing.T) { 295 | msg := NewMessage(MinimumPrimeSize) 296 | payload := make([]byte, len(msg.payloadB)) 297 | copy(payload, "test") 298 | msg.SetPayloadB(payload) 299 | 300 | if !bytes.Equal(payload, msg.payloadB) { 301 | t.Errorf("SetPayloadB failed to set payload B correctly."+ 302 | "\nexpected: %s\nreceived: %s", payload, msg.payloadB) 303 | } 304 | } 305 | 306 | // Error path: length of provided payload incorrect. 307 | func TestMessage_SetPayloadB_LengthError(t *testing.T) { 308 | payload := make([]byte, MinimumPrimeSize/4) 309 | msg := NewMessage(MinimumPrimeSize) 310 | defer func() { 311 | if r := recover(); r == nil { 312 | t.Errorf("SetPayloadB failed to panic when the length of the "+ 313 | "provided payload (%d) is not the same as the message payload "+ 314 | "length (%d).", len(payload), len(msg.GetPayloadB())) 315 | } 316 | }() 317 | 318 | msg.SetPayloadB(payload) 319 | } 320 | 321 | // Happy path. 322 | func TestMessage_ContentsSize(t *testing.T) { 323 | msg := NewMessage(MinimumPrimeSize) 324 | 325 | if msg.ContentsSize() != MinimumPrimeSize*2-AssociatedDataSize-1 { 326 | t.Errorf("ContentsSize returned the wrong content size."+ 327 | "\nexpected: %d\nreceived: %d", 328 | MinimumPrimeSize*2-AssociatedDataSize-1, msg.ContentsSize()) 329 | } 330 | } 331 | 332 | // Happy path. 333 | func TestMessage_GetContents(t *testing.T) { 334 | msg := NewMessage(MinimumPrimeSize) 335 | contents := makeAndFillSlice(MinimumPrimeSize*2-AssociatedDataSize-1, 'a') 336 | 337 | copy(msg.contents1, contents[:len(msg.contents1)]) 338 | copy(msg.contents2, contents[len(msg.contents1):]) 339 | 340 | retrieved := msg.GetContents() 341 | 342 | if !bytes.Equal(retrieved, contents) { 343 | t.Errorf("GetContents did not return the expected contents."+ 344 | "\nexpected: %s\nreceived: %s", contents, retrieved) 345 | } 346 | } 347 | 348 | // Happy path: set contents that is large enough to fit in both contents. 349 | func TestMessage_SetContents(t *testing.T) { 350 | msg := NewMessage(MinimumPrimeSize) 351 | contents := makeAndFillSlice(MinimumPrimeSize*2-AssociatedDataSize-1, 'a') 352 | 353 | msg.SetContents(contents) 354 | 355 | if !bytes.Equal(msg.contents1, contents[:len(msg.contents1)]) { 356 | t.Errorf("SetContents did not set contents1 correctly."+ 357 | "\nexpected: %s\nreceived: %s", 358 | contents[:len(msg.contents1)], msg.contents1) 359 | } 360 | 361 | if !bytes.Equal(msg.contents2, contents[len(msg.contents1):]) { 362 | t.Errorf("SetContents did not set contents2 correctly."+ 363 | "\nexpected: %s\nreceived: %s", 364 | contents[len(msg.contents1):], msg.contents2) 365 | } 366 | } 367 | 368 | // Happy path: set contents that is small enough to fit in the first contents. 369 | func TestMessage_SetContents_ShortContents(t *testing.T) { 370 | msg := NewMessage(MinimumPrimeSize) 371 | contents := makeAndFillSlice(MinimumPrimeSize-KeyFPLen-1, 'a') 372 | 373 | msg.SetContents(contents) 374 | 375 | if !bytes.Equal(msg.contents1, contents[:len(msg.contents1)]) { 376 | t.Errorf("SetContents did not set contents1 correctly."+ 377 | "\nexpected: %s\nreceived: %s", 378 | contents[:len(msg.contents1)], msg.contents1) 379 | } 380 | 381 | expectedContents2 := 382 | make([]byte, MinimumPrimeSize-MacLen-EphemeralRIDLen-SIHLen) 383 | if !bytes.Equal(msg.contents2, expectedContents2) { 384 | t.Errorf("SetContents did not set contents2 correctly."+ 385 | "\nexpected: %+v\nreceived: %+v", expectedContents2, msg.contents2) 386 | } 387 | } 388 | 389 | // Error path: content size too large. 390 | func TestMessage_SetContents_ContentsTooLargeError(t *testing.T) { 391 | msg := NewMessage(MinimumPrimeSize) 392 | contents := makeAndFillSlice(MinimumPrimeSize*2, 'a') 393 | 394 | defer func() { 395 | if r := recover(); r == nil { 396 | t.Errorf("SetContents failed to panic when the length of the "+ 397 | "provided contents (%d) is larger than the max content length "+ 398 | "(%d).", len(contents), len(msg.contents1)+len(msg.contents2)) 399 | } 400 | }() 401 | 402 | msg.SetContents(contents) 403 | } 404 | 405 | // Happy path. 406 | func TestMessage_GetRawContentsSize(t *testing.T) { 407 | msg := NewMessage(MinimumPrimeSize) 408 | 409 | expectedLen := (2 * MinimumPrimeSize) - RecipientIDLen 410 | 411 | if msg.GetRawContentsSize() != expectedLen { 412 | t.Errorf("GetRawContentsSize did not return the expected size."+ 413 | "\nexpected: %d\nreceived: %d", expectedLen, msg.GetRawContentsSize()) 414 | } 415 | } 416 | 417 | // Happy path. 418 | func TestMessage_GetRawContents(t *testing.T) { 419 | msg := NewMessage(MinimumPrimeSize) 420 | 421 | // Created expected data 422 | var expectedRawContents []byte 423 | keyFP := makeAndFillSlice(KeyFPLen, 'a') 424 | mac := makeAndFillSlice(MacLen, 'b') 425 | contents1 := makeAndFillSlice(MinimumPrimeSize-KeyFPLen-1, 'c') 426 | contents2 := makeAndFillSlice(MinimumPrimeSize-MacLen-RecipientIDLen, 'd') 427 | expectedRawContents = append(expectedRawContents, keyFP...) 428 | expectedRawContents = append(expectedRawContents, byte(messagePayloadVersion)) 429 | expectedRawContents = append(expectedRawContents, contents1...) 430 | expectedRawContents = append(expectedRawContents, mac...) 431 | expectedRawContents = append(expectedRawContents, contents2...) 432 | 433 | // Copy contents into message 434 | copy(msg.keyFP, keyFP) 435 | copy(msg.mac, mac) 436 | copy(msg.version, []byte{messagePayloadVersion}) 437 | copy(msg.contents1, contents1) 438 | copy(msg.contents2, contents2) 439 | 440 | // Make sure the 1st and middle+1 bits are 1 441 | msg.payloadA[0] |= 0b10000000 442 | msg.payloadB[0] |= 0b10000000 443 | 444 | rawContents := msg.GetRawContents() 445 | if !bytes.Equal(expectedRawContents, rawContents) { 446 | t.Errorf("GetRawContents did not return the expected raw contents."+ 447 | "\nexpected: %s\nreceived: %s", expectedRawContents, rawContents) 448 | } 449 | 450 | if rawContents[0]&0b10000000 != 0 { 451 | t.Errorf("First bit not set to zero") 452 | } 453 | 454 | fmt.Println(rawContents[msg.GetPrimeByteLen()]) 455 | 456 | if rawContents[msg.GetPrimeByteLen()]&0b10000000 != 0 { 457 | t.Errorf("middle plus one bit not set to zero") 458 | } 459 | 460 | } 461 | 462 | // Happy path. 463 | func TestMessage_SetRawContents(t *testing.T) { 464 | msg := NewMessage(MinimumPrimeSize) 465 | spLen := (2 * MinimumPrimeSize) - RecipientIDLen 466 | sp := make([]byte, spLen) 467 | 468 | fp := makeAndFillSlice(len(msg.keyFP), 'f') 469 | mac := makeAndFillSlice(len(msg.mac), 'm') 470 | c1 := makeAndFillSlice(len(msg.contents1), 'a') 471 | c2 := makeAndFillSlice(len(msg.contents2), 'b') 472 | 473 | copy(sp[:KeyFPLen], fp) 474 | copy(sp[MinimumPrimeSize:MinimumPrimeSize+MacLen], mac) 475 | 476 | copy(sp[KeyFPLen:MinimumPrimeSize], c1) 477 | copy(sp[MinimumPrimeSize+MacLen:2*MinimumPrimeSize-RecipientIDLen], c2) 478 | 479 | msg.SetRawContents(sp) 480 | 481 | if bytes.Contains(msg.keyFP, []byte("a")) || 482 | bytes.Contains(msg.keyFP, []byte("b")) || 483 | bytes.Contains(msg.keyFP, []byte("m")) || 484 | !bytes.Contains(msg.keyFP, []byte("f")) { 485 | t.Errorf("Setting raw payload failed, key fingerprint contains "+ 486 | "wrong data: %s", msg.keyFP) 487 | } 488 | 489 | if bytes.Contains(msg.mac, []byte("a")) || 490 | bytes.Contains(msg.mac, []byte("b")) || 491 | !bytes.Contains(msg.mac, []byte("m")) || 492 | bytes.Contains(msg.mac, []byte("f")) { 493 | t.Errorf( 494 | "Setting raw payload failed, mac contains wrong data: %s", msg.mac) 495 | } 496 | 497 | if !bytes.Contains(msg.contents1, []byte("a")) || 498 | bytes.Contains(msg.contents1, []byte("b")) || 499 | bytes.Contains(msg.contents1, []byte("m")) || 500 | bytes.Contains(msg.contents1, []byte("f")) { 501 | t.Errorf("Setting raw payload failed, contents1 contains wrong data: %s", 502 | msg.contents1) 503 | } 504 | 505 | if bytes.Contains(msg.contents2, []byte("a")) || 506 | !bytes.Contains(msg.contents2, []byte("b")) || 507 | bytes.Contains(msg.contents2, []byte("m")) || 508 | bytes.Contains(msg.contents2, []byte("f")) { 509 | t.Errorf("Setting raw payload failed, contents2 contains wrong data: %s", 510 | msg.contents2) 511 | } 512 | 513 | } 514 | 515 | // Error path: length of provided raw contents incorrect. 516 | func TestMessage_SetRawContents_LengthError(t *testing.T) { 517 | msg := NewMessage(MinimumPrimeSize) 518 | 519 | defer func() { 520 | if r := recover(); r == nil { 521 | t.Error("SetRawContents failed to panic when length of the " + 522 | "provided data is incorrect.") 523 | } 524 | }() 525 | 526 | msg.SetRawContents(make([]byte, MinimumPrimeSize)) 527 | } 528 | 529 | // Happy path. 530 | func TestMessage_GetKeyFP(t *testing.T) { 531 | msg := NewMessage(MinimumPrimeSize) 532 | keyFP := NewFingerprint(makeAndFillSlice(SIHLen, 'e')) 533 | msg.keyFP[0] |= 0b10000000 534 | copy(msg.keyFP, keyFP.Bytes()) 535 | 536 | if keyFP != msg.GetKeyFP() { 537 | t.Errorf("GetKeyFP failed to get the correct keyFP."+ 538 | "\nexpected: %+v\nreceived: %+v", keyFP, msg.GetKeyFP()) 539 | } 540 | 541 | // Ensure that the data is copied 542 | keyFP[2] = 'x' 543 | if msg.sih[2] == 'x' { 544 | t.Error("GetKeyFP failed to make a copy of keyFP.") 545 | } 546 | 547 | if msg.GetKeyFP()[0]&0b10000000 != 0 { 548 | t.Errorf("First bit not set to zero") 549 | } 550 | } 551 | 552 | // Happy path. 553 | func TestMessage_SetKeyFP(t *testing.T) { 554 | msg := NewMessage(MinimumPrimeSize) 555 | fp := NewFingerprint(makeAndFillSlice(SIHLen, 'e')) 556 | 557 | msg.SetKeyFP(fp) 558 | 559 | if !bytes.Equal(fp.Bytes(), msg.keyFP) { 560 | t.Errorf("SetKeyFP failed to set keyFP."+ 561 | "\nexpected: %+v\nreceived: %+v", fp, msg.keyFP) 562 | } 563 | } 564 | 565 | // Error path: first bit of provided data is not 0. 566 | func TestMessage_SetKeyFP_FirstBitError(t *testing.T) { 567 | msg := NewMessage(MinimumPrimeSize) 568 | fp := NewFingerprint([]byte{0b11111111}) 569 | 570 | defer func() { 571 | if r := recover(); r == nil { 572 | t.Error("SetKeyFP failed to panic when the first bit of the " + 573 | "provided data is not 0.") 574 | } 575 | }() 576 | 577 | msg.SetKeyFP(fp) 578 | } 579 | 580 | // Happy path. 581 | func TestMessage_GetMac(t *testing.T) { 582 | msg := NewMessage(MinimumPrimeSize) 583 | mac := makeAndFillSlice(MacLen, 'm') 584 | copy(msg.mac, mac) 585 | msg.mac[0] |= 0b10000000 586 | 587 | if !bytes.Equal(mac, msg.GetMac()) { 588 | t.Errorf("GetMac failed to get the correct MAC."+ 589 | "\nexpected: %+v\nreceived: %+v", mac, msg.GetMac()) 590 | } 591 | 592 | // Ensure that the data is copied 593 | mac[2] = 'x' 594 | if msg.mac[2] == 'x' { 595 | t.Error("GetMac failed to make a copy of mac.") 596 | } 597 | 598 | if msg.GetMac()[0]&0b10000000 != 0 { 599 | t.Errorf("First bit not set to zero") 600 | } 601 | } 602 | 603 | // Happy path. 604 | func TestMessage_SetMac(t *testing.T) { 605 | msg := NewMessage(MinimumPrimeSize) 606 | mac := makeAndFillSlice(MacLen, 'm') 607 | 608 | msg.SetMac(mac) 609 | 610 | if !bytes.Equal(mac, msg.mac) { 611 | t.Errorf("SetMac failed to set the MAC."+ 612 | "\nexpected: %+v\nreceived: %+v", mac, msg.mac) 613 | } 614 | } 615 | 616 | // Error path: first bit of provided data is not 0. 617 | func TestMessage_SetMac_FirstBitError(t *testing.T) { 618 | msg := NewMessage(MinimumPrimeSize) 619 | mac := make([]byte, MacLen) 620 | mac[0] = 0b11111111 621 | 622 | defer func() { 623 | if r := recover(); r == nil { 624 | t.Error("SetMac failed to panic when the first bit of the " + 625 | "provided data is not 0.") 626 | } 627 | }() 628 | 629 | msg.SetMac(mac) 630 | } 631 | 632 | // Error path: the length of the provided data is incorrect. 633 | func TestMessage_SetMac_LenError(t *testing.T) { 634 | msg := NewMessage(MinimumPrimeSize) 635 | mac := makeAndFillSlice(MacLen+1, 'm') 636 | 637 | defer func() { 638 | if r := recover(); r == nil { 639 | t.Error("SetMac failed to panic when the length of the provided " + 640 | "MAC is wrong.") 641 | } 642 | }() 643 | 644 | msg.SetMac(mac) 645 | } 646 | 647 | // Happy path. 648 | func TestMessage_GetEphemeralRID(t *testing.T) { 649 | msg := NewMessage(MinimumPrimeSize) 650 | ephemeralRID := makeAndFillSlice(EphemeralRIDLen, 'e') 651 | copy(msg.ephemeralRID, ephemeralRID) 652 | 653 | if !bytes.Equal(ephemeralRID, msg.GetEphemeralRID()) { 654 | t.Errorf("GetEphemeralRID failed to get the correct ephemeralRID."+ 655 | "\nexpected: %+v\nreceived: %+v", ephemeralRID, msg.GetEphemeralRID()) 656 | } 657 | 658 | // Ensure that the data is copied 659 | ephemeralRID[2] = 'x' 660 | if msg.ephemeralRID[2] == 'x' { 661 | t.Error("GetEphemeralRID failed to make a copy of ephemeralRID.") 662 | } 663 | } 664 | 665 | // Happy path. 666 | func TestMessage_SetEphemeralRID(t *testing.T) { 667 | msg := NewMessage(MinimumPrimeSize) 668 | ephemeralRID := makeAndFillSlice(EphemeralRIDLen, 'e') 669 | 670 | // Ensure that the data is copied 671 | msg.SetEphemeralRID(ephemeralRID) 672 | if !bytes.Equal(ephemeralRID, msg.ephemeralRID) { 673 | t.Errorf("SetEphemeralRID failed to set the ephemeralRID."+ 674 | "\nexpected: %+v\nreceived: %+v", ephemeralRID, msg.ephemeralRID) 675 | } 676 | } 677 | 678 | // Error path: provided ephemeral recipient ID data too short. 679 | func TestMessage_SetEphemeralRID_LengthError(t *testing.T) { 680 | msg := NewMessage(MinimumPrimeSize) 681 | 682 | defer func() { 683 | if r := recover(); r == nil { 684 | t.Errorf("SetEphemeralRID failed to panic when the length of " + 685 | "the provided data is incorrect.") 686 | } 687 | }() 688 | 689 | msg.SetEphemeralRID(make([]byte, EphemeralRIDLen*2)) 690 | } 691 | 692 | // Happy path. 693 | func TestMessage_GetIdentityFP(t *testing.T) { 694 | msg := NewMessage(MinimumPrimeSize) 695 | identityFP := makeAndFillSlice(SIHLen, 'e') 696 | copy(msg.sih, identityFP) 697 | 698 | if !bytes.Equal(identityFP, msg.GetSIH()) { 699 | t.Errorf("GetSIH failed to get the correct sih."+ 700 | "\nexpected: %+v\nreceived: %+v", identityFP, msg.GetSIH()) 701 | } 702 | 703 | // Ensure that the data is copied 704 | identityFP[2] = 'x' 705 | if msg.sih[2] == 'x' { 706 | t.Error("GetSIH failed to make a copy of sih.") 707 | } 708 | } 709 | 710 | // Happy path. 711 | func TestMessage_SetIdentityFP(t *testing.T) { 712 | msg := NewMessage(MinimumPrimeSize) 713 | identityFP := makeAndFillSlice(SIHLen, 'e') 714 | 715 | msg.SetSIH(identityFP) 716 | if !bytes.Equal(identityFP, msg.sih) { 717 | t.Errorf("SetSIH failed to set the sih."+ 718 | "\nexpected: %+v\nreceived: %+v", identityFP, msg.sih) 719 | } 720 | } 721 | 722 | // Error path: size of provided data is incorrect. 723 | func TestMessage_SetIdentityFP_LengthError(t *testing.T) { 724 | msg := NewMessage(MinimumPrimeSize) 725 | 726 | defer func() { 727 | if r := recover(); r == nil { 728 | t.Errorf("SetSIH failed to panic when the length of " + 729 | "the provided data is incorrect.") 730 | } 731 | }() 732 | 733 | msg.SetSIH(make([]byte, SIHLen*2)) 734 | } 735 | 736 | // Tests that digests come out correctly and are different. 737 | func TestMessage_Digest(t *testing.T) { 738 | 739 | expectedA := "/9SqCYEP3uUixw1ua1D7" 740 | expectedB := "v+183UhPfK61KCNeSClT" 741 | 742 | msgA := NewMessage(MinimumPrimeSize) 743 | 744 | contentsA := makeAndFillSlice(MinimumPrimeSize*2-AssociatedDataSize-1, 'a') 745 | 746 | msgA.SetContents(contentsA) 747 | 748 | digestA := msgA.Digest() 749 | 750 | if digestA != expectedA { 751 | t.Errorf("Digest A does not match expected: "+ 752 | "DigestA: %s, Expected: %s", digestA, expectedA) 753 | } 754 | 755 | msgB := NewMessage(MinimumPrimeSize) 756 | 757 | contentsB := makeAndFillSlice(MinimumPrimeSize*2-AssociatedDataSize-1, 'b') 758 | 759 | msgB.SetContents(contentsB) 760 | 761 | digestB := msgB.Digest() 762 | 763 | if digestB != expectedB { 764 | t.Errorf("Digest B does not match expected: "+ 765 | "DigestB: %s, Expected: %s", digestB, expectedB) 766 | } 767 | 768 | if digestA == digestB { 769 | t.Errorf("Digest A and Digest B are the same, they "+ 770 | "should be diferent; A: %s, B: %s", digestA, digestB) 771 | } 772 | } 773 | 774 | // Unit test of Message.GoString. 775 | func TestMessage_GoString(t *testing.T) { 776 | // Create message 777 | msg := NewMessage(MinimumPrimeSize) 778 | msg.SetKeyFP(NewFingerprint(makeAndFillSlice(KeyFPLen, 'c'))) 779 | msg.SetMac(makeAndFillSlice(MacLen, 'd')) 780 | msg.SetEphemeralRID(makeAndFillSlice(EphemeralRIDLen, 'e')) 781 | msg.SetSIH(makeAndFillSlice(SIHLen, 'f')) 782 | msg.SetContents(makeAndFillSlice(MinimumPrimeSize*2-AssociatedDataSize-1, 'g')) 783 | 784 | expected := 785 | "format.Message{keyFP:Y2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2M=, " + 786 | "MAC:ZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQ=, " + 787 | "ephemeralRID:7306357456645743973, " + 788 | "sih:ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZg==, " + 789 | "contents:\"gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg\"}" 790 | 791 | if expected != msg.GoString() { 792 | t.Errorf("GoString returned incorrect string."+ 793 | "\nexpected: %s\nreceived: %s", expected, msg.GoString()) 794 | } 795 | } 796 | 797 | // Unit test of Message.GoString with an empty Message. 798 | func TestMessage_GoString_EmptyMessage(t *testing.T) { 799 | var msg Message 800 | 801 | expected := "format.Message{keyFP:, MAC:, " + 802 | "ephemeralRID:, sih:, contents:\"\"}" 803 | 804 | if expected != msg.GoString() { 805 | t.Errorf("GoString returned incorrect string."+ 806 | "\nexpected: %s\nreceived: %s", expected, msg.GoString()) 807 | } 808 | } 809 | 810 | func TestMessage_SetGroupBits(t *testing.T) { 811 | 812 | var msgsToTest []Message 813 | 814 | for i := 0; i < 2; i++ { 815 | for j := 0; j < 2; j++ { 816 | msg := generateMsg() 817 | if i == 1 { 818 | msg.payloadA[0] |= 0b10000000 819 | } 820 | if j == 1 { 821 | msg.payloadB[0] |= 0b10000000 822 | } 823 | msgsToTest = append(msgsToTest, msg) 824 | } 825 | } 826 | 827 | count := 0 828 | for _, msg := range msgsToTest { 829 | for i := 0; i < 2; i++ { 830 | for j := 0; j < 2; j++ { 831 | msg.SetGroupBits(i == 1, j == 1) 832 | if int(msg.payloadA[0])>>7 != i { 833 | t.Errorf("first group bit not set. Expected %t, "+ 834 | "got: %t on test %d-A", i == 1, int(msg.payloadA[0])>>8 == 1, count) 835 | } 836 | if int(msg.payloadB[0])>>7 != j { 837 | t.Errorf("second group bit not set. Expected %t, "+ 838 | "got: %t on test %d-B", j == 1, int(msg.payloadB[0])>>8 == 1, count) 839 | } 840 | count++ 841 | } 842 | } 843 | } 844 | } 845 | 846 | func TestSetFirstBit(t *testing.T) { 847 | b := []byte{0, 0, 0} 848 | setFirstBit(b, true) 849 | if b[0] != 0b10000000 { 850 | t.Errorf("first bit did not set") 851 | } 852 | 853 | b = []byte{255, 0, 0} 854 | setFirstBit(b, false) 855 | if b[0] != 0b01111111 { 856 | t.Errorf("first bit did not get unset set") 857 | } 858 | } 859 | 860 | func generateMsg() Message { 861 | msg := NewMessage(MinimumPrimeSize) 862 | 863 | // Created expected data 864 | var expectedRawContents []byte 865 | keyFP := makeAndFillSlice(KeyFPLen, 'a') 866 | mac := makeAndFillSlice(MacLen, 'b') 867 | contents1 := makeAndFillSlice(MinimumPrimeSize-KeyFPLen, 'c') 868 | contents2 := makeAndFillSlice(MinimumPrimeSize-MacLen-RecipientIDLen, 'd') 869 | expectedRawContents = append(expectedRawContents, keyFP...) 870 | expectedRawContents = append(expectedRawContents, contents1...) 871 | expectedRawContents = append(expectedRawContents, mac...) 872 | expectedRawContents = append(expectedRawContents, contents2...) 873 | 874 | // Copy contents into message 875 | copy(msg.keyFP, keyFP) 876 | copy(msg.mac, mac) 877 | copy(msg.contents1, contents1) 878 | copy(msg.contents2, contents2) 879 | 880 | return msg 881 | } 882 | 883 | // makeAndFillSlice creates a slice of the specified size filled with the 884 | // specified rune. 885 | func makeAndFillSlice(size int, r rune) []byte { 886 | buff := make([]byte, size) 887 | buff = bytes.Map(func(r2 rune) rune { return r }, buff) 888 | return buff 889 | } 890 | -------------------------------------------------------------------------------- /knownRounds/knownRounds_test.go: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2024 xx foundation // 3 | // // 4 | // Use of this source code is governed by a license that can be found in the // 5 | // LICENSE file. // 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | package knownRounds 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "math" 14 | "math/rand" 15 | "reflect" 16 | "strings" 17 | "testing" 18 | 19 | "gitlab.com/xx_network/primitives/id" 20 | ) 21 | 22 | // Tests happy path of NewKnownRound. 23 | func TestNewKnownRound(t *testing.T) { 24 | expectedKR := &KnownRounds{ 25 | bitStream: uint64Buff{0, 0, 0, 0, 0}, 26 | firstUnchecked: 0, 27 | lastChecked: 0, 28 | fuPos: 0, 29 | } 30 | 31 | testKR := NewKnownRound(310) 32 | 33 | if !reflect.DeepEqual(testKR, expectedKR) { 34 | t.Errorf("NewKnownRound did not produce the expected KnownRounds."+ 35 | "\nexpected: %v\nreceived: %v", 36 | expectedKR, testKR) 37 | } 38 | } 39 | 40 | // Happy path. 41 | func TestNewFromParts(t *testing.T) { 42 | expected := &KnownRounds{ 43 | bitStream: uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}, 44 | firstUnchecked: 75, 45 | lastChecked: 150, 46 | fuPos: 75, 47 | } 48 | 49 | received := NewFromParts(expected.bitStream, expected.firstUnchecked, 50 | expected.lastChecked, expected.fuPos) 51 | 52 | if !reflect.DeepEqual(expected, received) { 53 | t.Errorf("NewFromParts did not return the expected KnownRounds."+ 54 | "\nexpected: %v\nreceived: %v", expected, received) 55 | } 56 | } 57 | 58 | // Tests happy path of KnownRounds.Marshal. 59 | func TestKnownRounds_Marshal_Unmarshal(t *testing.T) { 60 | testKR := &KnownRounds{ 61 | bitStream: uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}, 62 | firstUnchecked: 55, 63 | lastChecked: 270, 64 | fuPos: 55, 65 | } 66 | 67 | data := testKR.Marshal() 68 | 69 | newKR := &KnownRounds{} 70 | err := newKR.Unmarshal(data) 71 | if err != nil { 72 | t.Errorf("Unmarshal produced an error: %+v", err) 73 | } 74 | 75 | if !reflect.DeepEqual(testKR, newKR) { 76 | t.Errorf("Original KnownRounds does not match Unmarshalled."+ 77 | "\nexpected: %+v\nreceived: %+v", testKR, newKR) 78 | } 79 | } 80 | 81 | // Tests happy path of KnownRounds.Marshal. 82 | func TestKnownRounds_Marshal(t *testing.T) { 83 | testKR := &KnownRounds{ 84 | bitStream: uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}, 85 | firstUnchecked: 75, 86 | lastChecked: 150, 87 | fuPos: 75, 88 | } 89 | 90 | expectedData := []byte{75, 0, 0, 0, 0, 0, 0, 0, 150, 0, 0, 0, 0, 0, 0, 0, 2, 91 | 1, 255, 8, 0, 8} 92 | 93 | data := testKR.Marshal() 94 | 95 | if !bytes.Equal(expectedData, data) { 96 | t.Errorf("Marshal produced incorrect data."+ 97 | "\nexpected: %+v\nreceived: %+v", expectedData, data) 98 | } 99 | 100 | } 101 | 102 | // Tests happy path of KnownRounds.Unmarshal. 103 | func TestKnownRounds_Unmarshal(t *testing.T) { 104 | testKR := &KnownRounds{ 105 | bitStream: uint64Buff{0, math.MaxUint64, 0, 0, 0}, 106 | firstUnchecked: 75, 107 | lastChecked: 150, 108 | fuPos: 11, 109 | } 110 | 111 | data := testKR.Marshal() 112 | 113 | newKR := NewKnownRound(310) 114 | err := newKR.Unmarshal(data) 115 | if err != nil { 116 | t.Errorf("Unmarshal produced an unexpected error."+ 117 | "\nexpected: %+v\nreceived: %+v", nil, err) 118 | } 119 | 120 | if !reflect.DeepEqual(newKR, testKR) { 121 | t.Errorf("Unmarshal produced an incorrect KnownRounds from the data."+ 122 | "\nexpected: %v\nreceived: %v", testKR, newKR) 123 | } 124 | } 125 | 126 | // Tests that KnownRounds.Unmarshal errors when the new bit stream is too 127 | // small. 128 | func TestKnownRounds_Unmarshal_SizeError(t *testing.T) { 129 | testKR := &KnownRounds{ 130 | bitStream: uint64Buff{0, math.MaxUint64, 0, 0, 0}, 131 | firstUnchecked: 75, 132 | lastChecked: 150, 133 | fuPos: 11, 134 | } 135 | 136 | data := testKR.Marshal() 137 | 138 | newKR := NewKnownRound(1) 139 | err := newKR.Unmarshal(data) 140 | if err == nil { 141 | t.Error("Unmarshal did not produce an error when the size of new " + 142 | "KnownRound bit stream is too small.") 143 | } 144 | } 145 | 146 | // Tests that KnownRounds.Unmarshal errors when given invalid JSON data. 147 | func TestKnownRounds_Unmarshal_JsonError(t *testing.T) { 148 | newKR := NewKnownRound(1) 149 | err := newKR.Unmarshal([]byte("hello")) 150 | if err == nil { 151 | t.Error("Unmarshal did not produce an error on invalid JSON data.") 152 | } 153 | } 154 | 155 | // Happy path. 156 | func TestKnownRounds_OutputBuffChanges(t *testing.T) { 157 | // Generate test round IDs and expected buffers 158 | const max = math.MaxUint64 159 | testData := []struct { 160 | current KnownRounds 161 | old []uint64 162 | changes KrChanges 163 | }{{ 164 | current: KnownRounds{uint64Buff{}, 75, 320, 75}, 165 | old: []uint64{}, 166 | changes: KrChanges{}, 167 | }, { 168 | current: KnownRounds{uint64Buff{0, max, 0, max, 0}, 75, 320, 75}, 169 | old: []uint64{0, max, 0, max, 0}, 170 | changes: KrChanges{}, 171 | }, { 172 | current: KnownRounds{uint64Buff{0, max, 0, max, 0}, 75, 320, 75}, 173 | old: []uint64{0, max, 0, max, 0}, 174 | changes: KrChanges{}, 175 | }, { 176 | current: KnownRounds{uint64Buff{1, max, 0, max, 0}, 75, 320, 75}, 177 | old: []uint64{0, max, 0, max, 0}, 178 | changes: KrChanges{0: 1}, 179 | }, { 180 | current: KnownRounds{uint64Buff{0, max, 0, max, 0}, 75, 320, 75}, 181 | old: []uint64{max, 0, max, 0, max}, 182 | changes: KrChanges{0: 0, 1: max, 2: 0, 3: max, 4: 0}, 183 | }} 184 | 185 | for i, data := range testData { 186 | changes, firstUnchecked, lastChecked, fuPos, err := 187 | data.current.OutputBuffChanges(data.old) 188 | if err != nil { 189 | t.Errorf("OutputBuffChanges produced an error (%d): %+v", i, err) 190 | } 191 | 192 | if data.current.firstUnchecked != firstUnchecked { 193 | t.Errorf("OutputBuffChanges returned incorrect firstUnchecked (%d)."+ 194 | "\nexpected: %d\nreceived: %d", 195 | i, data.current.firstUnchecked, firstUnchecked) 196 | } 197 | 198 | if data.current.lastChecked != lastChecked { 199 | t.Errorf("OutputBuffChanges returned incorrect lastChecked (%d)."+ 200 | "\nexpected: %d\nreceived: %d", 201 | i, data.current.lastChecked, lastChecked) 202 | } 203 | 204 | if data.current.fuPos != fuPos { 205 | t.Errorf("OutputBuffChanges returned incorrect fuPos (%d)."+ 206 | "\nexpected: %d\nreceived: %d", i, data.current.fuPos, fuPos) 207 | } 208 | 209 | if !reflect.DeepEqual(data.changes, changes) { 210 | t.Errorf("OutputBuffChanges returned incorrect changes (%d)."+ 211 | "\nexpected: %v\nreceived: %v", i, data.changes, changes) 212 | } 213 | } 214 | } 215 | 216 | // Error path: buffers are not the same length. 217 | func TestKnownRounds_OutputBuffChanges_IncorrectLengthError(t *testing.T) { 218 | // Generate test round IDs and expected buffers 219 | const max = math.MaxUint64 220 | testData := []struct { 221 | current KnownRounds 222 | old []uint64 223 | }{{ 224 | current: KnownRounds{uint64Buff{0, max, 0, max, 0}, 75, 320, 75}, 225 | old: []uint64{0, max, 0}, 226 | }, { 227 | current: KnownRounds{uint64Buff{0, max, 0}, 75, 320, 75}, 228 | old: []uint64{0, max, 0, max, 0}, 229 | }} 230 | 231 | expectedErr := "not the same as length of the current buffer" 232 | for i, data := range testData { 233 | _, _, _, _, err := data.current.OutputBuffChanges(data.old) 234 | if err == nil || !strings.Contains(err.Error(), expectedErr) { 235 | t.Errorf("OutputBuffChanges did not produce the expected error "+ 236 | "when the buffers are the wrong lengths (%d)."+ 237 | "\nexpected: %s\nreceived: %+v", i, expectedErr, err) 238 | } 239 | } 240 | } 241 | 242 | // Tests that KnownRounds.GetFirstUnchecked returns the expected value. 243 | func TestKnownRounds_GetFirstUnchecked(t *testing.T) { 244 | kr := KnownRounds{ 245 | bitStream: uint64Buff{0, 1, 2, 3, 4, 5, 6, 7}, 246 | firstUnchecked: 65, 247 | lastChecked: 556, 248 | fuPos: 1, 249 | } 250 | 251 | if kr.firstUnchecked != kr.GetFirstUnchecked() { 252 | t.Errorf("GetFirstUnchecked did not return the expected value."+ 253 | "\nexpected: %d\nreceived: %d", kr.firstUnchecked, kr.GetFirstUnchecked()) 254 | } 255 | } 256 | 257 | // Tests that KnownRounds.GetLastChecked returns the expected value. 258 | func TestKnownRounds_GetLastChecked(t *testing.T) { 259 | kr := KnownRounds{ 260 | bitStream: uint64Buff{0, 1, 2, 3, 4, 5, 6, 7}, 261 | firstUnchecked: 65, 262 | lastChecked: 556, 263 | fuPos: 1, 264 | } 265 | 266 | if kr.lastChecked != kr.GetLastChecked() { 267 | t.Errorf("GetLastChecked did not return the expected value."+ 268 | "\nexpected: %d\nreceived: %d", kr.lastChecked, kr.GetLastChecked()) 269 | } 270 | } 271 | 272 | // Tests that KnownRounds.GetFuPos returns the expected value. 273 | func TestKnownRounds_GetFuPos(t *testing.T) { 274 | kr := KnownRounds{ 275 | bitStream: uint64Buff{0, 1, 2, 3, 4, 5, 6, 7}, 276 | firstUnchecked: 65, 277 | lastChecked: 556, 278 | fuPos: 1, 279 | } 280 | 281 | if kr.fuPos != kr.GetFuPos() { 282 | t.Errorf("GetFuPos did not return the expected value."+ 283 | "\nexpected: %d\nreceived: %d", kr.fuPos, kr.GetFuPos()) 284 | } 285 | } 286 | 287 | // Tests that KnownRounds.GetBitStream returns the expected value. 288 | func TestKnownRounds_GetBitStream(t *testing.T) { 289 | kr := KnownRounds{ 290 | bitStream: uint64Buff{0, 1, 2, 3, 4, 5, 6, 7}, 291 | firstUnchecked: 65, 292 | lastChecked: 556, 293 | fuPos: 1, 294 | } 295 | 296 | if !reflect.DeepEqual([]uint64(kr.bitStream), kr.GetBitStream()) { 297 | t.Errorf("GetFuPos did not return the expected value."+ 298 | "\nexpected: %#v\nreceived: %#v", kr.bitStream, kr.GetBitStream()) 299 | } 300 | } 301 | 302 | // Tests happy path of KnownRounds.Check. 303 | func TestKnownRounds_Check(t *testing.T) { 304 | // Generate test round IDs and expected buffers 305 | testData := []struct { 306 | rid, expectedLastChecked id.Round 307 | buff uint64Buff 308 | }{ 309 | {0, 200, uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}}, 310 | {75, 200, uint64Buff{4503599627370496, math.MaxUint64, 0, math.MaxUint64, 0}}, 311 | {95, 200, uint64Buff{4294967296, math.MaxUint64, 0, math.MaxUint64, 0}}, 312 | {150, 200, uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}}, 313 | {320, 320, uint64Buff{0, math.MaxUint64, 0, 0, 0x8000000000000000}}, 314 | {519, 519, uint64Buff{0, 0, 0x100000000000000, 0, 0}}, 315 | } 316 | 317 | for i, data := range testData { 318 | kr := KnownRounds{ 319 | bitStream: uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}, 320 | firstUnchecked: 75, 321 | lastChecked: 200, 322 | fuPos: 11, 323 | } 324 | 325 | kr.Check(data.rid) 326 | if !reflect.DeepEqual(kr.bitStream, data.buff) { 327 | t.Errorf("Incorrect resulting buffer after checking round ID %d (%d)."+ 328 | "\nexpected: %064b\nreceived: %064b"+ 329 | "\n\033[38;5;59m 0123456789012345678901234567890123456789012345678901234567890123 4567890123456789012345678901234567890123456789012345678901234567 8901234567890123456789012345678901234567890123456789012345678901 2345678901234567890123456789012345678901234567890123456789012345 6789012345678901234567890123456789012345678901234567890123456789 0123456789012345678901234567890123456789012345678901234567890123"+ 330 | "\n\u001B[38;5;59m 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8"+ 331 | "\n\u001B[38;5;59m 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3", 332 | data.rid, i, data.buff, kr.bitStream) 333 | } 334 | 335 | if kr.lastChecked != data.expectedLastChecked { 336 | t.Errorf("Check did not modify the lastChecked round correctly "+ 337 | "for round ID %d (%d).\nexpected: %d\nreceived: %d", 338 | data.rid, i, data.expectedLastChecked, kr.lastChecked) 339 | } 340 | } 341 | } 342 | 343 | // Tests happy path of KnownRounds.Check with a new KnownRounds. 344 | func TestKnownRounds_Check_NewKR(t *testing.T) { 345 | // Generate test round IDs and expected buffers 346 | testData := []struct { 347 | rid, expectedLastChecked id.Round 348 | buff uint64Buff 349 | }{ 350 | {1, 1, uint64Buff{0x4000000000000000, 0, 0, 0, 0}}, 351 | {0, 1, uint64Buff{0x8000000000000000, 0, 0, 0, 0}}, 352 | {75, 75, uint64Buff{0, 0x10000000000000, 0, 0, 0}}, 353 | {319, 319, uint64Buff{0, 0, 0, 0, 1}}, 354 | } 355 | 356 | for i, data := range testData { 357 | kr := NewKnownRound(310) 358 | kr.Check(data.rid) 359 | if !reflect.DeepEqual(kr.bitStream, data.buff) { 360 | t.Errorf("Resulting buffer after checking round ID %d (%d)."+ 361 | "\nexpected: %064b\nreceived: %064b", 362 | data.rid, i, data.buff, kr.bitStream) 363 | } 364 | 365 | if kr.lastChecked != data.expectedLastChecked { 366 | t.Errorf("Check did not modify the lastChecked round correctly "+ 367 | "for round ID %d (%d).\nexpected: %d\nreceived: %d", 368 | data.rid, i, data.expectedLastChecked, kr.lastChecked) 369 | } 370 | } 371 | } 372 | 373 | // Happy path of KnownRounds.Checked. 374 | func TestKnownRounds_Checked(t *testing.T) { 375 | // Generate test positions and expected value 376 | testData := []struct { 377 | rid id.Round 378 | value bool 379 | }{ 380 | {75, false}, 381 | {76, false}, 382 | {123, false}, 383 | {124, false}, 384 | {74, true}, 385 | {60, true}, 386 | {0, true}, 387 | {319, false}, 388 | {320, false}, 389 | } 390 | kr := KnownRounds{ 391 | bitStream: uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}, 392 | firstUnchecked: 75, 393 | lastChecked: 200, 394 | fuPos: 11, 395 | } 396 | 397 | for i, data := range testData { 398 | value := kr.Checked(data.rid) 399 | if value != data.value { 400 | t.Errorf("Checked returned incorrect value for round ID %d (%d)."+ 401 | "\nexpected: %v\nreceived: %v", data.rid, i, data.value, value) 402 | } 403 | } 404 | } 405 | 406 | // Happy path of KnownRounds.Checked with a new KnownRounds. 407 | func TestKnownRounds_Checked_NewKR(t *testing.T) { 408 | // Generate test positions and expected value 409 | testData := []struct { 410 | rid id.Round 411 | value bool 412 | }{ 413 | {0, false}, 414 | {1, false}, 415 | {2, false}, 416 | {320, false}, 417 | } 418 | 419 | for i, data := range testData { 420 | kr := NewKnownRound(5) 421 | value := kr.Checked(data.rid) 422 | if value != data.value { 423 | t.Errorf("Checked returned incorrect value for round ID %d (%d)."+ 424 | "\nexpected: %v\nreceived: %v", data.rid, i, data.value, value) 425 | } 426 | } 427 | } 428 | 429 | // Tests happy path of KnownRounds.Forward. 430 | func TestKnownRounds_Forward(t *testing.T) { 431 | // Generate test round IDs and expected buffers 432 | testData := []struct { 433 | rid, expectedFirstChecked, expectedLastChecked id.Round 434 | expectedFusPos int 435 | }{ 436 | {75, 75, 200, 11}, 437 | {76, 76, 200, 12}, 438 | {192, 192, 200, 128}, 439 | {150, 192, 200, 128}, 440 | {200, 200, 200, 136}, 441 | {210, 210, 210, 210 % 64}, 442 | } 443 | kr := KnownRounds{ 444 | bitStream: uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}, 445 | firstUnchecked: 75, 446 | lastChecked: 200, 447 | fuPos: 11, 448 | } 449 | 450 | for i, data := range testData { 451 | kr.bitStream = uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0} 452 | kr.Forward(data.rid) 453 | if kr.firstUnchecked != data.expectedFirstChecked { 454 | t.Errorf("Forward did not modify the firstUnchecked round "+ 455 | "correctly for round ID %d (%d).\nexpected: %d\nreceived: %d", 456 | data.rid, i, data.expectedFirstChecked, kr.firstUnchecked) 457 | } 458 | if kr.lastChecked != data.expectedLastChecked { 459 | t.Errorf("Forward did not modify the lastChecked round correctly "+ 460 | "or round ID %d (%d).\nexpected: %d\nreceived: %d", 461 | data.rid, i, data.expectedLastChecked, kr.lastChecked) 462 | } 463 | if kr.fuPos != data.expectedFusPos { 464 | t.Errorf("Forward did not modify the fuPos round correctly for "+ 465 | "round ID %d (%d).\nexpected: %d\nreceived: %d", 466 | data.rid, i, data.expectedFusPos, kr.fuPos) 467 | } 468 | } 469 | } 470 | 471 | // Tests happy path of KnownRounds.Forward with a new KnownRounds. 472 | func TestKnownRounds_Forward_NewKR(t *testing.T) { 473 | // Generate test round IDs and expected buffers 474 | testData := []struct { 475 | rid, expectedFirstUnchecked, expectedLastChecked id.Round 476 | expectedFusPos int 477 | }{ 478 | {0, 0, 0, 0}, 479 | {1, 1, 1, 1}, 480 | {2, 2, 2, 2}, 481 | {320, 320, 320, 0}, 482 | } 483 | 484 | for i, data := range testData { 485 | kr := NewKnownRound(5) 486 | kr.Forward(data.rid) 487 | if kr.firstUnchecked != data.expectedFirstUnchecked { 488 | t.Errorf("Forward did not modify the firstUnchecked round "+ 489 | "correctly for round ID %d (%d).\nexpected: %d\nreceived: %d", 490 | data.rid, i, data.expectedFirstUnchecked, kr.firstUnchecked) 491 | } 492 | if kr.lastChecked != data.expectedLastChecked { 493 | t.Errorf("Forward did not modify the lastChecked round correctly "+ 494 | "for round ID %d (%d).\nexpected: %d\nreceived: %d", 495 | data.rid, i, data.expectedLastChecked, kr.lastChecked) 496 | } 497 | if kr.fuPos != data.expectedFusPos { 498 | t.Errorf("Forward did not modify the fuPos round correctly for "+ 499 | "round ID %d (%d).\nexpected: %d\nreceived: %d", 500 | data.rid, i, data.expectedFusPos, kr.fuPos) 501 | } 502 | } 503 | } 504 | 505 | // Test happy path of KnownRounds.RangeUnchecked. 506 | func TestKnownRounds_RangeUnchecked(t *testing.T) { 507 | // Generate test round IDs and expected buffers 508 | testData := []struct { 509 | oldestUnknown, expected id.Round 510 | has, unknown []id.Round 511 | }{ 512 | {55, 141, makeRange(55, 127), makeRange(128, 140)}, 513 | {65, 141, makeRange(65, 127), makeRange(128, 140)}, 514 | {75, 141, makeRange(75, 127), makeRange(128, 140)}, 515 | {85, 141, makeRange(85, 127), makeRange(128, 140)}, 516 | {191, 191, nil, nil}, 517 | {192, 192, nil, nil}, 518 | {292, 292, nil, nil}, 519 | } 520 | roundCheck := func(id id.Round) bool { 521 | return true 522 | } 523 | 524 | // Bitstream = 0x00000000, 0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000 525 | // ^ firstUnchecked, fuPos (position 75) 526 | // ^ lastChecked (position 191) 527 | // ^-^ unknown (position 128 - 140) 528 | // ^-----^ has (position 64 - 127) 529 | // xx also has (position 55 - 63), outside defined buffer 530 | for i, data := range testData { 531 | kr := KnownRounds{ 532 | bitStream: uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}, 533 | firstUnchecked: 75, 534 | lastChecked: 191, 535 | fuPos: 75, 536 | } 537 | 538 | earliestRound, has, unknown := 539 | kr.RangeUnchecked(data.oldestUnknown, 50, roundCheck, 1000) 540 | 541 | if earliestRound != data.expected { 542 | t.Errorf("RangeUnchecked did not return the correct round (%d)."+ 543 | "\nexpected: %d\nreceived: %d", 544 | i, data.expected, earliestRound) 545 | } 546 | 547 | if len(data.has) != len(has) { 548 | t.Errorf("RangeUnchecked did not return the correct has list (%d)."+ 549 | "\nexpected: %v\nreceived: %v", 550 | i, data.has, has) 551 | } 552 | 553 | if !reflect.DeepEqual(data.unknown, unknown) { 554 | t.Errorf("RangeUnchecked did not return the correct unknown list (%d)."+ 555 | "\nexpected: %v\nreceived: %v", 556 | i, data.unknown, unknown) 557 | } 558 | } 559 | } 560 | 561 | // Test happy path of KnownRounds.RangeUnchecked with a new KnownRounds. 562 | func TestKnownRounds_RangeUnchecked_NewKR(t *testing.T) { 563 | // Generate test round IDs and expected buffers 564 | testData := []struct { 565 | oldestUnknown, expected id.Round 566 | has, unknown []id.Round 567 | }{ 568 | {55, 55, nil, nil}, 569 | {65, 65, nil, nil}, 570 | {75, 75, nil, nil}, 571 | {85, 85, nil, nil}, 572 | {191, 191, nil, nil}, 573 | {192, 192, nil, nil}, 574 | {292, 292, nil, nil}, 575 | } 576 | roundCheck := func(id id.Round) bool { 577 | return id%2 == 1 578 | } 579 | 580 | for i, data := range testData { 581 | kr := NewKnownRound(310) 582 | 583 | earliestRound, has, unknown := 584 | kr.RangeUnchecked(data.oldestUnknown, 50, roundCheck, 1000) 585 | 586 | if earliestRound != data.expected { 587 | t.Errorf("RangeUnchecked did not return the correct round (%d)."+ 588 | "\nexpected: %d\nreceived: %d", 589 | i, data.expected, earliestRound) 590 | } 591 | 592 | if !reflect.DeepEqual(data.has, has) { 593 | t.Errorf("RangeUnchecked did not return the correct has list (%d)."+ 594 | "\nexpected: %v\nreceived: %v", 595 | i, data.has, has) 596 | } 597 | 598 | if !reflect.DeepEqual(data.unknown, unknown) { 599 | t.Errorf("RangeUnchecked did not return the correct unknown list (%d)."+ 600 | "\nexpected: %v\nreceived: %v", 601 | i, data.unknown, unknown) 602 | } 603 | } 604 | } 605 | 606 | // Test happy path of KnownRounds.RangeUncheckedMasked. 607 | func TestKnownRounds_RangeUncheckedMasked(t *testing.T) { 608 | expectedKR := KnownRounds{ 609 | bitStream: uint64Buff{42949672960, 18446744073709551615, 0, 18446744073709551615, 0}, 610 | firstUnchecked: 15, 611 | lastChecked: 191, 612 | fuPos: 0, 613 | } 614 | kr := KnownRounds{ 615 | bitStream: uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}, 616 | firstUnchecked: 15, 617 | lastChecked: 191, 618 | fuPos: 0, 619 | } 620 | kr2 := &KnownRounds{ 621 | bitStream: uint64Buff{math.MaxUint64}, 622 | firstUnchecked: 20, 623 | lastChecked: 47, 624 | fuPos: 0, 625 | } 626 | 627 | roundCheck := func(id id.Round) bool { 628 | return id%2 == 1 629 | } 630 | 631 | kr.RangeUncheckedMasked(kr2, roundCheck, 5) 632 | if !reflect.DeepEqual(expectedKR, kr) { 633 | t.Errorf("RangeUncheckedMasked incorrectl modified KnownRounds."+ 634 | "\nexpected: %+v\nreceived: %+v", expectedKR, kr) 635 | } 636 | fmt.Printf("kr.bitStream: %+v\n", kr.bitStream) 637 | } 638 | 639 | // Happy path of getBitStreamPos. 640 | func TestKnownRounds_getBitStreamPos(t *testing.T) { 641 | // Generate test round IDs and their expected positions 642 | testData := []struct { 643 | rid id.Round 644 | pos int 645 | }{ 646 | {75, 11}, 647 | {76, 12}, 648 | {123, 59}, 649 | {124, 60}, 650 | {74, 10}, 651 | {60, 316}, 652 | {0, 256}, 653 | {319, 255}, 654 | {320, 256}, 655 | } 656 | kr := KnownRounds{ 657 | bitStream: uint64Buff{0, math.MaxUint64, 0, math.MaxUint64, 0}, 658 | firstUnchecked: 75, 659 | lastChecked: 85, 660 | fuPos: 11, 661 | } 662 | for i, data := range testData { 663 | pos := kr.getBitStreamPos(data.rid) 664 | if pos != data.pos { 665 | t.Errorf("getBitStreamPos returned incorrect position for round "+ 666 | "ID %d (%d).\nexpected: %v\nreceived: %v", 667 | data.rid, i, data.pos, pos) 668 | } 669 | } 670 | } 671 | 672 | /* 673 | // Test happy path of KnownRounds.RangeUncheckedMasked. 674 | func TestKnownRounds_RangeUncheckedMasked_2(t *testing.T) { 675 | expectedKR := KnownRounds{ 676 | bitStream: make(uint64Buff, 245), 677 | firstUnchecked: 30, 678 | lastChecked: 57, 679 | fuPos: 30, 680 | } 681 | expectedKR.bitStream[0] = 0xFFFFFFFD40000040 682 | kr := KnownRounds{ 683 | bitStream: make(uint64Buff, 245), 684 | firstUnchecked: 30, 685 | lastChecked: 39, 686 | fuPos: 30, 687 | } 688 | kr.bitStream[0] = 0xFFFFFFFC00000000 689 | 690 | mask := &KnownRounds{ 691 | bitStream: uint64Buff{0xFEFFFFFBFFFFFFC0}, 692 | firstUnchecked: 7, 693 | lastChecked: 57, 694 | fuPos: 7, 695 | } 696 | 697 | roundCheck := func(id id.Round) bool { 698 | return id%2 == 1 699 | } 700 | 701 | kr.RangeUncheckedMasked(mask, roundCheck, 5) 702 | if !reflect.DeepEqual(expectedKR, kr) { 703 | t.Errorf("RangeUncheckedMasked incorrect modified KnownRounds."+ 704 | "\nexpected: %+v\nreceived: %+v", expectedKR, kr) 705 | } 706 | fmt.Printf("kr.bitStream: %064b\n", kr.bitStream) 707 | }*/ 708 | 709 | // // Test happy path of KnownRounds.RangeUncheckedMasked. 710 | // func TestKnownRounds_RangeUncheckedMasked_3(t *testing.T) { 711 | // expectedKR := KnownRounds{ 712 | // bitStream: make(uint64Buff, 245), 713 | // firstUnchecked: 30, 714 | // lastChecked: 57, 715 | // fuPos: 30, 716 | // } 717 | // expectedKR.bitStream[0] = 0b1111111111010000000000000000000000000000000000000000000000000000 718 | // kr := KnownRounds{ 719 | // bitStream: make(uint64Buff, 245), 720 | // firstUnchecked: 9, 721 | // lastChecked: 9, 722 | // fuPos: 9, 723 | // } 724 | // kr.bitStream[0] = 0b1111111111000000000000000000000000000000000000000000000000000000 725 | // 726 | // mask := &KnownRounds{ 727 | // bitStream: uint64Buff{0b1111111101111111111111111111111111111111111111111111111111111111, 0b1111100000000000000000000000000000000000000000000000000000000000}, 728 | // firstUnchecked: 8, 729 | // lastChecked: 68, 730 | // fuPos: 8, 731 | // } 732 | // 733 | // roundCheck := func(id id.Round) bool { 734 | // return id%2 == 1 735 | // } 736 | // 737 | // kr.RangeUncheckedMasked(mask, roundCheck, 5) 738 | // if !reflect.DeepEqual(expectedKR, kr) { 739 | // t.Errorf("RangeUncheckedMasked incorrect modified KnownRounds."+ 740 | // "\nexpected: %064b\nreceived: %064b", expectedKR, kr) 741 | // } 742 | // fmt.Printf("kr.bitStream: %064b\n", kr.bitStream) 743 | // } 744 | 745 | // 746 | // // Tests that KnownRounds.subSample produces the correct buffer for a new 747 | // // KnownRounds. 748 | // func TestKnownRounds_subSample(t *testing.T) { 749 | // kr := NewKnownRound(1) 750 | // expectedU64b := make(uint64Buff, 3) 751 | // 752 | // fmt.Printf("kr: %+v\n", kr) 753 | // 754 | // u64b, length := kr.subSample(5, 189) 755 | // if !reflect.DeepEqual(expectedU64b, u64b) { 756 | // t.Errorf("subSample returned incorrect buffer." + 757 | // "\nexpected: %064b\nreceived: %064b", expectedU64b, u64b) 758 | // } 759 | // 760 | // if len(expectedU64b) != length { 761 | // t.Errorf("subSample returned incorrect buffer length." + 762 | // "\nexpected: %d\nreceived: %d", len(expectedU64b), length) 763 | // } 764 | // } 765 | // 766 | // // Tests that KnownRounds.subSample produces the correct buffer for a new 767 | // // KnownRounds. 768 | // func TestKnownRounds_subSample2(t *testing.T) { 769 | // kr := &KnownRounds{ 770 | // bitStream: make(uint64Buff, 15626), 771 | // firstUnchecked: 23, 772 | // lastChecked: 22, 773 | // fuPos: 23, 774 | // } 775 | // mask := &KnownRounds{ 776 | // bitStream: make(uint64Buff, 1), 777 | // firstUnchecked: 0, 778 | // lastChecked: 1, 779 | // fuPos: 0, 780 | // } 781 | // fmt.Printf("mask: %+v\n", mask) 782 | // mask.Forward(kr.firstUnchecked) 783 | // fmt.Printf("mask: %+v\n", mask) 784 | // expectedU64b := make(uint64Buff, 3) 785 | // 786 | // 787 | // u64b, length := kr.subSample(mask.firstUnchecked, mask.lastChecked) 788 | // if !reflect.DeepEqual(expectedU64b, u64b) { 789 | // t.Errorf("subSample returned incorrect buffer." + 790 | // "\nexpected: %064b\nreceived: %064b", expectedU64b, u64b) 791 | // } 792 | // 793 | // if len(expectedU64b) != length { 794 | // t.Errorf("subSample returned incorrect buffer length." + 795 | // "\nexpected: %d\nreceived: %d", len(expectedU64b), length) 796 | // } 797 | // } 798 | // 799 | // func TestKnownRounds_RangeUncheckedMasked2(t *testing.T) { 800 | // kr := &KnownRounds{ 801 | // bitStream: make(uint64Buff, 15626), 802 | // firstUnchecked: 23, 803 | // lastChecked: 22, 804 | // fuPos: 23, 805 | // } 806 | // mask := &KnownRounds{ 807 | // bitStream: make(uint64Buff, 1), 808 | // firstUnchecked: 0, 809 | // lastChecked: 1, 810 | // fuPos: 0, 811 | // } 812 | // 813 | // roundCheck := func(id id.Round) bool { 814 | // return id%2 == 1 815 | // } 816 | // 817 | // kr.RangeUncheckedMasked(mask, roundCheck, 500) 818 | // } 819 | 820 | func TestKnownRounds_Truncate(t *testing.T) { 821 | kr := KnownRounds{ 822 | bitStream: uint64Buff{math.MaxUint64, 0, math.MaxUint64, 0}, 823 | firstUnchecked: 64, 824 | lastChecked: 130, 825 | fuPos: 1, 826 | } 827 | 828 | newKR := kr.Truncate(74) 829 | 830 | if newKR.firstUnchecked != 127 { 831 | t.Errorf("Failed to truncate. First unchecked not migrated correctly."+ 832 | "\nexpected: %d\nreceived: %d", 127, newKR.firstUnchecked) 833 | } 834 | 835 | krBytes := kr.Marshal() 836 | newKrBytes := newKR.Marshal() 837 | 838 | if len(newKrBytes) >= len(krBytes) { 839 | t.Errorf("Marshalled truncated KR larger than original."+ 840 | "\nexpected: %d\nrecived: %d", len(krBytes), len(newKrBytes)) 841 | } 842 | } 843 | 844 | // Same as test above but checking in the case that the circular buffer has 845 | // wrapped around. 846 | func TestKnownRounds_Truncate_Wrap_Around(t *testing.T) { 847 | kr := KnownRounds{ 848 | bitStream: uint64Buff{math.MaxUint64, 0, math.MaxUint64, 0}, 849 | firstUnchecked: 320, 850 | lastChecked: 390, 851 | fuPos: 1, 852 | } 853 | 854 | newKR := kr.Truncate(330) 855 | 856 | if newKR.firstUnchecked != 383 { 857 | t.Errorf("Failed to truncate. First unchecked not migrated correctly."+ 858 | "\nexpected: %d\nreceived: %d", 383, newKR.firstUnchecked) 859 | } 860 | 861 | krBytes := kr.Marshal() 862 | newKrBytes := newKR.Marshal() 863 | 864 | if len(newKrBytes) >= len(krBytes) { 865 | t.Errorf("Marshalled truncated KR larger than original."+ 866 | "\nexpected: %d\nrecived: %d", len(krBytes), len(newKrBytes)) 867 | } 868 | } 869 | 870 | // Simulate saving and reading from the database by: 871 | // 1. make random edits to the KnownRounds 872 | // 2. save after each random edit (KnownRounds.OutputBuffChanges) 873 | // 3. reconstructs the KnownRounds from the saved data (NewFromParts) 874 | // 4. compare the original KnownRounds to the reconstructed KnownRounds 875 | func TestKnownRounds_Database_Simulation(t *testing.T) { 876 | prng := rand.New(rand.NewSource(42)) 877 | n := 255 878 | 879 | kr := &KnownRounds{ 880 | bitStream: makeRandomUint64Slice(n, prng), 881 | firstUnchecked: 5, 882 | lastChecked: id.Round(n * 64), 883 | fuPos: 5, 884 | } 885 | 886 | saved := kr 887 | var err error 888 | var changes KrChanges 889 | 890 | for i := 0; i < 100; i++ { 891 | t.Logf("%d %v", i, kr) 892 | // Modify random round 893 | kr.Check(id.Round(prng.Int63n(int64(kr.lastChecked)))) 894 | t.Logf("%d %v", i, kr) 895 | 896 | // Save changes 897 | changes, saved.firstUnchecked, saved.lastChecked, saved.fuPos, err = 898 | kr.OutputBuffChanges(saved.bitStream) 899 | if err != nil { 900 | t.Errorf("Failed to output changed (%d): %+v", i, err) 901 | } 902 | 903 | // Apply changes to saved KnownRounds 904 | for j, word := range changes { 905 | saved.bitStream[j] = word 906 | } 907 | 908 | // Reconstructs the KnownRounds from the saved data 909 | newKR := NewFromParts(saved.bitStream, 910 | saved.firstUnchecked, saved.lastChecked, saved.fuPos) 911 | 912 | // Compare the original KnownRounds to the reconstructed KnownRounds 913 | if !reflect.DeepEqual(kr, newKR) { 914 | t.Errorf("Reconstructed KnownRounds does not match original."+ 915 | "\nexpected: %v\nreceived: %v", kr, newKR) 916 | } 917 | } 918 | } 919 | 920 | func makeRandomUint64Slice(n int, prng *rand.Rand) []uint64 { 921 | uints := make([]uint64, n) 922 | for i := range uints { 923 | uints[i] = prng.Uint64() 924 | } 925 | return uints 926 | } 927 | 928 | func makeRange(min, max int) []id.Round { 929 | a := make([]id.Round, max-min+1) 930 | for i := range a { 931 | a[i] = id.Round(min + i) 932 | } 933 | return a 934 | } 935 | 936 | func TestKnownRounds_Len(t *testing.T) { 937 | kr := NewKnownRound(0) 938 | 939 | decodeString := []byte{ 940 | 174, 69, 206, 0, 0, 0, 0, 0, 150, 73, 206, 0, 0, 0, 0, 0, 2, 1, 0, 136} 941 | 942 | err := kr.Unmarshal(decodeString) 943 | if err != nil { 944 | t.Errorf("Failed to unmarshal: %+v", err) 945 | } 946 | } 947 | --------------------------------------------------------------------------------