diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 343f8db7669105b8d56f446deb66f29838502562..c4dc10de3ec54dfff8c212fcf76c2e6aac6965aa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,15 +1,4 @@ -# From: https://about.gitlab.com/2017/09/21/how-to-create-ci-cd-pipeline-with-autodeploy-to-kubernetes-using-gitlab-and-helm/ - -variables: - REPO_DIR: gitlab.com/elixxir - REPO_NAME: client - DOCKER_IMAGE: elixxirlabs/cuda-go:go1.13-cuda11.1-mc - MIN_CODE_COVERAGE: "35" - before_script: - ## - ## Go Setup - ## - go version || echo "Go executable not found." - echo $CI_BUILD_REF - echo $CI_PROJECT_DIR @@ -18,17 +7,15 @@ before_script: - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null - mkdir -p ~/.ssh - chmod 700 ~/.ssh - - ssh-keyscan -t rsa gitlab.com > ~/.ssh/known_hosts - - git config --global url."git@gitlab.com:".insteadOf "https://gitlab.com/" + - ssh-keyscan -t rsa $GITLAB_SERVER > ~/.ssh/known_hosts + - git config --global url."git@$GITLAB_SERVER:".insteadOf "https://gitlab.com/" + - git config --global url."git@$GITLAB_SERVER:".insteadOf "https://git.xx.network/" - export PATH=$HOME/go/bin:$PATH - - export GOPRIVATE="*gitlab.com/elixxir/*,*gitlab.com/xx_network/*" - stages: - test - build - trigger_integration - - trigger_release_integration test: stage: test @@ -65,10 +52,10 @@ build: - tags script: - mkdir -p release - - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' ./... +# - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' ./... - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o release/client.linux64 main.go - - GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o release/client.win64 main.go - - GOOS=windows GOARCH=386 CGO_ENABLED=0 go build -ldflags '-w -s' -o release/client.win32 main.go +# - GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o release/client.win64 main.go +# - GOOS=windows GOARCH=386 CGO_ENABLED=0 go build -ldflags '-w -s' -o release/client.win32 main.go - GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o release/client.darwin64 main.go - /upload-artifacts.sh release/ artifacts: @@ -81,48 +68,55 @@ tag: - master image: $DOCKER_IMAGE script: - - git remote add origin_tags git@gitlab.com:elixxir/client.git || true + - git remote add origin_tags git@$GITLAB_SERVER:elixxir/client.git || true - git tag $(release/client.linux64 version | grep "Elixxir Client v"| cut -d ' ' -f3) -f - git push origin_tags -f --tags -bindings: +bindings-ios: stage: build except: - tags tags: - ios script: - - export PATH="/usr/local/opt/go@1.13/bin:$PATH" - - go get -u golang.org/x/mobile/cmd/gomobile - - go get -u golang.org/x/mobile/bind - - rm -rf $HOME/go/src/gitlab.com/elixxir/client/ - - mkdir -p $HOME/go/src/gitlab.com/elixxir/client/ - - cp -r * $HOME/go/src/gitlab.com/elixxir/client/ - - GO111MODULE=on gomobile bind -target android -androidapi 21 gitlab.com/elixxir/client/bindings - - GO111MODULE=on gomobile bind -target ios gitlab.com/elixxir/client/bindings - - zip -r iOS.zip Bindings.framework + - go get -u golang.org/x/mobile/cmd/gomobile@76c259c465ba39f84de7e2751a666612ddca556b + - gomobile bind -target ios gitlab.com/elixxir/client/bindings + - ls + - zip -r iOS.zip Bindings.xcframework artifacts: paths: - iOS.zip + +bindings-android: + stage: build + except: + - tags + tags: + - android + script: + - export ANDROID_HOME=/android-sdk + - export PATH=$PATH:/android-sdk/platform-tools/:/usr/local/go/bin + - go get -u golang.org/x/mobile/cmd/gomobile + - gomobile bind -target android -androidapi 21 gitlab.com/elixxir/client/bindings + artifacts: + paths: - bindings.aar - bindings-sources.jar -trigger_integration: +trigger-integration: stage: trigger_integration - script: - # UDB - - "curl -X POST -F token=dcf1a672991bbc2520e96cea271b5a -F ref=master https://gitlab.com/api/v4/projects/6317316/trigger/pipeline" - # integration - - "curl -X POST -F token=e34aa19ef1530e579c5d590873d3c6 -F ref=master https://gitlab.com/api/v4/projects/5615854/trigger/pipeline" + trigger: + project: elixxir/integration + branch: $CI_COMMIT_REF_NAME only: - master + - release -trigger_release_integration: - stage: trigger_release_integration - script: - # UDB - - "curl -X POST -F token=dcf1a672991bbc2520e96cea271b5a -F ref=release https://gitlab.com/api/v4/projects/6317316/trigger/pipeline" - # integration - - "curl -X POST -F token=e34aa19ef1530e579c5d590873d3c6 -F ref=release -F \"variables[CLIENT_ID]=release\" -F \"variables[GATEWAY_ID]=release\" -F \"variables[REGISTRATION_ID]=release\" -F \"variables[SERVER_ID]=release\" -F \"variables[UDB_ID]=release\" https://gitlab.com/api/v4/projects/5615854/trigger/pipeline" +trigger-udb: + stage: trigger_integration + trigger: + project: elixxir/user-discovery-bot + branch: $CI_COMMIT_REF_NAME only: + - master - release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..03ce3f2f34129931075fb1ead5eaeac7b9746c57 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2020, xx network SEZC + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile index bd6383ff69c75244eaf1b62ef3cc9c0fb91c5154..25edc0f05d5333c3c5bc541fd4a3cec93aebf485 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,4 @@ -.PHONY: update master release setup update_master update_release build clean version - -setup: - git config --global --add url."git@gitlab.com:".insteadOf "https://gitlab.com/" +.PHONY: update master release update_master update_release build clean version version: go run main.go generate @@ -13,19 +10,19 @@ clean: go mod vendor update: - -GOFLAGS="" go get -u all + -GOFLAGS="" go get all build: go build ./... go mod tidy update_release: - GOFLAGS="" go get -u gitlab.com/xx_network/primitives@release - GOFLAGS="" go get -u gitlab.com/elixxir/primitives@release - GOFLAGS="" go get -u gitlab.com/xx_network/crypto@release - GOFLAGS="" go get -u gitlab.com/elixxir/crypto@release - GOFLAGS="" go get -u gitlab.com/xx_network/comms@release - GOFLAGS="" go get -u gitlab.com/elixxir/comms@release + GOFLAGS="" go get gitlab.com/xx_network/primitives@release + GOFLAGS="" go get gitlab.com/elixxir/primitives@release + GOFLAGS="" go get gitlab.com/xx_network/crypto@release + GOFLAGS="" go get gitlab.com/elixxir/crypto@release + GOFLAGS="" go get gitlab.com/xx_network/comms@release + GOFLAGS="" go get gitlab.com/elixxir/comms@release update_master: GOFLAGS="" go get gitlab.com/xx_network/primitives@master @@ -35,6 +32,6 @@ update_master: GOFLAGS="" go get gitlab.com/xx_network/comms@master GOFLAGS="" go get gitlab.com/elixxir/comms@master -master: clean update_master build version +master: update_master clean build version -release: clean update_release build version +release: update_release clean build version diff --git a/README.md b/README.md index 0cd7989af96e9851126932c253e8385c3f9ea8d7..ebf490f59a5ab4ac6c8a154785443cff0ed28310 100644 --- a/README.md +++ b/README.md @@ -154,12 +154,17 @@ Available Commands: Flags: --accept-channel Accept the channel request for the corresponding recipient ID + --delete-channel Delete the channel information for the + corresponding recipient ID --destfile string Read this contact file for the destination id -d, --destid string ID to send message to (if below 40, will be precanned. Use '0x' or 'b64:' for hex and base64 representations) (default "0") --forceHistoricalRounds Force all rounds to be sent to historical round retrieval + --forceMessagePickupRetry Enable a mechanism which forces a 50% chance + of no message pickup, instead triggering the + message pickup retry mechanism -h, --help help for client -l, --log string Path to the log output path (- is stdout) (default "-") @@ -176,6 +181,9 @@ Flags: (default 500) --sendid uint Use precanned user id (must be between 1 and 40, inclusive) + --slowPolling bool Enables polling for all network updates and RSA signed rounds. + Defaults to true (filtered updates with ECC signed rounds) if not set + -s, --session string Sets the initial directory for client storage --unsafe Send raw, unsafe messages without e2e encryption. diff --git a/api/authenticatedChannel.go b/api/authenticatedChannel.go index 88682af983c107345b6415648bec9558f2eb4d6b..746572e94e73eb85c16dc60851adb04d901297a1 100644 --- a/api/authenticatedChannel.go +++ b/api/authenticatedChannel.go @@ -19,7 +19,7 @@ import ( // RequestAuthenticatedChannel sends a request to another party to establish an // authenticated channel -// It will not run if the network status is not healthy +// It will not run if the network state is not healthy // An error will be returned if a channel already exists or if a request was // already received // When a confirmation occurs, the channel will be created and the callback @@ -57,7 +57,7 @@ func (c *Client) GetAuthenticatedChannelRequest(partner *id.ID) (contact.Contact // ConfirmAuthenticatedChannel creates an authenticated channel out of a valid // received request and sends a message to the requestor that the request has // been confirmed -// It will not run if the network status is not healthy +// It will not run if the network state is not healthy // An error will be returned if a channel already exists, if a request doest // exist, or if the passed in contact does not exactly match the received // request @@ -125,3 +125,17 @@ func (c *Client) MakePrecannedContact(precannedID uint) contact.Contact { Facts: make([]fact.Fact, 0), } } + +// GetRelationshipFingerprint returns a unique 15 character fingerprint for an +// E2E relationship. An error is returned if no relationship with the partner +// is found. +func (c *Client) GetRelationshipFingerprint(partner *id.ID) (string, error) { + m, err := c.storage.E2e().GetPartner(partner) + if err != nil { + return "", errors.Errorf("could not get partner %s: %+v", partner, err) + } else if m == nil { + return "", errors.Errorf("manager for partner %s is nil.", partner) + } + + return m.GetRelationshipFingerprint(), nil +} diff --git a/api/client.go b/api/client.go index 118615bdcb7d4dc23b6a7fde682a3fdc224b4a6a..b1edca2841c9c349c1443cd6e1f903530265963b 100644 --- a/api/client.go +++ b/api/client.go @@ -8,10 +8,6 @@ package api import ( - "gitlab.com/xx_network/comms/connect" - "gitlab.com/xx_network/primitives/id" - "time" - "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/auth" @@ -20,7 +16,7 @@ import ( "gitlab.com/elixxir/client/interfaces/user" "gitlab.com/elixxir/client/keyExchange" "gitlab.com/elixxir/client/network" - "gitlab.com/elixxir/client/permissioning" + "gitlab.com/elixxir/client/registration" "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/client/switchboard" @@ -28,12 +24,19 @@ import ( "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/elixxir/primitives/version" + "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/crypto/large" "gitlab.com/xx_network/crypto/signature/rsa" + "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/ndf" + "gitlab.com/xx_network/primitives/region" + "math" + "time" ) +const followerStoppableName = "client" + type Client struct { //generic RNG for client rng *fastRNG.StreamGenerator @@ -52,18 +55,17 @@ type Client struct { // loop network interfaces.NetworkManager //object used to register and communicate with permissioning - permissioning *permissioning.Permissioning + permissioning *registration.Registration //object containing auth interactions auth *auth.Manager - //contains stopables for all running threads - runner *stoppable.Multi - status *statusTracker - - //handler for external services - services *serviceProcessiesList + //services system to track running threads + followerServices *services clientErrorChannel chan interfaces.ClientError + + // Event reporting in event.go + events *eventManager } // NewClient creates client storage, generates keys, connects, and registers @@ -71,19 +73,20 @@ type Client struct { // merely creates a new cryptographic identity for adding such information // at a later date. func NewClient(ndfJSON, storageDir string, password []byte, registrationCode string) error { - jww.INFO.Printf("NewClient()") + jww.INFO.Printf("NewClient(dir: %s)", storageDir) // Use fastRNG for RNG ops (AES fortuna based RNG using system RNG) rngStreamGen := fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG) - rngStream := rngStreamGen.GetStream() // Parse the NDF def, err := parseNDF(ndfJSON) if err != nil { return err } - cmixGrp, e2eGrp := decodeGroups(def) - protoUser := createNewUser(rngStream, cmixGrp, e2eGrp) + cmixGrp, e2eGrp := decodeGroups(def) + start := time.Now() + protoUser := createNewUser(rngStreamGen, cmixGrp, e2eGrp) + jww.DEBUG.Printf("User generation took: %s", time.Now().Sub(start)) err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, cmixGrp, e2eGrp, rngStreamGen, false, registrationCode) @@ -176,14 +179,15 @@ func OpenClient(storageDir string, password []byte, parameters params.Network) ( // Set up a new context c := &Client{ - storage: storageSess, - switchboard: switchboard.New(), - rng: rngStreamGen, - comms: nil, - network: nil, - runner: stoppable.NewMulti("client"), - status: newStatusTracker(), - parameters: parameters, + storage: storageSess, + switchboard: switchboard.New(), + rng: rngStreamGen, + comms: nil, + network: nil, + followerServices: newServices(), + parameters: parameters, + clientErrorChannel: make(chan interfaces.ClientError, 1000), + events: newEventManager(), } return c, nil @@ -203,19 +207,16 @@ func Login(storageDir string, password []byte, parameters params.Network) (*Clie jww.INFO.Printf("Client Logged in: \n\tTransmisstionID: %s "+ "\n\tReceptionID: %s", u.TransmissionID, u.ReceptionID) - //Attach the services interface - c.services = newServiceProcessiesList(c.runner) - // initialize comms err = c.initComms() if err != nil { return nil, err } - //get the NDF to pass into permissioning and the network manager - def := c.storage.GetBaseNDF() + //get the NDF to pass into registration and the network manager + def := c.storage.GetNDF() - //initialize permissioning + //initialize registration if def.Registration.Address != "" { err = c.initPermissioning(def) if err != nil { @@ -229,6 +230,8 @@ func Login(storageDir string, password []byte, parameters params.Network) (*Clie if def.Notification.Address != "" { hp := connect.GetDefaultHostParams() + // Client will not send KeepAlive packets + hp.KaClientOpts.Time = time.Duration(math.MaxInt64) hp.AuthEnabled = false hp.MaxRetries = 5 _, err = c.comms.AddHost(&id.NotificationBot, def.Notification.Address, []byte(def.Notification.TlsCertificate), hp) @@ -238,8 +241,8 @@ func Login(storageDir string, password []byte, parameters params.Network) (*Clie } // Initialize network and link it to context - c.network, err = network.NewManager(c.storage, c.switchboard, c.rng, c.comms, - parameters, def) + c.network, err = network.NewManager(c.storage, c.switchboard, c.rng, + c.events, c.comms, parameters, def) if err != nil { return nil, err } @@ -247,6 +250,12 @@ func Login(storageDir string, password []byte, parameters params.Network) (*Clie // initialize the auth tracker c.auth = auth.NewManager(c.switchboard, c.storage, c.network) + // Add all processes to the followerServices + err = c.registerFollower() + if err != nil { + return nil, err + } + return c, nil } @@ -270,9 +279,6 @@ func LoginWithNewBaseNDF_UNSAFE(storageDir string, password []byte, return nil, err } - //Attach the services interface - c.services = newServiceProcessiesList(c.runner) - //initialize comms err = c.initComms() if err != nil { @@ -280,9 +286,9 @@ func LoginWithNewBaseNDF_UNSAFE(storageDir string, password []byte, } //store the updated base NDF - c.storage.SetBaseNDF(def) + c.storage.SetNDF(def) - //initialize permissioning + //initialize registration if def.Registration.Address != "" { err = c.initPermissioning(def) if err != nil { @@ -295,8 +301,8 @@ func LoginWithNewBaseNDF_UNSAFE(storageDir string, password []byte, } // Initialize network and link it to context - c.network, err = network.NewManager(c.storage, c.switchboard, c.rng, c.comms, - parameters, def) + c.network, err = network.NewManager(c.storage, c.switchboard, c.rng, + c.events, c.comms, parameters, def) if err != nil { return nil, err } @@ -304,6 +310,11 @@ func LoginWithNewBaseNDF_UNSAFE(storageDir string, password []byte, // initialize the auth tracker c.auth = auth.NewManager(c.switchboard, c.storage, c.network) + err = c.registerFollower() + if err != nil { + return nil, err + } + return c, nil } @@ -327,14 +338,14 @@ func (c *Client) initComms() error { func (c *Client) initPermissioning(def *ndf.NetworkDefinition) error { var err error - //initialize permissioning - c.permissioning, err = permissioning.Init(c.comms, def) + //initialize registration + c.permissioning, err = registration.Init(c.comms, def) if err != nil { return errors.WithMessage(err, "failed to init "+ "permissioning handler") } - //register with permissioning if necessary + //register with registration if necessary if c.storage.GetRegistrationStatus() == storage.KeyGenComplete { jww.INFO.Printf("Client has not registered yet, attempting registration") err = c.registerWithPermissioning() @@ -347,7 +358,58 @@ func (c *Client) initPermissioning(def *ndf.NetworkDefinition) error { return nil } +// registerFollower adds the follower processes to the client's follower service list. +// This should only ever be called once +func (c *Client) registerFollower() error { + //build the error callback + cer := func(source, message, trace string) { + select { + case c.clientErrorChannel <- interfaces.ClientError{ + Source: source, + Message: message, + Trace: trace, + }: + default: + jww.WARN.Printf("Failed to notify about ClientError from %s: %s", source, message) + } + } + + err := c.followerServices.add(c.events.eventService) + if err != nil { + return errors.WithMessage(err, "Couldn't start event reporting") + } + + //register the core follower service + err = c.followerServices.add(func() (stoppable.Stoppable, error) { return c.network.Follow(cer) }) + if err != nil { + return errors.WithMessage(err, "Failed to start following "+ + "the network") + } + + //register the incremental key upgrade service + err = c.followerServices.add(c.auth.StartProcesses) + if err != nil { + return errors.WithMessage(err, "Failed to start following "+ + "the network") + } + + //register the key exchange service + keyXchange := func() (stoppable.Stoppable, error) { + return keyExchange.Start(c.switchboard, c.storage, c.network, c.parameters.Rekey) + } + err = c.followerServices.add(keyXchange) + + return nil +} + // ----- Client Functions ----- + +// GetErrorsChannel returns a channel which passess errors from the +// long running threads controlled by StartNetworkFollower and StopNetworkFollower +func (c *Client) GetErrorsChannel() <-chan interfaces.ClientError { + return c.clientErrorChannel +} + // StartNetworkFollower kicks off the tracking of the network. It starts // long running network client threads and returns an object for checking // state and stopping those threads. @@ -378,73 +440,22 @@ func (c *Client) initPermissioning(def *ndf.NetworkDefinition) error { // Responds to confirmations of successful rekey operations // - Auth Callback (/auth/callback.go) // Handles both auth confirm and requests -func (c *Client) StartNetworkFollower() (<-chan interfaces.ClientError, error) { +func (c *Client) StartNetworkFollower(timeout time.Duration) error { u := c.GetUser() jww.INFO.Printf("StartNetworkFollower() \n\tTransmisstionID: %s "+ "\n\tReceptionID: %s", u.TransmissionID, u.ReceptionID) - c.clientErrorChannel = make(chan interfaces.ClientError, 1000) - - cer := func(source, message, trace string) { - select { - case c.clientErrorChannel <- interfaces.ClientError{ - Source: source, - Message: message, - Trace: trace, - }: - default: - jww.WARN.Printf("Failed to notify about ClientError from %s: %s", source, message) - } - } - - err := c.status.toStarting() - if err != nil { - return nil, errors.WithMessage(err, "Failed to Start the Network Follower") - } - - stopAuth := c.auth.StartProcessies() - c.runner.Add(stopAuth) - - stopFollow, err := c.network.Follow(cer) - if err != nil { - return nil, errors.WithMessage(err, "Failed to start following "+ - "the network") - } - c.runner.Add(stopFollow) - // Key exchange - c.runner.Add(keyExchange.Start(c.switchboard, c.storage, c.network, c.parameters.Rekey)) - - err = c.status.toRunning() - if err != nil { - return nil, errors.WithMessage(err, "Failed to Start the Network Follower") - } - - c.services.run(c.runner) - - return c.clientErrorChannel, nil + return c.followerServices.start(timeout) } // StopNetworkFollower stops the network follower if it is running. -// It returns errors if the Follower is in the wrong status to stop or if it +// It returns errors if the Follower is in the wrong state to stop or if it // fails to stop it. // if the network follower is running and this fails, the client object will // most likely be in an unrecoverable state and need to be trashed. -func (c *Client) StopNetworkFollower(timeout time.Duration) error { - err := c.status.toStopping() - if err != nil { - return errors.WithMessage(err, "Failed to Stop the Network Follower") - } - err = c.runner.Close(timeout) - c.runner = stoppable.NewMulti("client") - err2 := c.status.toStopped() - if err2 != nil { - if err ==nil{ - err = err2 - }else{ - err = errors.WithMessage(err,err2.Error()) - } - } - return err +func (c *Client) StopNetworkFollower() error { + jww.INFO.Printf("StopNetworkFollower()") + return c.followerServices.stop() } // NetworkFollowerStatus Gets the state of the network follower. Returns: @@ -454,7 +465,7 @@ func (c *Client) StopNetworkFollower(timeout time.Duration) error { // Stopping - 3000 func (c *Client) NetworkFollowerStatus() Status { jww.INFO.Printf("NetworkFollowerStatus()") - return c.status.get() + return c.followerServices.status() } // Returns the health tracker for registration and polling @@ -479,8 +490,8 @@ func (c *Client) GetRoundEvents() interfaces.RoundEvents { // AddService adds a service ot be controlled by the client thread control, // these will be started and stopped with the network follower -func (c *Client) AddService(sp ServiceProcess) { - c.services.Add(sp) +func (c *Client) AddService(sp Service) error { + return c.followerServices.add(sp) } // GetUser returns the current user Identity for this client. This @@ -510,7 +521,7 @@ func (c *Client) GetNetworkInterface() interfaces.NetworkManager { return c.network } -// GetNodeRegistrationStatus gets the current status of node registration. It +// GetNodeRegistrationStatus gets the current state of node registration. It // returns the the total number of nodes in the NDF and the number of those // which are currently registers with. An error is returned if the network is // not healthy. @@ -541,6 +552,74 @@ func (c *Client) GetNodeRegistrationStatus() (int, int, error) { return numRegistered, len(nodes), nil } +// DeleteContact is a function which removes a partner from Client's storage +func (c *Client) DeleteContact(partnerId *id.ID) error { + jww.DEBUG.Printf("Deleting contact with ID %s", partnerId) + if err := c.storage.E2e().DeletePartner(partnerId); err != nil { + return err + } + if err := c.storage.Auth().Delete(partnerId); err != nil { + return err + } + c.storage.Conversations().Delete(partnerId) + return nil +} + +// SetProxiedBins updates the host pool filter that filters out gateways that +// are not in one of the specified bins. +func (c *Client) SetProxiedBins(binStrings []string) error { + // Convert each region string into a region.GeoBin and place in a map for + // easy lookup + bins := make(map[region.GeoBin]bool, len(binStrings)) + for i, binStr := range binStrings { + bin, err := region.GetRegion(binStr) + if err != nil { + return errors.Errorf("failed to parse geographic bin #%d: %+v", i, err) + } + + bins[bin] = true + } + + // Create filter func + f := func(m map[id.ID]int, netDef *ndf.NetworkDefinition) map[id.ID]int { + prunedList := make(map[id.ID]int, len(m)) + for gwID, i := range m { + if bins[netDef.Gateways[i].Bin] { + prunedList[gwID] = i + } + } + return prunedList + } + + c.network.SetPoolFilter(f) + + return nil +} + +// GetPreferredBins returns the geographic bin or bins that the provided two +// character country code is a part of. +func (c *Client) GetPreferredBins(countryCode string) ([]string, error) { + // Get the bin that the country is in + bin, exists := region.GetCountryBin(countryCode) + if !exists { + return nil, errors.Errorf("failed to find geographic bin for country %q", + countryCode) + } + + // Add bin to list of geographic bins + bins := []string{bin.String()} + + // Add additional bins in special cases + switch bin { + case region.Africa: + bins = append(bins, region.WesternEurope.String()) + case region.MiddleEast: + bins = append(bins, region.EasternEurope.String()) + } + + return bins, nil +} + // ----- Utility Functions ----- // parseNDF parses the initial ndf string for the client. do not check the // signature, it is deprecated. @@ -593,7 +672,7 @@ func checkVersionAndSetupStorage(def *ndf.NetworkDefinition, storageDir string, } // Save NDF to be used in the future - storageSess.SetBaseNDF(def) + storageSess.SetNDF(def) if !isPrecanned { //store the registration code for later use @@ -601,7 +680,7 @@ func checkVersionAndSetupStorage(def *ndf.NetworkDefinition, storageDir string, //move the registration state to keys generated err = storageSess.ForwardRegistrationStatus(storage.KeyGenComplete) } else { - //move the registration state to indicate registered with permissioning + //move the registration state to indicate registered with registration err = storageSess.ForwardRegistrationStatus(storage.PermissioningComplete) } diff --git a/api/event.go b/api/event.go new file mode 100644 index 0000000000000000000000000000000000000000..86e75b14ef212033ca906858e265c713022ee709 --- /dev/null +++ b/api/event.go @@ -0,0 +1,130 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package api + +import ( + "fmt" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/client/stoppable" + "sync" +) + +// ReportableEvent is used to surface events to client users. +type reportableEvent struct { + Priority int + Category string + EventType string + Details string +} + +// String stringer interace implementation +func (e reportableEvent) String() string { + return fmt.Sprintf("Event(%d, %s, %s, %s)", e.Priority, e.Category, + e.EventType, e.Details) +} + +// Holds state for the event reporting system +type eventManager struct { + eventCh chan reportableEvent + eventCbs sync.Map +} + +func newEventManager() *eventManager { + return &eventManager{ + eventCh: make(chan reportableEvent, 1000), + } +} + +// Report reports an event from the client to api users, providing a +// priority, category, eventType, and details +func (e *eventManager) Report(priority int, category, evtType, details string) { + re := reportableEvent{ + Priority: priority, + Category: category, + EventType: evtType, + Details: details, + } + select { + case e.eventCh <- re: + jww.TRACE.Printf("Event reported: %s", re) + default: + jww.ERROR.Printf("Event Queue full, unable to report: %s", re) + } +} + +// RegisterEventCallback records the given function to receive +// ReportableEvent objects. It returns the internal index +// of the callback so that it can be deleted later. +func (e *eventManager) RegisterEventCallback(name string, + myFunc interfaces.EventCallbackFunction) error { + _, existsAlready := e.eventCbs.LoadOrStore(name, myFunc) + if existsAlready { + return errors.Errorf("Key %s already exists as event callback", + name) + } + return nil +} + +// UnregisterEventCallback deletes the callback identified by the +// index. It returns an error if it fails. +func (e *eventManager) UnregisterEventCallback(name string) { + e.eventCbs.Delete(name) +} + +func (e *eventManager) eventService() (stoppable.Stoppable, error) { + stop := stoppable.NewSingle("EventReporting") + go e.reportEventsHandler(stop) + return stop, nil +} + +// reportEventsHandler reports events to every registered event callback +func (e *eventManager) reportEventsHandler(stop *stoppable.Single) { + jww.DEBUG.Print("reportEventsHandler routine started") + for { + select { + case <-stop.Quit(): + jww.DEBUG.Printf("Stopping reportEventsHandler") + stop.ToStopped() + return + case evt := <-e.eventCh: + jww.DEBUG.Printf("Received event: %s", evt) + // NOTE: We could call each in a routine but decided + // against it. It's the users responsibility not to let + // the event queue explode. The API will report errors + // in the logging any time the event queue gets full. + e.eventCbs.Range(func(name, myFunc interface{}) bool { + f := myFunc.(interfaces.EventCallbackFunction) + f(evt.Priority, evt.Category, evt.EventType, + evt.Details) + return true + }) + } + } +} + +// ReportEvent reports an event from the client to api users, providing a +// priority, category, eventType, and details +func (c *Client) ReportEvent(priority int, category, evtType, details string) { + c.events.Report(priority, category, evtType, details) +} + +// RegisterEventCallback records the given function to receive +// ReportableEvent objects. It returns the internal index +// of the callback so that it can be deleted later. +func (c *Client) RegisterEventCallback(name string, + myFunc interfaces.EventCallbackFunction) error { + return c.events.RegisterEventCallback(name, myFunc) +} + +// UnregisterEventCallback deletes the callback identified by the +// index. It returns an error if it fails. +func (c *Client) UnregisterEventCallback(name string) { + c.events.UnregisterEventCallback(name) +} diff --git a/api/event_test.go b/api/event_test.go new file mode 100644 index 0000000000000000000000000000000000000000..775d2a6e6609009be6082cb29bdd73dba0a880ee --- /dev/null +++ b/api/event_test.go @@ -0,0 +1,83 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package api + +import ( + "testing" + "time" +) + +func TestEventReporting(t *testing.T) { + evts := make([]reportableEvent, 0) // used for convenience... + myCb := func(priority int, cat, ty, det string) { + evt := reportableEvent{ + Priority: priority, + Category: cat, + EventType: ty, + Details: det, + } + t.Logf("EVENT: %s", evt) + evts = append(evts, evt) + } + + evtMgr := newEventManager() + stop, _ := evtMgr.eventService() + // Register a callback + err := evtMgr.RegisterEventCallback("test", myCb) + if err != nil { + t.Errorf("TestEventReporting unexpected error: %+v", err) + } + + // Send a few events + evtMgr.Report(10, "Hi", "TypityType", "I'm an event") + evtMgr.Report(1, "Hi", "TypeII", "Type II errors are the worst") + evtMgr.Report(20, "Hi", "TypityType3", "eventy details") + evtMgr.Report(22, "Hi", "TypityType4", "I'm an event 2") + + time.Sleep(100 * time.Millisecond) + + if len(evts) != 4 { + t.Errorf("TestEventReporting: Got %d events, expected 4", + len(evts)) + } + + // Verify events are received + if evts[0].Priority != 10 { + t.Errorf("TestEventReporting: Expected priority 10, got: %s", + evts[0]) + } + if evts[1].Category != "Hi" { + t.Errorf("TestEventReporting: Expected cat Hi, got: %s", + evts[1]) + } + if evts[2].EventType != "TypityType3" { + t.Errorf("TestEventReporting: Expected TypeityType3, got: %s", + evts[2]) + } + if evts[3].Details != "I'm an event 2" { + t.Errorf("TestEventReporting: Expected event 2, got: %s", + evts[3]) + } + + // Delete callback + evtMgr.UnregisterEventCallback("test") + // Send more events + evtMgr.Report(10, "Hi", "TypityType", "I'm an event") + evtMgr.Report(1, "Hi", "TypeII", "Type II errors are the worst") + evtMgr.Report(20, "Hi", "TypityType3", "eventy details") + evtMgr.Report(22, "Hi", "TypityType4", "I'm an event 2") + + time.Sleep(100 * time.Millisecond) + + // Verify events are not received + if len(evts) != 4 { + t.Errorf("TestEventReporting: Got %d events, expected 4", + len(evts)) + } + stop.Close() +} diff --git a/api/mnemonic.go b/api/mnemonic.go new file mode 100644 index 0000000000000000000000000000000000000000..fd3ce7259be248692591a48bc86f745fd0089e7c --- /dev/null +++ b/api/mnemonic.go @@ -0,0 +1,133 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package api + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/xx_network/crypto/csprng" + xxMnemonic "gitlab.com/xx_network/crypto/mnemonic" + "gitlab.com/xx_network/primitives/utils" + "golang.org/x/crypto/chacha20poly1305" + "path/filepath" + "strings" +) + +const mnemonicFile = ".recovery" + +// StoreSecretWithMnemonic creates a mnemonic and uses it to encrypt the secret. +// This encrypted data saved in storage. +func StoreSecretWithMnemonic(secret []byte, path string) (string, error) { + // Use fastRNG for RNG ops (AES fortuna based RNG using system RNG) + rng := fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG).GetStream() + + // Ensure path is appended by filepath separator "/" + if !strings.HasSuffix(path, string(filepath.Separator)) { + path = path + string(filepath.Separator) + } + + // Create a mnemonic + mnemonic, err := xxMnemonic.GenerateMnemonic(rng, 32) + if err != nil { + return "", errors.Errorf("Failed to generate mnemonic: %v", err) + } + + // Decode mnemonic + decodedMnemonic, err := xxMnemonic.DecodeMnemonic(mnemonic) + if err != nil { + return "", errors.Errorf("Failed to decode mnemonic: %v", err) + } + + // Encrypt secret with mnemonic as key + ciphertext, err := encryptWithMnemonic(secret, decodedMnemonic, rng) + if err != nil { + return "", errors.Errorf("Failed to encrypt secret with mnemonic: %v", err) + } + + // Save encrypted secret to file + recoveryFile := path + mnemonicFile + err = utils.WriteFileDef(recoveryFile, ciphertext) + if err != nil { + return "", errors.Errorf("Failed to save mnemonic information to file") + } + + return mnemonic, nil +} + +// LoadSecretWithMnemonic loads the encrypted secret from storage and decrypts +// the secret using the given mnemonic. +func LoadSecretWithMnemonic(mnemonic, path string) (secret []byte, err error) { + // Ensure path is appended by filepath separator "/" + if !strings.HasSuffix(path, string(filepath.Separator)) { + path = path + string(filepath.Separator) + } + + // Ensure that the recovery file exists + recoveryFile := path + mnemonicFile + if !utils.Exists(recoveryFile) { + return nil, errors.Errorf("Recovery file does not exist. " + + "Did you properly set up recovery or provide an incorrect filepath?") + } + + // Read file from storage + data, err := utils.ReadFile(recoveryFile) + if err != nil { + return nil, errors.Errorf("Failed to load mnemonic information: %v", err) + } + + // Decode mnemonic + decodedMnemonic, err := xxMnemonic.DecodeMnemonic(mnemonic) + if err != nil { + return nil, errors.Errorf("Failed to decode mnemonic: %v", err) + } + + // Decrypt the stored secret + secret, err = decryptWithMnemonic(data, decodedMnemonic) + if err != nil { + return nil, errors.Errorf("Failed to decrypt secret: %v", err) + } + + return secret, nil +} + +// encryptWithMnemonic is a helper function which encrypts the given secret +// using the mnemonic as the key. +func encryptWithMnemonic(data, decodedMnemonic []byte, + rng csprng.Source) (ciphertext []byte, error error) { + chaCipher, err := chacha20poly1305.NewX(decodedMnemonic[:]) + if err != nil { + return nil, errors.Errorf("Failed to initalize encryption algorithm: %v", err) + } + + // Generate the nonce + nonce := make([]byte, chaCipher.NonceSize()) + nonce, err = csprng.Generate(chaCipher.NonceSize(), rng) + if err != nil { + return nil, errors.Errorf("Failed to generate nonce: %v", err) + } + + ciphertext = chaCipher.Seal(nonce, nonce, data, nil) + return ciphertext, nil +} + +// decryptWithMnemonic is a helper function which decrypts the secret +// from storage, using the mnemonic as the key. +func decryptWithMnemonic(data, decodedMnemonic []byte) ([]byte, error) { + chaCipher, err := chacha20poly1305.NewX(decodedMnemonic[:]) + if err != nil { + return nil, errors.Errorf("Failed to initalize encryption algorithm: %v", err) + } + + nonceLen := chaCipher.NonceSize() + nonce, ciphertext := data[:nonceLen], data[nonceLen:] + plaintext, err := chaCipher.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, errors.Wrap(err, "Cannot decrypt with password!") + } + return plaintext, nil +} diff --git a/api/mnemonic_test.go b/api/mnemonic_test.go new file mode 100644 index 0000000000000000000000000000000000000000..66b2f13dfc8e3f297be051f1f58206c94d22c399 --- /dev/null +++ b/api/mnemonic_test.go @@ -0,0 +1,106 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package api + +import ( + "bytes" + "gitlab.com/xx_network/crypto/csprng" + xxMnemonic "gitlab.com/xx_network/crypto/mnemonic" + "gitlab.com/xx_network/primitives/utils" + "io" + "math/rand" + "testing" +) + +func TestStoreSecretWithMnemonic(t *testing.T) { + secret := []byte("test123") + storageDir := "ignore.1/" + mnemonic, err := StoreSecretWithMnemonic(secret, storageDir) + if err != nil { + t.Errorf("StoreSecretWithMnemonic error; %v", err) + } + + // Tests the mnemonic returned is valid + _, err = xxMnemonic.DecodeMnemonic(mnemonic) + if err != nil { + t.Errorf("StoreSecretWithMnemonic did not return a decodable mnemonic: %v", err) + } + + // Test that the file was written to + if !utils.Exists(storageDir + mnemonicFile) { + t.Errorf("Mnemonic file does not exist in storage: %v", err) + } + +} + +func TestEncryptDecryptMnemonic(t *testing.T) { + prng := NewPrng(32) + + // Generate a test mnemonic + testMnemonic, err := xxMnemonic.GenerateMnemonic(prng, 32) + if err != nil { + t.Fatalf("GenerateMnemonic error: %v", err) + } + + decodedMnemonic, err := xxMnemonic.DecodeMnemonic(testMnemonic) + if err != nil { + t.Fatalf("DecodeMnemonic error: %v", err) + } + + secret := []byte("test123") + + // Encrypt the secret + ciphertext, err := encryptWithMnemonic(secret, decodedMnemonic, prng) + if err != nil { + t.Fatalf("encryptWithMnemonic error: %v", err) + } + + // Decrypt the secret + received, err := decryptWithMnemonic(ciphertext, decodedMnemonic) + if err != nil { + t.Fatalf("decryptWithMnemonic error: %v", err) + } + + // Test if secret matches decrypted data + if !bytes.Equal(received, secret) { + t.Fatalf("Decrypted data does not match original plaintext."+ + "\n\tExpected: %v\n\tReceived: %v", secret, received) + } +} + +func TestLoadSecretWithMnemonic(t *testing.T) { + secret := []byte("test123") + storageDir := "ignore.1" + mnemonic, err := StoreSecretWithMnemonic(secret, storageDir) + if err != nil { + t.Errorf("StoreSecretWithMnemonic error; %v", err) + } + + received, err := LoadSecretWithMnemonic(mnemonic, storageDir) + if err != nil { + t.Errorf("LoadSecretWithMnemonic error: %v", err) + } + + if !bytes.Equal(received, secret) { + t.Fatalf("Loaded secret does not match original data."+ + "\n\tExpected: %v\n\tReceived: %v", secret, received) + } + + _, err = LoadSecretWithMnemonic(mnemonic, "badDirectory") + if err == nil { + t.Fatalf("LoadSecretWithMnemonic should error when provided a path " + + "where a recovery file does not exist.") + } +} + +// Prng is a PRNG that satisfies the csprng.Source interface. +type Prng struct{ prng io.Reader } + +func NewPrng(seed int64) csprng.Source { return &Prng{rand.New(rand.NewSource(seed))} } +func (s *Prng) Read(b []byte) (int, error) { return s.prng.Read(b) } +func (s *Prng) SetSeed([]byte) error { return nil } diff --git a/api/notifications.go b/api/notifications.go index 99ab1d89c79faa1c955d4c2e8f587f20e2f48d60..2a7d1b7c107128360f89930c8bf0bcb16dfc54de 100644 --- a/api/notifications.go +++ b/api/notifications.go @@ -8,7 +8,6 @@ package api import ( - "fmt" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/comms/mixmessages" @@ -26,7 +25,6 @@ import ( // risk to the user. func (c *Client) RegisterForNotifications(token string) error { jww.INFO.Printf("RegisterForNotifications(%s)", token) - fmt.Println("RegisterforNotifications") // Pull the host from the manage notificationBotHost, ok := c.comms.GetHost(&id.NotificationBot) if !ok { @@ -36,16 +34,16 @@ func (c *Client) RegisterForNotifications(token string) error { if err != nil { return err } - fmt.Println("Sending message") // Send the register message _, err = c.comms.RegisterForNotifications(notificationBotHost, &mixmessages.NotificationRegisterRequest{ Token: token, IntermediaryId: intermediaryReceptionID, - TransmissionRsa: rsa.CreatePublicKeyPem(c.GetUser().TransmissionRSA.GetPublic()), - TransmissionRsaSig: sig, + TransmissionRsa: rsa.CreatePublicKeyPem(c.GetStorage().User().GetCryptographicIdentity().GetTransmissionRSA().GetPublic()), TransmissionSalt: c.GetUser().TransmissionSalt, - IIDTransmissionRsaSig: []byte("temp"), + TransmissionRsaSig: c.GetStorage().User().GetTransmissionRegistrationValidationSignature(), + IIDTransmissionRsaSig: sig, + RegistrationTimestamp: c.GetUser().RegistrationTimestamp.UnixNano(), }) if err != nil { err := errors.Errorf( @@ -96,9 +94,12 @@ func (c *Client) getIidAndSig() ([]byte, []byte, error) { return nil, nil, errors.WithMessage(err, "RegisterForNotifications: Failed to write intermediary ID to hash") } - sig, err := rsa.Sign(c.rng.GetStream(), c.GetUser().TransmissionRSA, hash.CMixHash, h.Sum(nil), nil) + stream := c.rng.GetStream() + c.GetUser() + sig, err := rsa.Sign(stream, c.storage.User().GetCryptographicIdentity().GetTransmissionRSA(), hash.CMixHash, h.Sum(nil), nil) if err != nil { return nil, nil, errors.WithMessage(err, "RegisterForNotifications: Failed to sign intermediary ID") } + stream.Close() return intermediaryReceptionID, sig, nil } diff --git a/api/permissioning.go b/api/permissioning.go index b4691291befcb12e7b28b06948980ecc76143c78..ef94e61daf315472ecde87eddc360335c08b445d 100644 --- a/api/permissioning.go +++ b/api/permissioning.go @@ -26,8 +26,9 @@ func (c *Client) registerWithPermissioning() error { "permissioning") } - //register with permissioning - transmissionRegValidationSignature, receptionRegValidationSignature, err := c.permissioning.Register(transmissionPubKey, receptionPubKey, regCode) + //register with registration + transmissionRegValidationSignature, receptionRegValidationSignature, + registrationTimestamp, err := c.permissioning.Register(transmissionPubKey, receptionPubKey, regCode) if err != nil { return errors.WithMessage(err, "failed to register with "+ "permissioning") @@ -36,8 +37,9 @@ func (c *Client) registerWithPermissioning() error { //store the signature userData.SetTransmissionRegistrationValidationSignature(transmissionRegValidationSignature) userData.SetReceptionRegistrationValidationSignature(receptionRegValidationSignature) + userData.SetRegistrationTimestamp(registrationTimestamp) - //update the registration status + //update the registration state err = c.storage.ForwardRegistrationStatus(storage.PermissioningComplete) if err != nil { return errors.WithMessage(err, "failed to update local state "+ diff --git a/api/processies.go b/api/processies.go deleted file mode 100644 index ad4a714992660faf386ea72e23a6e1de1d90b9f7..0000000000000000000000000000000000000000 --- a/api/processies.go +++ /dev/null @@ -1,48 +0,0 @@ -package api - -import ( - "gitlab.com/elixxir/client/stoppable" - "sync" -) - -// a service process starts itself in a new thread, returning from the -// originator a stopable to control it -type ServiceProcess func() stoppable.Stoppable - -type serviceProcessiesList struct { - serviceProcessies []ServiceProcess - multiStopable *stoppable.Multi - mux sync.Mutex -} - -// newServiceProcessiesList creates a new processies list which will add its -// processies to the passed mux -func newServiceProcessiesList(m *stoppable.Multi) *serviceProcessiesList { - return &serviceProcessiesList{ - serviceProcessies: make([]ServiceProcess, 0), - multiStopable: m, - } -} - -// Add adds the service process to the list and adds it to the multi-stopable -func (spl serviceProcessiesList) Add(sp ServiceProcess) { - spl.mux.Lock() - defer spl.mux.Unlock() - - spl.serviceProcessies = append(spl.serviceProcessies, sp) - // starts the process and adds it to the stopable - // there can be a race condition between the execution of the process and - // the stopable. - spl.multiStopable.Add(sp()) -} - -// Runs all processies, to be used after a stop. Must use a new stopable -func (spl serviceProcessiesList) run(m *stoppable.Multi) { - spl.mux.Lock() - defer spl.mux.Unlock() - - spl.multiStopable = m - for _, sp := range spl.serviceProcessies { - spl.multiStopable.Add(sp()) - } -} diff --git a/api/results.go b/api/results.go index 590634b9646f2b92f3b56e29d422145806807d66..f3987393121402f3565ec392c1930a8b29fcafd7 100644 --- a/api/results.go +++ b/api/results.go @@ -12,7 +12,6 @@ import ( jww "github.com/spf13/jwalterweatherman" pb "gitlab.com/elixxir/comms/mixmessages" - "gitlab.com/elixxir/comms/network" ds "gitlab.com/elixxir/comms/network/dataStructures" "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/comms/connect" @@ -108,7 +107,7 @@ func (c *Client) getRoundResults(roundList []id.Round, timeout time.Duration, roundsResults[rnd] = Failed allRoundsSucceeded = false } else { - // If in progress, add a channel monitoring its status + // If in progress, add a channel monitoring its state roundEvents.AddRoundEventChan(rnd, sendResults, timeout-time.Millisecond, states.COMPLETED, states.FAILED) numResults++ @@ -131,7 +130,7 @@ func (c *Client) getRoundResults(roundList []id.Round, timeout time.Duration, // Find out what happened to old (historical) rounds if any are needed if len(historicalRequest.Rounds) > 0 { - go c.getHistoricalRounds(historicalRequest, networkInstance, sendResults, commsInterface) + go c.getHistoricalRounds(historicalRequest, sendResults, commsInterface) } // Determine the results of all rounds requested @@ -180,7 +179,7 @@ func (c *Client) getRoundResults(roundList []id.Round, timeout time.Duration, // Helper function which asynchronously pings a random gateway until // it gets information on it's requested historical rounds func (c *Client) getHistoricalRounds(msg *pb.HistoricalRounds, - instance *network.Instance, sendResults chan ds.EventReturn, comms historicalRoundsComm) { + sendResults chan ds.EventReturn, comms historicalRoundsComm) { var resp *pb.HistoricalRoundsResponse @@ -189,7 +188,7 @@ func (c *Client) getHistoricalRounds(msg *pb.HistoricalRounds, // Find a gateway to request about the roundRequests result, err := c.GetNetworkInterface().GetSender().SendToAny(func(host *connect.Host) (interface{}, error) { return comms.RequestHistoricalRounds(host, msg) - }) + }, nil) // If an error, retry with (potentially) a different gw host. // If no error from received gateway request, exit loop @@ -206,7 +205,7 @@ func (c *Client) getHistoricalRounds(msg *pb.HistoricalRounds, return } - // Process historical rounds, sending back to the caller thread + // Service historical rounds, sending back to the caller thread for _, ri := range resp.Rounds { sendResults <- ds.EventReturn{ RoundInfo: ri, diff --git a/api/results_test.go b/api/results_test.go index 54433f2e9b8bcfd91e3d83e5745bdc1d3ceecc75..68e21e9aca1a317bada3ea1497c90166b1b4289c 100644 --- a/api/results_test.go +++ b/api/results_test.go @@ -40,7 +40,7 @@ func TestClient_GetRoundResults(t *testing.T) { // Create a new copy of the test client for this test client, err := newTestingClient(t) if err != nil { - t.Errorf("Failed in setup: %v", err) + t.Fatalf("Failed in setup: %v", err) } // Construct the round call back function signature @@ -103,7 +103,7 @@ func TestClient_GetRoundResults_FailedRounds(t *testing.T) { // Create a new copy of the test client for this test client, err := newTestingClient(t) if err != nil { - t.Errorf("Failed in setup: %v", err) + t.Fatalf("Failed in setup: %v", err) } // Construct the round call back function signature @@ -161,7 +161,7 @@ func TestClient_GetRoundResults_HistoricalRounds(t *testing.T) { // Create a new copy of the test client for this test client, err := newTestingClient(t) if err != nil { - t.Errorf("Failed in setup: %v", err) + t.Fatalf("Failed in setup: %v", err) } // Overpopulate the round buffer, ensuring a circle back of the ring buffer @@ -219,7 +219,7 @@ func TestClient_GetRoundResults_Timeout(t *testing.T) { // Create a new copy of the test client for this test client, err := newTestingClient(t) if err != nil { - t.Errorf("Failed in setup: %v", err) + t.Fatalf("Failed in setup: %v", err) } // Construct the round call back function signature diff --git a/api/send.go b/api/send.go index 19d6a54b853f911e47af7d2d9bcafa46e2b7bd4d..28b8435425b8941290581342ee54838a8de27eda 100644 --- a/api/send.go +++ b/api/send.go @@ -16,6 +16,7 @@ import ( "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" + "time" ) //This holds all functions to send messages over the network @@ -24,10 +25,10 @@ import ( // the provided msgType. Returns the list of rounds in which parts of // the message were sent or an error if it fails. func (c *Client) SendE2E(m message.Send, param params.E2E) ([]id.Round, - e2e.MessageID, error) { + e2e.MessageID, time.Time, error) { jww.INFO.Printf("SendE2E(%s, %d. %v)", m.Recipient, m.MessageType, m.Payload) - return c.network.SendE2E(m, param) + return c.network.SendE2E(m, param, nil) } // SendUnsafe sends an unencrypted payload to the provided recipient @@ -52,6 +53,14 @@ func (c *Client) SendCMIX(msg format.Message, recipientID *id.ID, return c.network.SendCMIX(msg, recipientID, param) } +// SendManyCMIX sends many "raw" CMIX message payloads to each of the +// provided recipients. Used for group chat functionality. Returns the +// round ID of the round the payload was sent or an error if it fails. +func (c *Client) SendManyCMIX(messages map[id.ID]format.Message, + params params.CMIX) (id.Round, []ephemeral.Id, error) { + return c.network.SendManyCMIX(messages, params) +} + // NewCMIXMessage Creates a new cMix message with the right properties // for the current cMix network. // FIXME: this is weird and shouldn't be necessary, but it is. diff --git a/api/services.go b/api/services.go new file mode 100644 index 0000000000000000000000000000000000000000..3acfddbfe32bc8cfb08ad3431b4932e9f3438922 --- /dev/null +++ b/api/services.go @@ -0,0 +1,117 @@ +package api + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/client/stoppable" + "sync" + "time" +) + +// a service process starts itself in a new thread, returning from the +// originator a stopable to control it +type Service func() (stoppable.Stoppable, error) + +type services struct { + services []Service + stoppable *stoppable.Multi + state Status + mux sync.Mutex +} + +// newServiceProcessiesList creates a new services list which will add its +// services to the passed mux +func newServices() *services { + return &services{ + services: make([]Service, 0), + stoppable: stoppable.NewMulti("services"), + state: Stopped, + } +} + +// Add adds the service process to the list and adds it to the multi-stopable. +// Start running it if services are running +func (s *services) add(sp Service) error { + s.mux.Lock() + defer s.mux.Unlock() + + //append the process to the list + s.services = append(s.services, sp) + + //if services are running, start the process + if s.state == Running { + stop, err := sp() + if err != nil { + return errors.WithMessage(err, "Failed to start added service") + } + s.stoppable.Add(stop) + } + return nil +} + +// Runs all services. If they are in the process of stopping, +// it will wait for the stop to complete or the timeout to ellapse +// Will error if already running +func (s *services) start(timeout time.Duration) error { + s.mux.Lock() + defer s.mux.Unlock() + + //handle various states + switch s.state { + case Stopped: + break + case Running: + return errors.New("Cannot start services when already Running") + case Stopping: + err := stoppable.WaitForStopped(s.stoppable, timeout) + if err != nil { + return errors.Errorf("Procesies did not all stop within %s, "+ + "unable to start services: %+v", timeout, err) + } + } + + //create a new stopable + s.stoppable = stoppable.NewMulti(followerStoppableName) + + //start all services and register with the stoppable + for _, sp := range s.services { + stop, err := sp() + if err != nil { + return errors.WithMessage(err, "Failed to start added service") + } + s.stoppable.Add(stop) + } + + s.state = Running + + return nil +} + +// Stops all currently running services. Will return an +// error if the state is not "running" +func (s *services) stop() error { + s.mux.Lock() + defer s.mux.Unlock() + + if s.state != Running { + return errors.Errorf("cannot stop services when they "+ + "are not Running, services are: %s", s.state) + } + + s.state = Stopping + + if err := s.stoppable.Close(); err != nil { + return errors.WithMessage(err, "Failed to stop services") + } + + s.state = Stopped + + return nil +} + +// returns the current state of services +func (s *services) status() Status { + s.mux.Lock() + defer s.mux.Unlock() + + return s.state +} diff --git a/api/services_test.go b/api/services_test.go new file mode 100644 index 0000000000000000000000000000000000000000..be65c1a5faeec4964eeac8a8568b60aacda7a2d1 --- /dev/null +++ b/api/services_test.go @@ -0,0 +1,122 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package api + +import ( + "errors" + "gitlab.com/elixxir/client/stoppable" + "reflect" + "testing" + "time" +) + +// Unit test +func TestNewServices(t *testing.T) { + expected := &services{ + services: make([]Service, 0), + stoppable: stoppable.NewMulti("services"), + state: Stopped, + } + + received := newServices() + + if !reflect.DeepEqual(expected, received) { + t.Fatalf("Unexpected value in constructor (newServices): "+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", expected, received) + } +} + +// Unit test +func TestServices_Add(t *testing.T) { + mockService := func() (stoppable.Stoppable, error) { + return nil, nil + } + + mockServices := newServices() + + err := mockServices.add(mockService) + if err != nil { + t.Fatalf("Failed to add mock service to services: %v", err) + } + + err = mockServices.start(500 * time.Millisecond) + if err != nil { + t.Fatalf("Failed to start mock services: %v", err) + } + + // Add a doomed to fail process + mockServiceErr := func() (stoppable.Stoppable, error) { + return nil, errors.New("Expected failure case") + } + + err = mockServices.add(mockServiceErr) + if err == nil { + t.Fatalf("Expected error case: " + + "Service should have started and failed") + } +} + +func TestServices_Start(t *testing.T) { + mockService := func() (stoppable.Stoppable, error) { + return nil, nil + } + + mockServices := newServices() + + err := mockServices.add(mockService) + if err != nil { + t.Fatalf("Failed to add mock service to services: %v", err) + } + + err = mockServices.start(500) + if err != nil { + t.Fatalf("Failed to start mock services: %v", err) + } + + // Try and start again should error + err = mockServices.start(500 * time.Millisecond) + if err == nil { + t.Fatalf("Should fail when calling start with running processes") + } +} + +func TestServices_Stop(t *testing.T) { + mockService := func() (stoppable.Stoppable, error) { + return stoppable.NewSingle("test"), nil + } + + mockServices := newServices() + + err := mockServices.add(mockService) + if err != nil { + t.Fatalf("Failed to add mock service to services: %v", err) + } + + err = mockServices.stop() + if err == nil { + t.Fatalf("Should error when calling " + + "stop on non-running service") + } + + err = mockServices.start(500 * time.Millisecond) + if err != nil { + t.Fatalf("Failed to start mock services: %v", err) + } + + err = mockServices.stop() + if err != nil { + t.Fatalf("Should not error when calling stop; %v", err) + } + + err = mockServices.stop() + if err == nil { + t.Fatalf("Should error when stopping a stopped service") + } + +} diff --git a/api/status.go b/api/status.go index 7fd31ae937955d12cb76e8471182c7a406df5b76..33e567725a16b457e936270a3f43acaf7c671129 100644 --- a/api/status.go +++ b/api/status.go @@ -9,15 +9,12 @@ package api import ( "fmt" - "github.com/pkg/errors" - "sync/atomic" ) type Status int const ( Stopped Status = 0 - Starting Status = 1000 Running Status = 2000 Stopping Status = 3000 ) @@ -26,62 +23,11 @@ func (s Status) String() string { switch s { case Stopped: return "Stopped" - case Starting: - return "Starting" case Running: return "Running" case Stopping: return "Stopping" default: - return fmt.Sprintf("Unknown status %d", s) + return fmt.Sprintf("Unknown state %d", s) } } - -type statusTracker struct { - s *uint32 -} - -func newStatusTracker() *statusTracker { - s := uint32(Stopped) - return &statusTracker{s: &s} -} - -func (s *statusTracker) toStarting() error { - if !atomic.CompareAndSwapUint32(s.s, uint32(Stopped), uint32(Starting)) { - return errors.Errorf("Failed to move to '%s' status, at '%s', "+ - "must be at '%s' for transition", Starting, - Status(atomic.LoadUint32(s.s)), Stopped) - } - return nil -} - -func (s *statusTracker) toRunning() error { - if !atomic.CompareAndSwapUint32(s.s, uint32(Starting), uint32(Running)) { - return errors.Errorf("Failed to move to '%s' status, at '%s', "+ - "must be at '%s' for transition", - Running, Status(atomic.LoadUint32(s.s)), Starting) - } - return nil -} - -func (s *statusTracker) toStopping() error { - if !atomic.CompareAndSwapUint32(s.s, uint32(Running), uint32(Stopping)) { - return errors.Errorf("Failed to move to '%s' status, at '%s',"+ - " must be at '%s' for transition", Stopping, - Status(atomic.LoadUint32(s.s)), Running) - } - return nil -} - -func (s *statusTracker) toStopped() error { - if !atomic.CompareAndSwapUint32(s.s, uint32(Stopping), uint32(Stopped)) { - return errors.Errorf("Failed to move to '%s' status, at '%s',"+ - " must be at '%s' for transition", Stopped, - Status(atomic.LoadUint32(s.s)), Stopping) - } - return nil -} - -func (s *statusTracker) get() Status { - return Status(atomic.LoadUint32(s.s)) -} diff --git a/api/user.go b/api/user.go index 48d7f992dfa32b48a3905752c55459ddc710e701..ef8eb1092ee61e8e766c00d2979cb4c20d108d82 100644 --- a/api/user.go +++ b/api/user.go @@ -12,6 +12,7 @@ import ( jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces/user" "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/crypto/signature/rsa" "gitlab.com/xx_network/crypto/xx" @@ -29,58 +30,100 @@ const ( ) // createNewUser generates an identity for cMix -func createNewUser(rng csprng.Source, cmix, e2e *cyclic.Group) user.User { +func createNewUser(rng *fastRNG.StreamGenerator, cmix, e2e *cyclic.Group) user.User { // CMIX Keygen - // FIXME: Why 256 bits? -- this is spec but not explained, it has - // to do with optimizing operations on one side and still preserves - // decent security -- cite this. - cMixKeyBytes, err := csprng.GenerateInGroup(cmix.GetPBytes(), 256, rng) - if err != nil { - jww.FATAL.Panicf(err.Error()) - } + var transmissionRsaKey, receptionRsaKey *rsa.PrivateKey - // DH Keygen - // FIXME: Why 256 bits? -- this is spec but not explained, it has - // to do with optimizing operations on one side and still preserves - // decent security -- cite this. Why valid for BOTH e2e and cmix? - e2eKeyBytes, err := csprng.GenerateInGroup(e2e.GetPBytes(), 256, rng) - if err != nil { - jww.FATAL.Panicf(err.Error()) - } + var cMixKeyBytes, e2eKeyBytes, transmissionSalt, receptionSalt []byte + + wg := sync.WaitGroup{} + + wg.Add(4) + + go func() { + defer wg.Done() + var err error + // FIXME: Why 256 bits? -- this is spec but not explained, it has + // to do with optimizing operations on one side and still preserves + // decent security -- cite this. + stream := rng.GetStream() + cMixKeyBytes, err = csprng.GenerateInGroup(cmix.GetPBytes(), 256, stream) + stream.Close() + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + }() + + go func() { + defer wg.Done() + var err error + // DH Keygen + // FIXME: Why 256 bits? -- this is spec but not explained, it has + // to do with optimizing operations on one side and still preserves + // decent security -- cite this. Why valid for BOTH e2e and cmix? + stream := rng.GetStream() + e2eKeyBytes, err = csprng.GenerateInGroup(e2e.GetPBytes(), 256, stream) + stream.Close() + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + }() // RSA Keygen (4096 bit defaults) - transmissionRsaKey, err := rsa.GenerateKey(rng, rsa.DefaultRSABitLen) + go func() { + defer wg.Done() + var err error + stream := rng.GetStream() + transmissionRsaKey, err = rsa.GenerateKey(stream, rsa.DefaultRSABitLen) + stream.Close() + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + }() + + go func() { + defer wg.Done() + var err error + stream := rng.GetStream() + receptionRsaKey, err = rsa.GenerateKey(stream, rsa.DefaultRSABitLen) + stream.Close() + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + }() + wg.Wait() + + // Salt, UID, etc gen + stream := rng.GetStream() + transmissionSalt = make([]byte, SaltSize) + + n, err := stream.Read(transmissionSalt) + if err != nil { jww.FATAL.Panicf(err.Error()) } - receptionRsaKey, err := rsa.GenerateKey(rng, rsa.DefaultRSABitLen) - if err != nil { - jww.FATAL.Panicf(err.Error()) + if n != SaltSize { + jww.FATAL.Panicf("transmissionSalt size too small: %d", n) } - // Salt, UID, etc gen - transmissionSalt := make([]byte, SaltSize) - n, err := csprng.NewSystemRNG().Read(transmissionSalt) + receptionSalt = make([]byte, SaltSize) + + n, err = stream.Read(receptionSalt) + if err != nil { jww.FATAL.Panicf(err.Error()) } if n != SaltSize { jww.FATAL.Panicf("transmissionSalt size too small: %d", n) } + + stream.Close() + transmissionID, err := xx.NewID(transmissionRsaKey.GetPublic(), transmissionSalt, id.User) if err != nil { jww.FATAL.Panicf(err.Error()) } - // Salt, UID, etc gen - receptionSalt := make([]byte, SaltSize) - n, err = csprng.NewSystemRNG().Read(receptionSalt) - if err != nil { - jww.FATAL.Panicf(err.Error()) - } - if n != SaltSize { - jww.FATAL.Panicf("receptionSalt size too small: %d", n) - } receptionID, err := xx.NewID(receptionRsaKey.GetPublic(), receptionSalt, id.User) if err != nil { jww.FATAL.Panicf(err.Error()) @@ -140,7 +183,7 @@ func createPrecannedUser(precannedID uint, rng csprng.Source, cmix, e2e *cyclic. } // createNewVanityUser generates an identity for cMix -// The identity's ReceptionID is not random but starts with the supplied prefix +// The identity's ReceptionID is not random but starts with the supplied prefix func createNewVanityUser(rng csprng.Source, cmix, e2e *cyclic.Group, prefix string) user.User { // CMIX Keygen // FIXME: Why 256 bits? -- this is spec but not explained, it has @@ -186,14 +229,14 @@ func createNewVanityUser(rng csprng.Source, cmix, e2e *cyclic.Group, prefix stri } var mu sync.Mutex // just in case more than one go routine tries to access receptionSalt and receptionID - done := make(chan struct{}) - found:= make(chan bool) - wg:= &sync.WaitGroup{} + done := make(chan struct{}) + found := make(chan bool) + wg := &sync.WaitGroup{} cores := runtime.NumCPU() var receptionSalt []byte - var receptionID *id.ID - + var receptionID *id.ID + pref := prefix ignoreCase := false // check if case-insensitivity is enabled @@ -207,13 +250,13 @@ func createNewVanityUser(rng csprng.Source, cmix, e2e *cyclic.Group, prefix stri jww.FATAL.Panicf("Prefix contains non-Base64 characters") } jww.INFO.Printf("Vanity userID generation started. Prefix: %s Ignore-Case: %v NumCPU: %d", pref, ignoreCase, cores) - for w := 0; w < cores; w++{ + for w := 0; w < cores; w++ { wg.Add(1) go func() { rSalt := make([]byte, SaltSize) - for { + for { select { - case <- done: + case <-done: defer wg.Done() return default: @@ -231,7 +274,7 @@ func createNewVanityUser(rng csprng.Source, cmix, e2e *cyclic.Group, prefix stri id := rID.String() if ignoreCase { id = strings.ToLower(id) - } + } if strings.HasPrefix(id, pref) { mu.Lock() receptionID = rID @@ -241,12 +284,12 @@ func createNewVanityUser(rng csprng.Source, cmix, e2e *cyclic.Group, prefix stri defer wg.Done() return } - } + } } }() } // wait for a solution then close the done channel to signal the workers to exit - <- found + <-found close(done) wg.Wait() return user.User{ diff --git a/api/utilsInterfaces_test.go b/api/utilsInterfaces_test.go index 62ab16f5231d0bdc81ab30f8e0a1b890fb0931c7..0997993aebfabf2a0bd4fa4ca2efb9c269112a1f 100644 --- a/api/utilsInterfaces_test.go +++ b/api/utilsInterfaces_test.go @@ -20,6 +20,7 @@ import ( "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" + "time" ) // Mock comm struct which returns no historical round data @@ -82,6 +83,12 @@ type testNetworkManagerGeneric struct { instance *network.Instance sender *gateway.Sender } +type dummyEventMgr struct{} + +func (d *dummyEventMgr) Report(p int, a, b, c string) {} +func (t *testNetworkManagerGeneric) GetEventManager() interfaces.EventManager { + return &dummyEventMgr{} +} /* Below methods built for interface adherence */ func (t *testNetworkManagerGeneric) GetHealthTracker() interfaces.HealthTracker { @@ -93,10 +100,10 @@ func (t *testNetworkManagerGeneric) Follow(report interfaces.ClientErrorReport) func (t *testNetworkManagerGeneric) CheckGarbledMessages() { return } -func (t *testNetworkManagerGeneric) SendE2E(m message.Send, p params.E2E) ( - []id.Round, cE2e.MessageID, error) { +func (t *testNetworkManagerGeneric) SendE2E(message.Send, params.E2E, *stoppable.Single) ( + []id.Round, cE2e.MessageID, time.Time, error) { rounds := []id.Round{id.Round(0), id.Round(1), id.Round(2)} - return rounds, cE2e.MessageID{}, nil + return rounds, cE2e.MessageID{}, time.Time{}, nil } func (t *testNetworkManagerGeneric) SendUnsafe(m message.Send, p params.Unsafe) ([]id.Round, error) { @@ -105,6 +112,9 @@ func (t *testNetworkManagerGeneric) SendUnsafe(m message.Send, p params.Unsafe) func (t *testNetworkManagerGeneric) SendCMIX(message format.Message, rid *id.ID, p params.CMIX) (id.Round, ephemeral.Id, error) { return id.Round(0), ephemeral.Id{}, nil } +func (t *testNetworkManagerGeneric) SendManyCMIX(messages map[id.ID]format.Message, p params.CMIX) (id.Round, []ephemeral.Id, error) { + return 0, []ephemeral.Id{}, nil +} func (t *testNetworkManagerGeneric) GetInstance() *network.Instance { return t.instance } @@ -125,3 +135,12 @@ func (t *testNetworkManagerGeneric) InProgressRegistrations() int { func (t *testNetworkManagerGeneric) GetSender() *gateway.Sender { return t.sender } + +func (t *testNetworkManagerGeneric) GetAddressSize() uint8 { return 0 } + +func (t *testNetworkManagerGeneric) RegisterAddressSizeNotification(string) (chan uint8, error) { + return nil, nil +} + +func (t *testNetworkManagerGeneric) UnregisterAddressSizeNotification(string) {} +func (t *testNetworkManagerGeneric) SetPoolFilter(gateway.Filter) {} diff --git a/api/utils_test.go b/api/utils_test.go index 9b9fccac0225438387907c405efbcc023fa0e5a6..a20f1e0bf64a0da34935d467ac483caccd742dce 100644 --- a/api/utils_test.go +++ b/api/utils_test.go @@ -62,7 +62,7 @@ func newTestingClient(face interface{}) (*Client, error) { thisInstance, err := network.NewInstanceTesting(instanceComms, def, def, nil, nil, face) if err != nil { - return nil, nil + return nil, err } p := gateway.DefaultPoolParams() @@ -87,6 +87,7 @@ func getNDF(face interface{}) *ndf.NetworkDefinition { return &ndf.NetworkDefinition{ Registration: ndf.Registration{ TlsCertificate: string(cert), + EllipticPubKey: "/WRtT+mDZGC3FXQbvuQgfqOonAjJ47IKE0zhaGTQQ70=", }, Nodes: []ndf.Node{ { @@ -151,6 +152,6 @@ func signRoundInfo(ri *pb.RoundInfo) error { ourPrivateKey := &rsa.PrivateKey{PrivateKey: *pk} - return signature.Sign(ri, ourPrivateKey) + return signature.SignRsa(ri, ourPrivateKey) } diff --git a/api/version_vars.go b/api/version_vars.go index 75d1adeef89b0d9ed44699e42d0bb7e6306fbbb6..4c7400d408c23d6a07c021ca80643bc7b362fc22 100644 --- a/api/version_vars.go +++ b/api/version_vars.go @@ -1,17 +1,17 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2021-05-07 09:33:37.4750421 -0700 PDT m=+0.043142701 +// 2021-07-27 14:13:01.428348 -0500 CDT m=+0.036560333 package api -const GITVERSION = `51fdae45 made stop network follower always allow restart` -const SEMVER = "2.5.0" +const GITVERSION = `758d1e91 Merge branch 'protoMainNet' into 'release'` +const SEMVER = "2.8.0" const DEPENDENCIES = `module gitlab.com/elixxir/client go 1.13 require ( github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 - github.com/golang/protobuf v1.4.3 + github.com/golang/protobuf v1.5.2 github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/magiconair/properties v1.8.4 // indirect github.com/mitchellh/mapstructure v1.4.0 // indirect @@ -24,21 +24,18 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/viper v1.7.1 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 - gitlab.com/elixxir/comms v0.0.4-0.20210506225017-37485f5ba063 - gitlab.com/elixxir/crypto v0.0.7-0.20210506223047-3196e4301110 + gitlab.com/elixxir/comms v0.0.4-0.20210714201329-5efcbdfac3ca + gitlab.com/elixxir/crypto v0.0.7-0.20210714201100-45fb778a00fb gitlab.com/elixxir/ekv v0.1.5 - gitlab.com/elixxir/primitives v0.0.3-0.20210504210415-34cf31c2816e - gitlab.com/xx_network/comms v0.0.4-0.20210505205155-48daa8448ad7 - gitlab.com/xx_network/crypto v0.0.5-0.20210504210244-9ddabbad25fd - gitlab.com/xx_network/primitives v0.0.4-0.20210504205835-db68f11de78a + gitlab.com/elixxir/primitives v0.0.3-0.20210714200942-a908050c230c + gitlab.com/xx_network/comms v0.0.4-0.20210714165756-8e3b40d71db1 + gitlab.com/xx_network/crypto v0.0.5-0.20210714165656-1ed326047ba9 + gitlab.com/xx_network/primitives v0.0.4-0.20210727175935-dd746a0d73de golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 - golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect + golang.org/x/net v0.0.0-20210525063256-abc453219eb5 google.golang.org/genproto v0.0.0-20210105202744-fe13368bc0e1 // indirect - google.golang.org/grpc v1.34.0 // indirect - google.golang.org/protobuf v1.26.0-rc.1 + google.golang.org/protobuf v1.26.0 gopkg.in/ini.v1 v1.62.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) - -replace google.golang.org/grpc => github.com/grpc/grpc-go v1.27.1 ` diff --git a/auth/callback.go b/auth/callback.go index 2c39824cb8631b222d6140fd66d2e9f143873639..4a5fba05b7e74335fd71bac6685db01126ae2527 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -8,6 +8,7 @@ package auth import ( + "fmt" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces" @@ -23,21 +24,22 @@ import ( "strings" ) -func (m *Manager) StartProcessies() stoppable.Stoppable { - +func (m *Manager) StartProcesses() (stoppable.Stoppable, error) { stop := stoppable.NewSingle("Auth") go func() { for { select { case <-stop.Quit(): + stop.ToStopped() return case msg := <-m.rawMessages: m.processAuthMessage(msg) } } }() - return stop + + return stop, nil } func (m *Manager) processAuthMessage(msg message.Receive) { @@ -69,7 +71,7 @@ func (m *Manager) processAuthMessage(msg message.Receive) { case auth.Specific: // if it is specific, that means the original request was sent // by this users and a confirmation has been received - jww.INFO.Printf("Received AutConfirm from %s, msgDigest: %s", + jww.INFO.Printf("Received AuthConfirm from %s, msgDigest: %s", sr.GetPartner(), cmixMsg.Digest()) m.handleConfirm(cmixMsg, sr, grp) } @@ -90,12 +92,16 @@ func (m *Manager) handleRequest(cmixMsg format.Message, jww.TRACE.Printf("handleRequest PARTNERPUBKEY: %v", partnerPubKey.Bytes()) //decrypt the message + jww.TRACE.Printf("handleRequest SALT: %v", baseFmt.GetSalt()) + jww.TRACE.Printf("handleRequest ECRPAYLOAD: %v", baseFmt.GetEcrPayload()) + jww.TRACE.Printf("handleRequest MAC: %v", cmixMsg.GetMac()) + success, payload := cAuth.Decrypt(myHistoricalPrivKey, partnerPubKey, baseFmt.GetSalt(), baseFmt.GetEcrPayload(), cmixMsg.GetMac(), grp) if !success { - jww.WARN.Printf("Recieved auth request failed " + + jww.WARN.Printf("Received auth request failed " + "its mac check") return } @@ -123,8 +129,11 @@ func (m *Manager) handleRequest(cmixMsg format.Message, return } - jww.INFO.Printf("Received AuthRequest from %s,"+ + events := m.net.GetEventManager() + em := fmt.Sprintf("Received AuthRequest from %s,"+ " msgDigest: %s", partnerID, cmixMsg.Digest()) + jww.INFO.Print(em) + events.Report(1, "Auth", "RequestReceived", em) /*do state edge checks*/ // check if a relationship already exists. @@ -132,26 +141,32 @@ func (m *Manager) handleRequest(cmixMsg format.Message, // confirmation in case there are state issues. // do not store if _, err := m.storage.E2e().GetPartner(partnerID); err == nil { - jww.WARN.Printf("Recieved Auth request for %s, "+ + em := fmt.Sprintf("Received Auth request for %s, "+ "channel already exists. Ignoring", partnerID) + jww.WARN.Print(em) + events.Report(5, "Auth", "RequestIgnored", em) //exit return } else { //check if the relationship already exists, rType, sr2, _, err := m.storage.Auth().GetRequest(partnerID) if err != nil && !strings.Contains(err.Error(), auth.NoRequest) { - // if another error is recieved, print it and exit - jww.WARN.Printf("Recieved new Auth request for %s, "+ + // if another error is received, print it and exit + em := fmt.Sprintf("Received new Auth request for %s, "+ "internal lookup produced bad result: %+v", partnerID, err) + jww.ERROR.Print(em) + events.Report(10, "Auth", "RequestError", em) return } else { //handle the events where the relationship already exists switch rType { // if this is a duplicate, ignore the message case auth.Receive: - jww.WARN.Printf("Recieved new Auth request for %s, "+ + em := fmt.Sprintf("Received new Auth request for %s, "+ "is a duplicate", partnerID) + jww.WARN.Print(em) + events.Report(5, "Auth", "DuplicateRequest", em) return // if we sent a request, then automatically confirm // then exit, nothing else needed @@ -162,8 +177,11 @@ func (m *Manager) handleRequest(cmixMsg format.Message, // do the confirmation if err := m.doConfirm(sr2, grp, partnerPubKey, m.storage.E2e().GetDHPrivateKey(), sr2.GetPartnerHistoricalPubKey(), ecrFmt.GetOwnership()); err != nil { - jww.WARN.Printf("Auto Confirmation with %s failed: %s", + em := fmt.Sprintf("Auto Confirmation with %s failed: %s", partnerID, err) + jww.WARN.Print(em) + events.Report(10, "Auth", + "RequestError", em) } //exit return @@ -175,8 +193,10 @@ func (m *Manager) handleRequest(cmixMsg format.Message, facts, msg, err := fact.UnstringifyFactList( string(requestFmt.msgPayload)) if err != nil { - jww.WARN.Printf("failed to parse facts and message "+ + em := fmt.Sprintf("failed to parse facts and message "+ "from Auth Request: %s", err) + jww.WARN.Print(em) + events.Report(10, "Auth", "RequestError", em) return } @@ -192,8 +212,10 @@ func (m *Manager) handleRequest(cmixMsg format.Message, // crash occurs after the store but before the conclusion of the callback //create the auth storage if err = m.storage.Auth().AddReceived(c); err != nil { - jww.WARN.Printf("failed to store contact Auth "+ + em := fmt.Sprintf("failed to store contact Auth "+ "Request: %s", err) + jww.WARN.Print(em) + events.Report(10, "Auth", "RequestError", em) return } @@ -209,10 +231,14 @@ func (m *Manager) handleRequest(cmixMsg format.Message, func (m *Manager) handleConfirm(cmixMsg format.Message, sr *auth.SentRequest, grp *cyclic.Group) { + events := m.net.GetEventManager() + // check if relationship already exists if mgr, err := m.storage.E2e().GetPartner(sr.GetPartner()); mgr != nil || err == nil { - jww.WARN.Printf("Cannot confirm auth for %s, channel already "+ + em := fmt.Sprintf("Cannot confirm auth for %s, channel already "+ "exists.", sr.GetPartner()) + jww.WARN.Print(em) + events.Report(10, "Auth", "ConfirmError", em) m.storage.Auth().Done(sr.GetPartner()) return } @@ -220,7 +246,9 @@ func (m *Manager) handleConfirm(cmixMsg format.Message, sr *auth.SentRequest, // extract the message baseFmt, partnerPubKey, err := handleBaseFormat(cmixMsg, grp) if err != nil { - jww.WARN.Printf("Failed to handle auth confirm: %s", err) + em := fmt.Sprintf("Failed to handle auth confirm: %s", err) + jww.WARN.Print(em) + events.Report(10, "Auth", "ConfirmError", em) m.storage.Auth().Done(sr.GetPartner()) return } @@ -229,21 +257,28 @@ func (m *Manager) handleConfirm(cmixMsg format.Message, sr *auth.SentRequest, jww.TRACE.Printf("handleConfirm SRMYPUBKEY: %v", sr.GetMyPubKey().Bytes()) // decrypt the payload + jww.TRACE.Printf("handleConfirm SALT: %v", baseFmt.GetSalt()) + jww.TRACE.Printf("handleConfirm ECRPAYLOAD: %v", baseFmt.GetEcrPayload()) + jww.TRACE.Printf("handleConfirm MAC: %v", cmixMsg.GetMac()) success, payload := cAuth.Decrypt(sr.GetMyPrivKey(), partnerPubKey, baseFmt.GetSalt(), baseFmt.GetEcrPayload(), cmixMsg.GetMac(), grp) if !success { - jww.WARN.Printf("Recieved auth confirmation failed its mac " + + em := fmt.Sprintf("Received auth confirmation failed its mac " + "check") + jww.WARN.Print(em) + events.Report(10, "Auth", "ConfirmError", em) m.storage.Auth().Done(sr.GetPartner()) return } ecrFmt, err := unmarshalEcrFormat(payload) if err != nil { - jww.WARN.Printf("Failed to unmarshal auth confirmation's "+ + em := fmt.Sprintf("Failed to unmarshal auth confirmation's "+ "encrypted payload: %s", err) + jww.WARN.Print(em) + events.Report(10, "Auth", "ConfirmError", em) m.storage.Auth().Done(sr.GetPartner()) return } @@ -251,7 +286,9 @@ func (m *Manager) handleConfirm(cmixMsg format.Message, sr *auth.SentRequest, // finalize the confirmation if err := m.doConfirm(sr, grp, partnerPubKey, sr.GetMyPrivKey(), sr.GetPartnerHistoricalPubKey(), ecrFmt.GetOwnership()); err != nil { - jww.WARN.Printf("Confirmation failed: %s", err) + em := fmt.Sprintf("Confirmation failed: %s", err) + jww.WARN.Print(em) + events.Report(10, "Auth", "ConfirmError", em) m.storage.Auth().Done(sr.GetPartner()) return } diff --git a/auth/confirm.go b/auth/confirm.go index 30a51181c97f3e8bea4993eac82555152655dc7f..f13d3871d9669b52c66924cfb7e4f17bc89b080c 100644 --- a/auth/confirm.go +++ b/auth/confirm.go @@ -8,6 +8,7 @@ package auth import ( + "fmt" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces" @@ -100,13 +101,17 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader, // the second does not or the two occur and the storage into critical // messages does not occur + events := net.GetEventManager() + //create local relationship p := storage.E2e().GetE2ESessionParams() if err := storage.E2e().AddPartner(partner.ID, partner.DhPubKey, newPrivKey, p, p); err != nil { - jww.WARN.Printf("Failed to create channel with partner (%s) "+ + em := fmt.Sprintf("Failed to create channel with partner (%s) "+ "on confirmation, this is likley a replay: %s", partner.ID, err.Error()) + jww.WARN.Print(em) + events.Report(10, "Auth", "SendConfirmError", em) } // delete the in progress negotiation @@ -131,8 +136,10 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader, return 0, errors.WithMessage(err, "Auth Confirm Failed to transmit") } - jww.INFO.Printf("Confirm Request with %s (msgDigest: %s) sent on round %d", + em := fmt.Sprintf("Confirm Request with %s (msgDigest: %s) sent on round %d", partner.ID, cmixMsg.Digest(), round) + jww.INFO.Print(em) + events.Report(1, "Auth", "SendConfirm", em) return round, nil } diff --git a/auth/fmt_test.go b/auth/fmt_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9e87d2f587da56e586e16578f94f915e6178c61b --- /dev/null +++ b/auth/fmt_test.go @@ -0,0 +1,472 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package auth + +import ( + "bytes" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "reflect" + "testing" +) + +// Tests newBaseFormat +func TestNewBaseFormat(t *testing.T) { + // Construct message + pubKeySize := 256 + payloadSize := saltSize + pubKeySize + baseMsg := newBaseFormat(payloadSize, pubKeySize) + + // Check that the base format was constructed properly + if !bytes.Equal(baseMsg.pubkey, make([]byte, pubKeySize)) { + t.Errorf("NewBaseFormat error: "+ + "Unexpected pubkey field in base format."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", make([]byte, pubKeySize), baseMsg.pubkey) + } + + if !bytes.Equal(baseMsg.salt, make([]byte, saltSize)) { + t.Errorf("NewBaseFormat error: "+ + "Unexpected salt field in base format."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", make([]byte, saltSize), baseMsg.salt) + } + + expectedEcrPayloadSize := payloadSize - (pubKeySize + saltSize) + if !bytes.Equal(baseMsg.ecrPayload, make([]byte, expectedEcrPayloadSize)) { + t.Errorf("NewBaseFormat error: "+ + "Unexpected payload field in base format."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", make([]byte, expectedEcrPayloadSize), baseMsg.ecrPayload) + } + + // Error case, where payload size is less than the public key plus salt + defer func() { + if r := recover(); r == nil { + t.Error("newBaseFormat() did not panic when the size of " + + "the payload is smaller than the size of the public key.") + } + }() + + newBaseFormat(0, pubKeySize) +} + +/* Tests the setter/getter methods for baseFormat */ + +// Set/Get PubKey tests +func TestBaseFormat_SetGetPubKey(t *testing.T) { + // Construct message + pubKeySize := 256 + payloadSize := saltSize + pubKeySize + baseMsg := newBaseFormat(payloadSize, pubKeySize) + + // Test setter + grp := getGroup() + pubKey := grp.NewInt(25) + baseMsg.SetPubKey(pubKey) + expectedBytes := pubKey.LeftpadBytes(uint64(len(baseMsg.pubkey))) + if !bytes.Equal(baseMsg.pubkey, expectedBytes) { + t.Errorf("SetPubKey() error: "+ + "Public key field does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", expectedBytes, baseMsg.pubkey) + } + + // Test getter + receivedKey := baseMsg.GetPubKey(grp) + if !bytes.Equal(pubKey.Bytes(), receivedKey.Bytes()) { + t.Errorf("GetPubKey() error: "+ + "Public key retrieved does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", pubKey, receivedKey) + } + +} + +// Set/Get salt tests +func TestBaseFormat_SetGetSalt(t *testing.T) { + // Construct message + pubKeySize := 256 + payloadSize := saltSize + pubKeySize + baseMsg := newBaseFormat(payloadSize, pubKeySize) + + // Test setter + salt := newSalt("salt") + baseMsg.SetSalt(salt) + if !bytes.Equal(salt, baseMsg.salt) { + t.Errorf("SetSalt() error: "+ + "Salt field does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", salt, baseMsg.salt) + } + + // Test getter + receivedSalt := baseMsg.GetSalt() + if !bytes.Equal(salt, receivedSalt) { + t.Errorf("GetSalt() error: "+ + "Salt retrieved does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", salt, receivedSalt) + } + + // Test setter error path: Setting salt of incorrect size + defer func() { + if r := recover(); r == nil { + t.Error("SetSalt() did not panic when the size of " + + "the salt is smaller than the required salt size.") + } + }() + + baseMsg.SetSalt([]byte("salt")) +} + +// Set/Get EcrPayload tests +func TestBaseFormat_SetGetEcrPayload(t *testing.T) { + // Construct message + pubKeySize := 256 + payloadSize := (saltSize + pubKeySize) * 2 + baseMsg := newBaseFormat(payloadSize, pubKeySize) + + // Test setter + ecrPayloadSize := payloadSize - (pubKeySize + saltSize) + ecrPayload := newPayload(ecrPayloadSize, "ecrPayload") + baseMsg.SetEcrPayload(ecrPayload) + if !bytes.Equal(ecrPayload, baseMsg.ecrPayload) { + t.Errorf("SetEcrPayload() error: "+ + "EcrPayload field does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", ecrPayload, baseMsg.ecrPayload) + + } + + // Test Getter + receivedEcrPayload := baseMsg.GetEcrPayload() + if !bytes.Equal(receivedEcrPayload, ecrPayload) { + t.Errorf("GetEcrPayload() error: "+ + "EcrPayload retrieved does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", ecrPayload, receivedEcrPayload) + } + + // Setter error path: Setting ecrPayload that + // does not completely fill field + defer func() { + if r := recover(); r == nil { + t.Error("SetEcrPayload() did not panic when the size of " + + "the ecrPayload is smaller than the pre-constructed field.") + } + }() + baseMsg.SetEcrPayload([]byte("ecrPayload")) +} + +// Marshal/ unmarshal tests +func TestBaseFormat_MarshalUnmarshal(t *testing.T) { + // Construct a fully populated message + pubKeySize := 256 + payloadSize := (saltSize + pubKeySize) * 2 + baseMsg := newBaseFormat(payloadSize, pubKeySize) + ecrPayloadSize := payloadSize - (pubKeySize + saltSize) + ecrPayload := newPayload(ecrPayloadSize, "ecrPayload") + baseMsg.SetEcrPayload(ecrPayload) + salt := newSalt("salt") + baseMsg.SetSalt(salt) + grp := getGroup() + pubKey := grp.NewInt(25) + baseMsg.SetPubKey(pubKey) + + // Test marshal + data := baseMsg.Marshal() + if !bytes.Equal(data, baseMsg.data) { + t.Errorf("baseFormat.Marshal() error: "+ + "Marshalled data is not expected."+ + "\n\tExpected: %v\n\tReceived: %v", baseMsg.data, data) + } + + // Test unmarshal + newMsg, err := unmarshalBaseFormat(data, pubKeySize) + if err != nil { + t.Errorf("unmarshalBaseFormat() error: "+ + "Could not unmarshal into baseFormat: %v", err) + } + + if !reflect.DeepEqual(newMsg, baseMsg) { + t.Errorf("unmarshalBaseFormat() error: "+ + "Unmarshalled message does not match originally marshalled message."+ + "\n\tExpected: %v\n\tRecieved: %v", baseMsg, newMsg) + } + + // Unmarshal error test: Invalid size parameter + _, err = unmarshalBaseFormat(make([]byte, 0), pubKeySize) + if err == nil { + t.Errorf("unmarshalBaseFormat() error: " + + "Should not be able to unmarshal when baseFormat is too small") + } + +} + +// Tests newEcrFormat +func TestNewEcrFormat(t *testing.T) { + // Construct message + payloadSize := ownershipSize * 2 + ecrMsg := newEcrFormat(payloadSize) + + // Check that the ecrFormat was constructed properly + if !bytes.Equal(ecrMsg.ownership, make([]byte, ownershipSize)) { + t.Errorf("newEcrFormat error: "+ + "Unexpected ownership field in ecrFormat."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", make([]byte, payloadSize), ecrMsg.ownership) + } + + if !bytes.Equal(ecrMsg.payload, make([]byte, payloadSize-ownershipSize)) { + t.Errorf("newEcrFormat error: "+ + "Unexpected ownership field in ecrFormat."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", make([]byte, payloadSize-ownershipSize), ecrMsg.payload) + } + + // Error case, where payload size is less than the public key plus salt + defer func() { + if r := recover(); r == nil { + t.Error("newEcrFormat() did not panic when the size of " + + "the payload is smaller than the size of the ownership") + } + }() + + newEcrFormat(0) +} + +/* Tests the setter/getter methods for ecrFormat */ + +// Set/Get ownership tests +func TestEcrFormat_SetGetOwnership(t *testing.T) { + // Construct message + payloadSize := ownershipSize * 2 + ecrMsg := newEcrFormat(payloadSize) + + // Test setter + ownership := newOwnership("owner") + ecrMsg.SetOwnership(ownership) + if !bytes.Equal(ownership, ecrMsg.ownership) { + t.Errorf("SetOwnership() error: "+ + "Ownership field does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", ownership, ecrMsg.ownership) + + } + + // Test getter + receivedOwnership := ecrMsg.GetOwnership() + if !bytes.Equal(receivedOwnership, ecrMsg.ownership) { + t.Errorf("GetOwnership() error: "+ + "Ownership retrieved does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", ownership, receivedOwnership) + + } + + // Test setter error path: Setting ownership of incorrect size + defer func() { + if r := recover(); r == nil { + t.Error("SetOwnership() did not panic when the size of " + + "the ownership is smaller than the required ownership size.") + } + }() + + ecrMsg.SetOwnership([]byte("ownership")) +} + +// Set/Get payload tests +func TestEcrFormat_SetGetPayload(t *testing.T) { + // Construct message + payloadSize := ownershipSize * 2 + ecrMsg := newEcrFormat(payloadSize) + + // Test set + expectedPayload := newPayload(payloadSize-ownershipSize, "ownership") + ecrMsg.SetPayload(expectedPayload) + + if !bytes.Equal(expectedPayload, ecrMsg.payload) { + t.Errorf("SetPayload() error: "+ + "Payload field does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", expectedPayload, ecrMsg.payload) + } + + // Test get + receivedPayload := ecrMsg.GetPayload() + if !bytes.Equal(receivedPayload, expectedPayload) { + t.Errorf("GetPayload() error: "+ + "Payload retrieved does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", expectedPayload, receivedPayload) + + } + + // Test setter error path: Setting payload of incorrect size + defer func() { + if r := recover(); r == nil { + t.Error("SetPayload() did not panic when the size of " + + "the payload is smaller than the required payload size.") + } + }() + + ecrMsg.SetPayload([]byte("payload")) +} + +// Marshal/ unmarshal tests +func TestEcrFormat_MarshalUnmarshal(t *testing.T) { + // Construct message + payloadSize := ownershipSize * 2 + ecrMsg := newEcrFormat(payloadSize) + expectedPayload := newPayload(payloadSize-ownershipSize, "ownership") + ecrMsg.SetPayload(expectedPayload) + ownership := newOwnership("owner") + ecrMsg.SetOwnership(ownership) + + // Test marshal + data := ecrMsg.Marshal() + if !bytes.Equal(data, ecrMsg.data) { + t.Errorf("ecrFormat.Marshal() error: "+ + "Marshalled data is not expected."+ + "\n\tExpected: %v\n\tReceived: %v", ecrMsg.data, data) + } + + // Test unmarshal + newMsg, err := unmarshalEcrFormat(data) + if err != nil { + t.Errorf("unmarshalEcrFormat() error: "+ + "Could not unmarshal into ecrFormat: %v", err) + } + + if !reflect.DeepEqual(newMsg, ecrMsg) { + t.Errorf("unmarshalBaseFormat() error: "+ + "Unmarshalled message does not match originally marshalled message."+ + "\n\tExpected: %v\n\tRecieved: %v", ecrMsg, newMsg) + } + + // Unmarshal error test: Invalid size parameter + _, err = unmarshalEcrFormat(make([]byte, 0)) + if err == nil { + t.Errorf("unmarshalEcrFormat() error: " + + "Should not be able to unmarshal when ecrFormat is too small") + } + +} + +// Tests newRequestFormat +func TestNewRequestFormat(t *testing.T) { + // Construct message + payloadSize := id.ArrIDLen*2 - 1 + ecrMsg := newEcrFormat(payloadSize) + expectedPayload := newPayload(id.ArrIDLen, "ownership") + ecrMsg.SetPayload(expectedPayload) + reqMsg, err := newRequestFormat(ecrMsg) + if err != nil { + t.Fatalf("newRequestFormat() error: "+ + "Failed to construct message: %v", err) + } + + // Check that the requestFormat was constructed properly + if !bytes.Equal(reqMsg.id, expectedPayload) { + t.Errorf("newRequestFormat() error: "+ + "Unexpected id field in requestFormat."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", make([]byte, id.ArrIDLen), reqMsg.id) + } + + if !bytes.Equal(reqMsg.msgPayload, make([]byte, 0)) { + t.Errorf("newRequestFormat() error: "+ + "Unexpected msgPayload field in requestFormat."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", make([]byte, 0), reqMsg.msgPayload) + } + + payloadSize = ownershipSize * 2 + ecrMsg = newEcrFormat(payloadSize) + reqMsg, err = newRequestFormat(ecrMsg) + if err == nil { + t.Errorf("Expecter error: Should be invalid size when calling newRequestFormat") + } + +} + +/* Setter/Getter tests for RequestFormat */ + +// Unit test for Get/SetID +func TestRequestFormat_SetGetID(t *testing.T) { + // Construct message + payloadSize := id.ArrIDLen*2 - 1 + ecrMsg := newEcrFormat(payloadSize) + expectedPayload := newPayload(id.ArrIDLen, "ownership") + ecrMsg.SetPayload(expectedPayload) + reqMsg, err := newRequestFormat(ecrMsg) + if err != nil { + t.Fatalf("newRequestFormat() error: "+ + "Failed to construct message: %v", err) + } + + // Test SetID + prng := rand.New(rand.NewSource(42)) + expectedId := randID(prng, id.User) + reqMsg.SetID(expectedId) + if !bytes.Equal(reqMsg.id, expectedId.Bytes()) { + t.Errorf("SetID() error: "+ + "Id field does not have expected value."+ + "\n\tExpected: %v\n\tReceived: %v", expectedId, reqMsg.msgPayload) + } + + // Test GetID + receivedId, err := reqMsg.GetID() + if err != nil { + t.Fatalf("GetID() error: "+ + "Retrieved id does not match expected value:"+ + "\n\tExpected: %v\n\tReceived: %v", expectedId, receivedId) + } + + // Test GetID error: unmarshal-able ID in requestFormat + reqMsg.id = []byte("badId") + receivedId, err = reqMsg.GetID() + if err == nil { + t.Errorf("GetID() error: " + + "Should not be able get ID from request message ") + } + +} + +// Unit test for Get/SetMsgPayload +func TestRequestFormat_SetGetMsgPayload(t *testing.T) { + // Construct message + payloadSize := id.ArrIDLen*3 - 1 + ecrMsg := newEcrFormat(payloadSize) + expectedPayload := newPayload(id.ArrIDLen*2, "ownership") + ecrMsg.SetPayload(expectedPayload) + reqMsg, err := newRequestFormat(ecrMsg) + if err != nil { + t.Fatalf("newRequestFormat() error: "+ + "Failed to construct message: %v", err) + } + + // Test SetMsgPayload + msgPayload := newPayload(id.ArrIDLen, "msgPayload") + reqMsg.SetMsgPayload(msgPayload) + if !bytes.Equal(reqMsg.msgPayload, msgPayload) { + t.Errorf("SetMsgPayload() error: "+ + "MsgPayload has unexpected value: "+ + "\n\tExpected: %v\n\tReceived: %v", msgPayload, reqMsg.msgPayload) + } + + // Test GetMsgPayload + retrievedMsgPayload := reqMsg.GetMsgPayload() + if !bytes.Equal(retrievedMsgPayload, msgPayload) { + t.Errorf("GetMsgPayload() error: "+ + "MsgPayload has unexpected value: "+ + "\n\tExpected: %v\n\tReceived: %v", msgPayload, retrievedMsgPayload) + + } + + // Test SetMsgPayload error: Invalid message payload size + defer func() { + if r := recover(); r == nil { + t.Error("SetMsgPayload() did not panic when the size of " + + "the payload is the incorrect size.") + } + }() + expectedPayload = append(expectedPayload, expectedPayload...) + reqMsg.SetMsgPayload(expectedPayload) +} diff --git a/auth/request.go b/auth/request.go index 76af994787f8e1658e13345bf14883e091e7790d..1be24089150fd7ef7dcce0c6ffaaffb1324bab2e 100644 --- a/auth/request.go +++ b/auth/request.go @@ -8,6 +8,7 @@ package auth import ( + "fmt" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces" @@ -61,13 +62,13 @@ func RequestAuth(partner, me contact.Contact, message string, rng io.Reader, "receiving a request") } else if rqType == auth.Sent { resend = true - }else{ - return 0, errors.Errorf("Cannot send a request after " + + } else { + return 0, errors.Errorf("Cannot send a request after "+ " a stored request with unknown rqType: %d", rqType) } - }else if !strings.Contains(err.Error(), auth.NoRequest){ + } else if !strings.Contains(err.Error(), auth.NoRequest) { return 0, errors.WithMessage(err, - "Cannot send a request after receiving unknown error " + + "Cannot send a request after receiving unknown error "+ "on requesting contact status") } @@ -105,11 +106,11 @@ func RequestAuth(partner, me contact.Contact, message string, rng io.Reader, // in this case we have an ongoing request so we can resend the extant // request - if resend{ + if resend { newPrivKey = sr.GetMyPrivKey() newPubKey = sr.GetMyPubKey() - //in this case it is a new request and we must generate new keys - }else{ + //in this case it is a new request and we must generate new keys + } else { //generate new keypair newPrivKey = diffieHellman.GeneratePrivateKey(256, grp, rng) newPubKey = diffieHellman.GeneratePublicKey(newPrivKey, grp) @@ -140,12 +141,16 @@ func RequestAuth(partner, me contact.Contact, message string, rng io.Reader, cmixMsg.SetMac(mac) cmixMsg.SetContents(baseFmt.Marshal()) + jww.TRACE.Printf("RequestAuth SALT: %v", salt) + jww.TRACE.Printf("RequestAuth ECRPAYLOAD: %v", baseFmt.GetEcrPayload()) + jww.TRACE.Printf("RequestAuth MAC: %v", mac) + /*store state*/ //fixme: channel is bricked if the first store succedes but the second fails //store the in progress auth - if !resend{ + if !resend { err = storage.Auth().AddSent(partner.ID, partner.DhPubKey, newPrivKey, - newPrivKey, confirmFp) + newPubKey, confirmFp) if err != nil { return 0, errors.Errorf("Failed to store auth request: %s", err) } @@ -160,13 +165,15 @@ func RequestAuth(partner, me contact.Contact, message string, rng io.Reader, if err != nil { // if the send fails just set it to failed, it will // but automatically retried - return 0, errors.WithMessagef(err, "Auth Request with %s " + + return 0, errors.WithMessagef(err, "Auth Request with %s "+ "(msgDigest: %s) failed to transmit: %+v", partner.ID, cmixMsg.Digest(), err) } - jww.INFO.Printf("Auth Request with %s (msgDigest: %s) sent"+ + em := fmt.Sprintf("Auth Request with %s (msgDigest: %s) sent"+ " on round %d", partner.ID, cmixMsg.Digest(), round) + jww.INFO.Print(em) + net.GetEventManager().Report(1, "Auth", "RequestSent", em) return round, nil } diff --git a/auth/utils_test.go b/auth/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..95ff91489d0b6a8c98f9417265f8fe09ef1680ff --- /dev/null +++ b/auth/utils_test.go @@ -0,0 +1,50 @@ +package auth + +import ( + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/xx_network/crypto/large" + "gitlab.com/xx_network/primitives/id" + "math/rand" +) + +func getGroup() *cyclic.Group { + return cyclic.NewGroup( + large.NewIntFromString("E2EE983D031DC1DB6F1A7A67DF0E9A8E5561DB8E8D4941"+ + "3394C049B7A8ACCEDC298708F121951D9CF920EC5D146727AA4AE535B0922C688"+ + "B55B3DD2AEDF6C01C94764DAB937935AA83BE36E67760713AB44A6337C20E7861"+ + "575E745D31F8B9E9AD8412118C62A3E2E29DF46B0864D0C951C394A5CBBDC6ADC"+ + "718DD2A3E041023DBB5AB23EBB4742DE9C1687B5B34FA48C3521632C4A530E8FF"+ + "B1BC51DADDF453B0B2717C2BC6669ED76B4BDD5C9FF558E88F26E5785302BEDBC"+ + "A23EAC5ACE92096EE8A60642FB61E8F3D24990B8CB12EE448EEF78E184C7242DD"+ + "161C7738F32BF29A841698978825B4111B4BC3E1E198455095958333D776D8B2B"+ + "EEED3A1A1A221A6E37E664A64B83981C46FFDDC1A45E3D5211AAF8BFBC072768C"+ + "4F50D7D7803D2D4F278DE8014A47323631D7E064DE81C0C6BFA43EF0E6998860F"+ + "1390B5D3FEACAF1696015CB79C3F9C2D93D961120CD0E5F12CBB687EAB045241F"+ + "96789C38E89D796138E6319BE62E35D87B1048CA28BE389B575E994DCA7554715"+ + "84A09EC723742DC35873847AEF49F66E43873", 16), + large.NewIntFromString("2", 16)) +} + +// randID returns a new random ID of the specified type. +func randID(rng *rand.Rand, t id.Type) *id.ID { + newID, _ := id.NewRandomID(rng, t) + return newID +} + +func newSalt(s string) []byte { + salt := make([]byte, saltSize) + copy(salt[:], s) + return salt +} + +func newPayload(size int, s string) []byte { + b := make([]byte, size) + copy(b[:], s) + return b +} + +func newOwnership(s string) []byte { + ownership := make([]byte, ownershipSize) + copy(ownership[:], s) + return ownership +} diff --git a/bindings/authenticatedChannels.go b/bindings/authenticatedChannels.go index 30b12f0a1b3039fba5b18eafeff874cc7a438a60..da1d0d7ea128c15a6e4d407f8a7a88430666fd16 100644 --- a/bindings/authenticatedChannels.go +++ b/bindings/authenticatedChannels.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "gitlab.com/elixxir/crypto/contact" + "gitlab.com/xx_network/primitives/id" ) // Create an insecure e2e relationship with a precanned user @@ -123,3 +124,15 @@ func (c *Client) VerifyOwnership(receivedMarshaled, verifiedMarshaled []byte) (b return c.api.VerifyOwnership(received, verified), nil } + +// GetRelationshipFingerprint returns a unique 15 character fingerprint for an +// E2E relationship. An error is returned if no relationship with the partner +// is found. +func (c *Client) GetRelationshipFingerprint(partnerID []byte) (string, error) { + partner, err := id.Unmarshal(partnerID) + if err != nil { + return "", err + } + + return c.api.GetRelationshipFingerprint(partner) +} diff --git a/bindings/callback.go b/bindings/callback.go index a6526d24d3ba961db2b79bdc2fbd6a1b950821dc..897ddcfc68ee19c9e967076ba52764f63fa5f667 100644 --- a/bindings/callback.go +++ b/bindings/callback.go @@ -16,7 +16,7 @@ import ( // Listener provides a callback to hear a message // An object implementing this interface can be called back when the client -// gets a message of the type that the regi sterer specified at registration +// gets a message of the type that the registerer specified at registration // time. type Listener interface { // Hear is called to receive a message in the UI diff --git a/bindings/client.go b/bindings/client.go index 33ef281983799bab25a50663a71b151749ba3535..f6fc9ef358c1a2000767f11d9f712f7ecd22fbc5 100644 --- a/bindings/client.go +++ b/bindings/client.go @@ -8,17 +8,21 @@ package bindings import ( + "bytes" + "encoding/csv" "errors" "fmt" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/api" "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/client/single" "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" + "strings" "sync" "time" ) @@ -37,7 +41,9 @@ func init() { // BindingsClient wraps the api.Client, implementing additional functions // to support the gomobile Client interface type Client struct { - api api.Client + api api.Client + single *single.Manager + singleMux sync.Mutex } // NewClient creates client storage, generates keys, connects, and registers @@ -100,6 +106,7 @@ func Login(storageDir string, password []byte, parameters string) (*Client, erro } extantClient = true clientSingleton := &Client{api: *client} + return clientSingleton, nil } @@ -198,19 +205,20 @@ func UnmarshalSendReport(b []byte) (*SendReport, error) { // Responds to sent rekeys and executes them // - KeyExchange Confirm (/keyExchange/confirm.go) // Responds to confirmations of successful rekey operations -func (c *Client) StartNetworkFollower(clientError ClientError) error { - errChan, err := c.api.StartNetworkFollower() - if err != nil { - return errors.New(fmt.Sprintf("Failed to start the "+ - "network follower: %+v", err)) - } +func (c *Client) StartNetworkFollower(timeoutMS int) error { + timeout := time.Duration(timeoutMS) * time.Millisecond + return c.api.StartNetworkFollower(timeout) +} +// RegisterClientErrorCallback registers the callback to handle errors from the +// long running threads controlled by StartNetworkFollower and StopNetworkFollower +func (c *Client) RegisterClientErrorCallback(clientError ClientError) { + errChan := c.api.GetErrorsChannel() go func() { for report := range errChan { go clientError.Report(report.Source, report.Message, report.Trace) } }() - return nil } // StopNetworkFollower stops the network follower if it is running. @@ -218,9 +226,8 @@ func (c *Client) StartNetworkFollower(clientError ClientError) error { // fails to stop it. // if the network follower is running and this fails, the client object will // most likely be in an unrecoverable state and need to be trashed. -func (c *Client) StopNetworkFollower(timeoutMS int) error { - timeout := time.Duration(timeoutMS) * time.Millisecond - if err := c.api.StopNetworkFollower(timeout); err != nil { +func (c *Client) StopNetworkFollower() error { + if err := c.api.StopNetworkFollower(); err != nil { return errors.New(fmt.Sprintf("Failed to stop the "+ "network follower: %+v", err)) } @@ -232,7 +239,7 @@ func (c *Client) StopNetworkFollower(timeoutMS int) error { func (c *Client) WaitForNetwork(timeoutMS int) bool { start := netTime.Now() timeout := time.Duration(timeoutMS) * time.Millisecond - for netTime.Now().Sub(start) < timeout { + for netTime.Since(start) < timeout { if c.api.GetHealth().IsHealthy() { return true } @@ -256,10 +263,15 @@ func (c *Client) IsNetworkHealthy() bool { return c.api.GetHealth().IsHealthy() } -// registers the network health callback to be called any time the network -// health changes -func (c *Client) RegisterNetworkHealthCB(nhc NetworkHealthCallback) { - c.api.GetHealth().AddFunc(nhc.Callback) +// RegisterNetworkHealthCB registers the network health callback to be called +// any time the network health changes. Returns a unique ID that can be used to +// unregister the network health callback. +func (c *Client) RegisterNetworkHealthCB(nhc NetworkHealthCallback) int64 { + return int64(c.api.GetHealth().AddFunc(nhc.Callback)) +} + +func (c *Client) UnregisterNetworkHealthCB(funcID int64) { + c.api.GetHealth().RemoveFunc(uint64(funcID)) } // RegisterListener records and installs a listener for messages @@ -419,6 +431,52 @@ func (c *Client) GetNodeRegistrationStatus() (*NodeRegistrationsStatus, error) { return &NodeRegistrationsStatus{registered, total}, err } +// DeleteContact is a function which removes a contact from Client's storage +func (c *Client) DeleteContact(b []byte) error { + contactObj, err := UnmarshalContact(b) + if err != nil { + return err + } + return c.api.DeleteContact(contactObj.c.ID) +} + +// SetProxiedBins updates the host pool filter that filters out gateways that +// are not in one of the specified bins. The provided bins should be CSV. +func (c *Client) SetProxiedBins(binStringsCSV string) error { + // Convert CSV to slice of strings + all, err := csv.NewReader(strings.NewReader(binStringsCSV)).ReadAll() + if err != nil { + return err + } + + binStrings := make([]string, 0, len(all[0])) + for _, a := range all { + binStrings = append(binStrings, a...) + } + + return c.api.SetProxiedBins(binStrings) +} + +// GetPreferredBins returns the geographic bin or bins that the provided two +// character country code is a part of. The bins are returned as CSV. +func (c *Client) GetPreferredBins(countryCode string) (string, error) { + bins, err := c.api.GetPreferredBins(countryCode) + if err != nil { + return "", err + } + + // Convert the slice of bins to CSV + buff := bytes.NewBuffer(nil) + csvWriter := csv.NewWriter(buff) + err = csvWriter.Write(bins) + if err != nil { + return "", err + } + csvWriter.Flush() + + return buff.String(), nil +} + /* // SearchWithHandler is a non-blocking search that also registers // a callback interface for user disovery events. @@ -439,3 +497,21 @@ func (b *BindingsClient) Search(data, separator string, searchTypes []byte) ContactList { return nil }*/ + +// getSingle is a function which returns the single mananger if it +// exists or creates a new one, checking appropriate constraints +// (that the network follower is running) if it needs to make one +func (c *Client) getSingle() (*single.Manager, error) { + c.singleMux.Lock() + defer c.singleMux.Unlock() + if c.single == nil { + apiClient := &c.api + c.single = single.NewManager(apiClient) + err := apiClient.AddService(c.single.StartProcesses) + if err != nil { + return nil, err + } + } + + return c.single, nil +} diff --git a/bindings/errors.go b/bindings/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..cfffaf0155a8993787120d0bce24f6c1071baeb2 --- /dev/null +++ b/bindings/errors.go @@ -0,0 +1,105 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "context" + "encoding/json" + "fmt" + "github.com/pkg/errors" + "strings" + "sync" +) + +// errToUserErr maps backend patterns to user friendly error messages. +// Example format: +// (Back-end) "Building new HostPool because no HostList stored:": (Front-end) "Missing host list", +var errToUserErr = map[string]string{ + // Registration errors + //"cannot create username when network is not health" : + // "Cannot create username, unable to connect to network", + //"failed to add due to malformed fact stringified facts must at least have a type at the start" : + // "Invalid fact, is the field empty?", + //// UD failures + //"failed to create user discovery manager: cannot return single manager, network is not health" : + // "Could not connect to user discovery", + //"user discovery returned error on search: no results found" : + // "No results found", + //"failed to search.: waiting for response to single-use transmisson timed out after 10s" : + // "Search timed out", + //"the phone number supplied was empty" : "Invalid phone number", + //"failed to create user discovery manager: cannot start ud manager when network follower is not running." : + // "Could not get network status", +} + +var errorMux sync.RWMutex + +// Error codes +const UnrecognizedCode = "UR: " +const UnrecognizedMessage = UnrecognizedCode + "Unrecognized error from XX backend, please report" + +// ErrorStringToUserFriendlyMessage takes a passed in errStr which will be +// a backend generated error. These may be error specifically written by +// the backend team or lower level errors gotten from low level dependencies. +// This function will parse the error string for common errors provided from +// errToUserErr to provide a more user-friendly error message for the front end. +// If the error is not common, some simple parsing is done on the error message +// to make it more user-accessible, removing backend specific jargon. +func ErrorStringToUserFriendlyMessage(errStr string) string { + errorMux.RLock() + defer errorMux.RUnlock() + // Go through common errors + for backendErr, userFriendly := range errToUserErr { + // Determine if error contains a common error + if strings.Contains(errStr, backendErr) { + return userFriendly + } + } + + descStr := "desc = " + // If this contains an rpc error, determine how to handle it + if strings.Contains(errStr, context.DeadlineExceeded.Error()) { + // If there is a context deadline exceeded message, return the higher level + // as context deadline exceeded is not informative + rpcErr := "rpc " + rpcIdx := strings.Index(errStr, rpcErr) + return errStr[:rpcIdx] + } else if strings.Contains(errStr, descStr) { + // If containing an rpc error where context deadline exceeded + // is NOT involved, the error returned server-side is often + //more informative + descIdx := strings.Index(errStr, descStr) + // return everything after "desc = " + return errStr[descIdx+len(descStr):] + } + + // If a compound error message, return the highest level message + errParts := strings.Split(errStr, ":") + if len(errParts) > 1 { + // Return everything before the first : + return UnrecognizedCode + errParts[0] + } + + return fmt.Sprintf("%s: %v", UnrecognizedCode, errStr) +} + +// UpdateCommonErrors takes the passed in contents of a JSON file and updates the +// errToUserErr map with the contents of the json file. The JSON's expected format +// conform with the commented examples provides in errToUserErr above. +// NOTE that you should not pass in a file path, but a preloaded JSON file +func UpdateCommonErrors(jsonFile string) error { + errorMux.Lock() + defer errorMux.Unlock() + err := json.Unmarshal([]byte(jsonFile), &errToUserErr) + if err != nil { + return errors.WithMessage(err, "Failed to unmarshal json file, "+ + "did you pass in the contents or the path?") + } + + return nil +} diff --git a/bindings/errors_test.go b/bindings/errors_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d93a9924fb336e68fbe6c31af30174709f049d8d --- /dev/null +++ b/bindings/errors_test.go @@ -0,0 +1,107 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "context" + "strings" + "testing" +) + +// Unit test +func TestErrorStringToUserFriendlyMessage(t *testing.T) { + // Setup: Populate map + backendErrs := []string{"Failed to Unmarshal Conversation", "failed to create group key preimage", + "Failed to unmarshal SentRequestMap"} + userErrs := []string{"Could not retrieve conversation", "Failed to initiate group chat", + "Failed to pull up friend requests"} + + for i, exampleErr := range backendErrs { + errToUserErr[exampleErr] = userErrs[i] + } + + // Check if a mapped common error returns the expected user friendly error + received := ErrorStringToUserFriendlyMessage(backendErrs[0]) + if strings.Compare(received, userErrs[0]) != 0 { + t.Errorf("Unexpected user friendly message returned from common error mapping."+ + "\n\tExpected: %s"+ + "\n\tReceived: %v", userErrs[0], received) + } + + // Test RPC error in which high level information should + // be passed along (ie context deadline exceeded error) + expected := "Could not poll network: " + rpcPrefix := "rpc error: desc = " + rpcErr := expected + rpcPrefix + context.DeadlineExceeded.Error() + received = ErrorStringToUserFriendlyMessage(rpcErr) + if strings.Compare(expected, received) != 0 { + t.Errorf("Rpc error parsed unxecpectedly with error "+ + "\n\"%s\" "+ + "\n\tExpected: %s"+ + "\n\tReceived: %v", rpcErr, UnrecognizedCode+expected, received) + } + + // Test RPC error where server side error information is provided + serverSideError := "Could not parse message! Please try again with a properly crafted message" + rpcErr = rpcPrefix + serverSideError + received = ErrorStringToUserFriendlyMessage(rpcErr) + if strings.Compare(serverSideError, received) != 0 { + t.Errorf("RPC error parsed unexpectedly with error "+ + "\n\"%s\" "+ + "\n\tExpected: %s"+ + "\n\tReceived: %v", rpcErr, UnrecognizedCode+serverSideError, received) + } + + // Test uncommon error, should return highest level message + expected = "failed to register with permissioning" + uncommonErr := expected + ": sendRegistrationMessage: Unable to contact Identity Server" + received = ErrorStringToUserFriendlyMessage(uncommonErr) + if strings.Compare(received, UnrecognizedCode+expected) != 0 { + t.Errorf("Uncommon error parsed unexpectedly with error "+ + "\n\"%s\" "+ + "\n\tExpected: %s"+ + "\n\tReceived: %s", uncommonErr, UnrecognizedCode+expected, received) + } + + // Test fully unrecognizable and un-parsable message, + // should hardcoded error message + uncommonErr = "failed to register with permissioning" + received = ErrorStringToUserFriendlyMessage(uncommonErr) + if strings.Compare(UnrecognizedCode+": "+uncommonErr, received) != 0 { + t.Errorf("Uncommon error parsed unexpectedly with error "+ + "\n\"%s\" "+ + "\n\tExpected: %s"+ + "\n\tReceived: %s", uncommonErr, UnrecognizedMessage, received) + } + +} + +// Unit test +func TestClient_UpdateCommonErrors(t *testing.T) { + + key, expectedVal := "failed to create group key preimage", "Failed to initiate group chat" + + jsonData := "{\"Failed to Unmarshal Conversation\":\"Could not retrieve conversation\",\"Failed to unmarshal SentRequestMap\":\"Failed to pull up friend requests\",\"failed to create group key preimage\":\"Failed to initiate group chat\"}\n" + + err := UpdateCommonErrors(jsonData) + if err != nil { + t.Fatalf("UpdateCommonErrors error: %v", err) + } + + val, ok := errToUserErr[key] + if !ok { + t.Fatalf("Expected entry was not populated") + } + + if strings.Compare(expectedVal, val) != 0 { + t.Fatalf("Entry in updated error map was not expected."+ + "\n\tExpected: %s"+ + "\n\tReceived: %s", expectedVal, val) + } + +} diff --git a/bindings/event.go b/bindings/event.go new file mode 100644 index 0000000000000000000000000000000000000000..7e28c72e025768a7dc454098e070d923c9832f2f --- /dev/null +++ b/bindings/event.go @@ -0,0 +1,26 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "gitlab.com/elixxir/client/interfaces" +) + +// RegisterEventCallback records the given function to receive +// ReportableEvent objects. It returns the internal index +// of the callback so that it can be deleted later. +func (c *Client) RegisterEventCallback(name string, + myFunc interfaces.EventCallbackFunction) error { + return c.api.RegisterEventCallback(name, myFunc) +} + +// UnregisterEventCallback deletes the callback identified by the +// index. It returns an error if it fails. +func (c *Client) UnregisterEventCallback(name string) { + c.api.UnregisterEventCallback(name) +} diff --git a/bindings/group.go b/bindings/group.go new file mode 100644 index 0000000000000000000000000000000000000000..00c45668e08563488884f68e5c9f76b2a0907687 --- /dev/null +++ b/bindings/group.go @@ -0,0 +1,301 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "github.com/pkg/errors" + gc "gitlab.com/elixxir/client/groupChat" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/xx_network/primitives/id" +) + +// GroupChat object contains the group chat manager. +type GroupChat struct { + m *gc.Manager +} + +// GroupRequestFunc contains a function callback that is called when a group +// request is received. +type GroupRequestFunc interface { + GroupRequestCallback(g Group) +} + +// GroupReceiveFunc contains a function callback that is called when a group +// message is received. +type GroupReceiveFunc interface { + GroupReceiveCallback(msg GroupMessageReceive) +} + +// NewGroupManager creates a new group chat manager. +func NewGroupManager(client *Client, requestFunc GroupRequestFunc, + receiveFunc GroupReceiveFunc) (GroupChat, error) { + + requestCallback := func(g gs.Group) { + requestFunc.GroupRequestCallback(Group{g}) + } + receiveCallback := func(msg gc.MessageReceive) { + receiveFunc.GroupReceiveCallback(GroupMessageReceive{msg}) + } + + // Create a new group chat manager + m, err := gc.NewManager(&client.api, requestCallback, receiveCallback) + if err != nil { + return GroupChat{}, err + } + + // Start group request and message retrieval workers + err = client.api.AddService(m.StartProcesses) + if err != nil { + return GroupChat{}, err + } + + return GroupChat{m}, nil +} + +// MakeGroup creates a new group and sends a group request to all members in the +// group. The ID of the new group, the rounds the requests were sent on, and the +// status of the send are contained in NewGroupReport. +func (g GroupChat) MakeGroup(membership IdList, name, message []byte) (NewGroupReport, error) { + grp, rounds, status, err := g.m.MakeGroup(membership.list, name, message) + return NewGroupReport{Group{grp}, rounds, status}, err +} + +// ResendRequest resends a group request to all members in the group. The rounds +// they were sent on and the status of the send are contained in NewGroupReport. +func (g GroupChat) ResendRequest(groupIdBytes []byte) (NewGroupReport, error) { + groupID, err := id.Unmarshal(groupIdBytes) + if err != nil { + return NewGroupReport{}, + errors.Errorf("Failed to unmarshal group ID: %+v", err) + } + + rounds, status, err := g.m.ResendRequest(groupID) + + return NewGroupReport{Group{}, rounds, status}, nil +} + +// JoinGroup allows a user to join a group when they receive a request. The +// caller must pass in the serialized bytes of a Group. +func (g GroupChat) JoinGroup(serializedGroupData []byte) error { + grp, err := gs.DeserializeGroup(serializedGroupData) + if err != nil { + return err + } + return g.m.JoinGroup(grp) +} + +// LeaveGroup deletes a group so a user no longer has access. +func (g GroupChat) LeaveGroup(groupIdBytes []byte) error { + groupID, err := id.Unmarshal(groupIdBytes) + if err != nil { + return errors.Errorf("Failed to unmarshal group ID: %+v", err) + } + + return g.m.LeaveGroup(groupID) +} + +// Send sends the message to the specified group. Returns the round the messages +// were sent on. +func (g GroupChat) Send(groupIdBytes, message []byte) (int64, error) { + groupID, err := id.Unmarshal(groupIdBytes) + if err != nil { + return 0, errors.Errorf("Failed to unmarshal group ID: %+v", err) + } + + round, err := g.m.Send(groupID, message) + return int64(round), err +} + +// GetGroups returns an IdList containing a list of group IDs that the user is a +// part of. +func (g GroupChat) GetGroups() IdList { + return IdList{g.m.GetGroups()} +} + +// GetGroup returns the group with the group ID. If no group exists, then the +// error "failed to find group" is returned. +func (g GroupChat) GetGroup(groupIdBytes []byte) (Group, error) { + groupID, err := id.Unmarshal(groupIdBytes) + if err != nil { + return Group{}, errors.Errorf("Failed to unmarshal group ID: %+v", err) + } + + grp, exists := g.m.GetGroup(groupID) + if !exists { + return Group{}, errors.New("failed to find group") + } + + return Group{grp}, nil +} + +// NumGroups returns the number of groups the user is a part of. +func (g GroupChat) NumGroups() int { + return g.m.NumGroups() +} + +// NewGroupReport is returned when creating a new group and contains the ID of +// the group, a list of rounds that the group requests were sent on, and the +// status of the send. +type NewGroupReport struct { + group Group + rounds []id.Round + status gc.RequestStatus +} + +// GetGroup returns the Group. +func (ngr NewGroupReport) GetGroup() Group { + return ngr.group +} + +// GetRoundList returns the RoundList containing a list of rounds requests were +// sent on. +func (ngr NewGroupReport) GetRoundList() RoundList { + return RoundList{ngr.rounds} +} + +// GetStatus returns the status of the requests sent when creating a new group. +// status = 0 an error occurred before any requests could be sent +// 1 all requests failed to send +// 2 some request failed and some succeeded +// 3, all requests sent successfully +func (ngr NewGroupReport) GetStatus() int { + return int(ngr.status) +} + +//// +// Group Structure +//// + +// Group structure contains the identifying and membership information of a +// group chat. +type Group struct { + g gs.Group +} + +// GetName returns the name set by the user for the group. +func (g Group) GetName() []byte { + return g.g.Name +} + +// GetID return the 33-byte unique group ID. +func (g Group) GetID() []byte { + return g.g.ID.Bytes() +} + +// GetMembership returns a list of contacts, one for each member in the group. +// The list is in order; the first contact is the leader/creator of the group. +// All subsequent members are ordered by their ID. +func (g Group) GetMembership() GroupMembership { + return GroupMembership{g.g.Members} +} + +// Serialize serializes the Group. +func (g Group) Serialize() []byte { + return g.g.Serialize() +} + +//// +// Membership Structure +//// + +// GroupMembership structure contains a list of members that are part of a +// group. The first member is the group leader. +type GroupMembership struct { + m group.Membership +} + +// Len returns the number of members in the group membership. +func (gm GroupMembership) Len() int { + return gm.Len() +} + +// Get returns the member at the index. The member at index 0 is always the +// group leader. An error is returned if the index is out of range. +func (gm GroupMembership) Get(i int) (GroupMember, error) { + if i < 0 || i > gm.Len() { + return GroupMember{}, errors.Errorf("ID list index must be between %d "+ + "and the last element %d.", 0, gm.Len()) + } + return GroupMember{gm.m[i]}, nil +} + +//// +// Member Structure +//// +// GroupMember represents a member in the group membership list. +type GroupMember struct { + group.Member +} + +// GetID returns the 33-byte user ID of the member. +func (gm GroupMember) GetID() []byte { + return gm.ID.Bytes() +} + +// GetDhKey returns the byte representation of the public Diffie–Hellman key of +// the member. +func (gm GroupMember) GetDhKey() []byte { + return gm.DhKey.Bytes() +} + +//// +// Message Receive Structure +//// + +// GroupMessageReceive contains a group message, its ID, and its data that a +// user receives. +type GroupMessageReceive struct { + gc.MessageReceive +} + +// GetGroupID returns the 33-byte group ID. +func (gmr GroupMessageReceive) GetGroupID() []byte { + return gmr.GroupID.Bytes() +} + +// GetMessageID returns the message ID. +func (gmr GroupMessageReceive) GetMessageID() []byte { + return gmr.ID.Bytes() +} + +// GetPayload returns the message payload. +func (gmr GroupMessageReceive) GetPayload() []byte { + return gmr.Payload +} + +// GetSenderID returns the 33-byte user ID of the sender. +func (gmr GroupMessageReceive) GetSenderID() []byte { + return gmr.SenderID.Bytes() +} + +// GetRecipientID returns the 33-byte user ID of the recipient. +func (gmr GroupMessageReceive) GetRecipientID() []byte { + return gmr.RecipientID.Bytes() +} + +// GetEphemeralID returns the ephemeral ID of the recipient. +func (gmr GroupMessageReceive) GetEphemeralID() int64 { + return gmr.EphemeralID.Int64() +} + +// GetTimestampNano returns the message timestamp in nanoseconds. +func (gmr GroupMessageReceive) GetTimestampNano() int64 { + return gmr.Timestamp.UnixNano() +} + +// GetRoundID returns the ID of the round the message was sent on. +func (gmr GroupMessageReceive) GetRoundID() int64 { + return int64(gmr.RoundID) +} + +// GetRoundTimestampNano returns the timestamp, in nanoseconds, of the round the +// message was sent on. +func (gmr GroupMessageReceive) GetRoundTimestampNano() int64 { + return gmr.RoundTimestamp.UnixNano() +} diff --git a/bindings/list.go b/bindings/list.go index c44fb1679fc0e6492c7c711e7d610bab01198c78..a97df24f299d994380d46de8ae9971ad9133c1e7 100644 --- a/bindings/list.go +++ b/bindings/list.go @@ -8,7 +8,7 @@ package bindings import ( - "errors" + "github.com/pkg/errors" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/primitives/fact" "gitlab.com/xx_network/primitives/id" @@ -115,3 +115,41 @@ func (fl *FactList) Add(factData string, factType int) error { func (fl *FactList) Stringify() (string, error) { return fl.c.Facts.Stringify(), nil } + +/* ID list */ +// IdList contains a list of IDs. +type IdList struct { + list []*id.ID +} + +// MakeIdList creates a new empty IdList. +func MakeIdList() IdList { + return IdList{[]*id.ID{}} +} + +// Len returns the number of IDs in the list. +func (idl IdList) Len() int { + return len(idl.list) +} + +// Add appends the ID bytes to the end of the list. +func (idl IdList) Add(idBytes []byte) error { + newID, err := id.Unmarshal(idBytes) + if err != nil { + return err + } + + idl.list = append(idl.list, newID) + return nil +} + +// Get returns the ID at the index. An error is returned if the index is out of +// range. +func (idl IdList) Get(i int) ([]byte, error) { + if i < 0 || i > len(idl.list) { + return nil, errors.Errorf("ID list index must be between %d and the "+ + "last element %d.", 0, len(idl.list)) + } + + return idl.list[i].Bytes(), nil +} diff --git a/bindings/message.go b/bindings/message.go index 32947d5fc3b0fc7e87579bdacd39c1bc69edb404..680d9caf778d8d8ceb700b0a3d3ad2b8b6b20d4b 100644 --- a/bindings/message.go +++ b/bindings/message.go @@ -17,33 +17,51 @@ type Message struct { r message.Receive } -//Returns the id of the message +// GetID returns the id of the message func (m *Message) GetID() []byte { return m.r.ID[:] } -// Returns the message's sender ID, if available +// GetSender returns the message's sender ID, if available func (m *Message) GetSender() []byte { return m.r.Sender.Bytes() } -// Returns the message's payload/contents +// GetPayload returns the message's payload/contents func (m *Message) GetPayload() []byte { return m.r.Payload } -// Returns the message's type +// GetMessageType returns the message's type func (m *Message) GetMessageType() int { return int(m.r.MessageType) } -// Returns the message's timestamp in ms +// GetTimestampMS returns the message's timestamp in milliseconds func (m *Message) GetTimestampMS() int64 { ts := m.r.Timestamp.UnixNano() - ts = (ts + 999999) / 1000000 + ts = (ts + 500000) / 1000000 return ts } +// GetTimestampNano returns the message's timestamp in nanoseconds func (m *Message) GetTimestampNano() int64 { return m.r.Timestamp.UnixNano() } + +// GetRoundTimestampMS returns the message's round timestamp in milliseconds +func (m *Message) GetRoundTimestampMS() int64 { + ts := m.r.RoundTimestamp.UnixNano() + ts = (ts + 999999) / 1000000 + return ts +} + +// GetRoundTimestampNano returns the message's round timestamp in nanoseconds +func (m *Message) GetRoundTimestampNano() int64 { + return m.r.RoundTimestamp.UnixNano() +} + +// GetRoundId returns the message's round ID +func (m *Message) GetRoundId() int64 { + return int64(m.r.RoundId) +} diff --git a/bindings/mnemonic.go b/bindings/mnemonic.go new file mode 100644 index 0000000000000000000000000000000000000000..7062341a1958bb3cd2c0e0e9a960b454f1a143cf --- /dev/null +++ b/bindings/mnemonic.go @@ -0,0 +1,34 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import "gitlab.com/elixxir/client/api" + +// StoreSecretWithMnemonic stores the secret tied with the mnemonic to storage. +// Unlike other storage operations, this does not use EKV, as that is +// intrinsically tied to client operations, which the user will not have while +// trying to recover their account. As such, we store the encrypted data +// directly, with a specified path. Path will be a valid filepath in which the +// recover file will be stored as ".recovery". +// +// As an example, given "home/user/xxmessenger/storagePath", +// the recovery file will be stored at +// "home/user/xxmessenger/storagePath/.recovery" +func StoreSecretWithMnemonic(secret []byte, path string) (string, error) { + return api.StoreSecretWithMnemonic(secret, path) +} + +// LoadSecretWithMnemonic loads the secret stored from the call to +// StoreSecretWithMnemonic. The path given should be the same filepath +// as the path given in StoreSecretWithMnemonic. There should be a file +// in this path called ".recovery". This operation is not tied +// to client operations, as the user will not have a client when trying to +// recover their account. +func LoadSecretWithMnemonic(mnemonic, path string) (secret []byte, err error) { + return api.LoadSecretWithMnemonic(mnemonic, path) +} diff --git a/bindings/params.go b/bindings/params.go index 35afbb8901cfd5e8309f8be7a30a14c907496c23..d4896a2e9e89c49c177689c85ff0a2cf410f9277 100644 --- a/bindings/params.go +++ b/bindings/params.go @@ -13,22 +13,22 @@ import ( "gitlab.com/elixxir/client/interfaces/params" ) -func (c *Client) GetCMIXParams() (string, error) { +func GetCMIXParams() (string, error) { p, err := params.GetDefaultCMIX().Marshal() return string(p), err } -func (c *Client) GetE2EParams() (string, error) { +func GetE2EParams() (string, error) { p, err := params.GetDefaultE2E().Marshal() return string(p), err } -func (c *Client) GetNetworkParams() (string, error) { +func GetNetworkParams() (string, error) { p, err := params.GetDefaultNetwork().Marshal() return string(p), err } -func (c *Client) GetUnsafeParams() (string, error) { +func GetUnsafeParams() (string, error) { p, err := params.GetDefaultUnsafe().Marshal() return string(p), err } diff --git a/bindings/secrets.go b/bindings/secrets.go new file mode 100644 index 0000000000000000000000000000000000000000..5bdeeed03c35cf6e0af99b8a64aaf54f213e8a23 --- /dev/null +++ b/bindings/secrets.go @@ -0,0 +1,34 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/xx_network/crypto/csprng" +) + +// GenerateSecret creates a secret password using a system-based +// pseudorandom number generator. It takes 1 parameter, `numBytes`, +// which should be set to 32, but can be set higher in certain cases. +func GenerateSecret(numBytes int) []byte { + if numBytes < 32 { + jww.FATAL.Panicf("Secrets must have at least 32 bytes " + + "(256 bits) of entropy.") + } + + out := make([]byte, numBytes) + rng := csprng.NewSystemRNG() + numRead, err := rng.Read(out) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + if numRead != numBytes { + jww.FATAL.Panicf("Unable to read %d bytes", numBytes) + } + return out +} diff --git a/bindings/secrets_test.go b/bindings/secrets_test.go new file mode 100644 index 0000000000000000000000000000000000000000..20e1a7d51f1b182fc44df8902e85d462b615b64b --- /dev/null +++ b/bindings/secrets_test.go @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the LICENSE file // +//////////////////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "bytes" + "testing" +) + +func TestGenerateSecret(t *testing.T) { + secret1 := GenerateSecret(32) + secret2 := GenerateSecret(32) + + if bytes.Compare(secret1, secret2) == 0 { + t.Errorf("GenerateSecret: Not generating entropy") + } + + // This runs after the test function and errors out if no panic was + // raised. + defer func() { + if r := recover(); r == nil { + t.Errorf("GenerateSecret: Low entropy was permitted") + } + }() + GenerateSecret(31) +} diff --git a/bindings/send.go b/bindings/send.go index 886bf0ea819557c50ddef4b41846d0ae3d5a5d00..cef9c99500772b54012dcf07f6b4d3aaea5628a8 100644 --- a/bindings/send.go +++ b/bindings/send.go @@ -15,6 +15,7 @@ import ( "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/crypto/e2e" "gitlab.com/xx_network/primitives/id" + "time" ) // SendCMIX sends a "raw" CMIX message payload to the provided @@ -57,6 +58,53 @@ func (c *Client) SendCmix(recipient, contents []byte, parameters string) (int, e return int(rid), nil } +// SendManyCMIX sends many "raw" CMIX message payloads to each of the +// provided recipients. Used for group chat functionality. Returns the +// round ID of the round the payload was sent or an error if it fails. +// This will return an error if: +// - any recipient ID is invalid +// - any of the the message contents are too long for the message structure +// - the message cannot be sent + +// This will return the round the message was sent on if it is successfully sent +// This can be used to register a round event to learn about message delivery. +// on failure a round id of -1 is returned +// fixme: cannot use a slice of slices over bindings. Will need to modify this function once +// a proper input format has been specified +//func (c *Client) SendManyCMIX(recipients, contents [][]byte, parameters string) (int, error) { +// +// p, err := params.GetCMIXParameters(parameters) +// if err != nil { +// return -1, errors.New(fmt.Sprintf("Failed to sendCmix: %+v", +// err)) +// } +// +// // Build messages +// messages := make(map[id.ID]format.Message, len(contents)) +// for i := 0; i < len(contents); i++ { +// msg, err := c.api.NewCMIXMessage(contents[i]) +// if err != nil { +// return -1, errors.New(fmt.Sprintf("Failed to sendCmix: %+v", +// err)) +// } +// +// u, err := id.Unmarshal(recipients[i]) +// if err != nil { +// return -1, errors.New(fmt.Sprintf("Failed to sendCmix: %+v", +// err)) +// } +// +// messages[*u] = msg +// } +// +// rid, _, err := c.api.SendManyCMIX(messages, p) +// if err != nil { +// return -1, errors.New(fmt.Sprintf("Failed to sendCmix: %+v", +// err)) +// } +// return int(rid), nil +//} + // SendUnsafe sends an unencrypted payload to the provided recipient // with the provided msgType. Returns the list of rounds in which parts // of the message were sent or an error if it fails. @@ -116,7 +164,7 @@ func (c *Client) SendE2E(recipient, payload []byte, messageType int, parameters MessageType: message.Type(messageType), } - rids, mid, err := c.api.SendE2E(m, p) + rids, mid, ts, err := c.api.SendE2E(m, p) if err != nil { return nil, errors.New(fmt.Sprintf("Failed SendE2E: %+v", err)) } @@ -124,6 +172,7 @@ func (c *Client) SendE2E(recipient, payload []byte, messageType int, parameters sr := SendReport{ rl: &RoundList{list: rids}, mid: mid, + ts: ts, } return &sr, nil @@ -133,6 +182,7 @@ func (c *Client) SendE2E(recipient, payload []byte, messageType int, parameters type SendReport struct { rl *RoundList mid e2e.MessageID + ts time.Time } type SendReportDisk struct { @@ -148,6 +198,18 @@ func (sr *SendReport) GetMessageID() []byte { return sr.mid[:] } +// GetTimestampMS returns the message's timestamp in milliseconds +func (sr *SendReport) GetTimestampMS() int64 { + ts := sr.ts.UnixNano() + ts = (ts + 500000) / 1000000 + return ts +} + +// GetTimestampNano returns the message's timestamp in nanoseconds +func (sr *SendReport) GetTimestampNano() int64 { + return sr.ts.UnixNano() +} + func (sr *SendReport) Marshal() ([]byte, error) { srd := SendReportDisk{ List: sr.rl.list, diff --git a/bindings/timeNow.go b/bindings/timeNow.go index 82cc2d798d8da4c16936b53b2ed3621b175e0e02..fc457b459ae43452a1920c26a8b9e24c53725d74 100644 --- a/bindings/timeNow.go +++ b/bindings/timeNow.go @@ -19,6 +19,6 @@ type TimeSource interface { // SetTimeSource sets the network time to a custom source. func SetTimeSource(timeNow TimeSource) { netTime.Now = func() time.Time { - return time.Unix(0,timeNow.NowMs()*int64(time.Millisecond)) + return time.Unix(0, timeNow.NowMs()*int64(time.Millisecond)) } } diff --git a/bindings/ud.go b/bindings/ud.go index 91a700d3b5e94316418699396dccf2f68ab00d5a..15b0e012a7ffaad5b290da54b832bcffdf39fa57 100644 --- a/bindings/ud.go +++ b/bindings/ud.go @@ -9,7 +9,6 @@ package bindings import ( "github.com/pkg/errors" - "gitlab.com/elixxir/client/single" "gitlab.com/elixxir/client/ud" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/primitives/fact" @@ -31,11 +30,16 @@ type UserDiscovery struct { // the bindings to think the other is in charge of the client object. // In general this is not an issue because the client object should exist // for the life of the program. +// This must be called while start network follower is running. func NewUserDiscovery(client *Client) (*UserDiscovery, error) { - m, err := ud.NewManager(&client.api, &single.Manager{}) + single, err := client.getSingle() + if err != nil { + return nil, errors.WithMessage(err, "Failed to create User Discovery Manager") + } + m, err := ud.NewManager(&client.api, single) if err != nil { - return nil, err + return nil, errors.WithMessage(err, "Failed to create User Discovery Manager") } else { return &UserDiscovery{ud: m}, nil } @@ -76,6 +80,7 @@ func (ud *UserDiscovery) ConfirmFact(confirmationID, code string) error { // Removes a previously confirmed fact. Will fail if the passed fact string is // not well formed or if the fact is not associated with this client. +// Users cannot remove username facts and must instead remove the user. func (ud *UserDiscovery) RemoveFact(fStr string) error { f, err := fact.UnstringifyFact(fStr) if err != nil { @@ -85,6 +90,18 @@ func (ud *UserDiscovery) RemoveFact(fStr string) error { return ud.ud.RemoveFact(f) } +// RemoveUser deletes a user. The fact sent must be the username. +// This function preserves the username forever and makes it +// unusable. +func (ud *UserDiscovery) RemoveUser(fStr string) error { + f, err := fact.UnstringifyFact(fStr) + if err != nil { + return errors.WithMessage(err, "Failed to remove due to "+ + "malformed fact") + } + return ud.ud.RemoveUser(f) +} + // SearchCallback returns the result of a search type SearchCallback interface { Callback(contacts *ContactList, error string) diff --git a/cmd/getndf.go b/cmd/getndf.go index 7ac150020c911d695f646993e3a409c18d6cb8da..22edea7c6f99803110bcf95a88914ab01e84f67c 100644 --- a/cmd/getndf.go +++ b/cmd/getndf.go @@ -70,8 +70,8 @@ var getNDFCmd = &cobra.Command{ Partial: &pb.NDFHash{ Hash: nil, }, - LastUpdate: uint64(0), - ReceptionID: dummyID[:], + LastUpdate: uint64(0), + ReceptionID: dummyID[:], ClientVersion: []byte(api.SEMVER), } resp, err := comms.SendPoll(host, pollMsg) @@ -110,7 +110,7 @@ func init() { viper.BindPFlag("gwhost", getNDFCmd.Flags().Lookup("gwhost")) getNDFCmd.Flags().StringP("permhost", "", "", - "Poll this permissioning host:port for the NDF") + "Poll this registration host:port for the NDF") viper.BindPFlag("permhost", getNDFCmd.Flags().Lookup("permhost")) diff --git a/cmd/group.go b/cmd/group.go new file mode 100644 index 0000000000000000000000000000000000000000..f1e4508a72e2ce6c0c29b836e67e7b0bef8d91d9 --- /dev/null +++ b/cmd/group.go @@ -0,0 +1,348 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +// The group subcommand allows creation and sending messages to groups + +package cmd + +import ( + "bufio" + "fmt" + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" + "gitlab.com/elixxir/client/api" + "gitlab.com/elixxir/client/groupChat" + "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/xx_network/primitives/id" + "os" + "time" +) + +// groupCmd represents the base command when called without any subcommands +var groupCmd = &cobra.Command{ + Use: "group", + Short: "Group commands for cMix client", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + + client := initClient() + + // Print user's reception ID + user := client.GetUser() + jww.INFO.Printf("User: %s", user.ReceptionID) + + _, _ = initClientCallbacks(client) + + err := client.StartNetworkFollower(5 * time.Second) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + + // Initialize the group chat manager + groupManager, recChan, reqChan := initGroupManager(client) + + // Wait until connected or crash on timeout + connected := make(chan bool, 10) + client.GetHealth().AddChannel(connected) + waitUntilConnected(connected) + + // After connection, make sure we have registered with at least 85% of + // the nodes + for numReg, total := 1, 100; numReg < (total*3)/4; { + time.Sleep(1 * time.Second) + numReg, total, err = client.GetNodeRegistrationStatus() + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + + jww.INFO.Printf("Registering with nodes (%d/%d)...", numReg, total) + } + + // Get group message and name + msgBody := []byte(viper.GetString("message")) + name := []byte(viper.GetString("name")) + timeout := viper.GetDuration("receiveTimeout") + + if viper.IsSet("create") { + filePath := viper.GetString("create") + createGroup(name, msgBody, filePath, groupManager) + } + + if viper.IsSet("resend") { + groupIdString := viper.GetString("resend") + resendRequests(groupIdString, groupManager) + } + + if viper.GetBool("join") { + joinGroup(reqChan, timeout, groupManager) + } + + if viper.IsSet("leave") { + groupIdString := viper.GetString("leave") + leaveGroup(groupIdString, groupManager) + } + + if viper.IsSet("sendMessage") { + groupIdString := viper.GetString("sendMessage") + sendGroup(groupIdString, msgBody, groupManager) + } + + if viper.IsSet("wait") { + numMessages := viper.GetUint("wait") + messageWait(numMessages, timeout, recChan) + } + + if viper.GetBool("list") { + listGroups(groupManager) + } + + if viper.IsSet("show") { + groupIdString := viper.GetString("show") + showGroup(groupIdString, groupManager) + } + }, +} + +// initGroupManager creates a new group chat manager and starts the process +// service. +func initGroupManager(client *api.Client) (*groupChat.Manager, + chan groupChat.MessageReceive, chan groupStore.Group) { + recChan := make(chan groupChat.MessageReceive, 10) + receiveCb := func(msg groupChat.MessageReceive) { + recChan <- msg + } + + reqChan := make(chan groupStore.Group, 10) + requestCb := func(g groupStore.Group) { + reqChan <- g + } + + jww.INFO.Print("Creating new group manager.") + manager, err := groupChat.NewManager(client, requestCb, receiveCb) + if err != nil { + jww.FATAL.Panicf("Failed to initialize group chat manager: %+v", err) + } + + // Start group request and message receiver + err = client.AddService(manager.StartProcesses) + if err != nil { + jww.FATAL.Panicf("Failed to start groupchat services: %+v", err) + } + + return manager, recChan, reqChan +} + +// createGroup creates a new group with the provided name and sends out requests +// to the list of user IDs found at the given file path. +func createGroup(name, msg []byte, filePath string, gm *groupChat.Manager) { + userIdStrings := ReadLines(filePath) + userIDs := make([]*id.ID, 0, len(userIdStrings)) + for _, userIdStr := range userIdStrings { + userID, _ := parseRecipient(userIdStr) + userIDs = append(userIDs, userID) + } + + grp, rids, status, err := gm.MakeGroup(userIDs, name, msg) + if err != nil { + jww.FATAL.Panicf("Failed to create new group: %+v", err) + } + + // Integration grabs the group ID from this line + jww.INFO.Printf("NewGroupID: b64:%s", grp.ID) + jww.INFO.Printf("Created Group: Requests:%s on rounds %#v, %v", status, rids, grp) + fmt.Printf("Created new group with name %q and message %q\n", grp.Name, + grp.InitMessage) +} + +// resendRequests resends group requests for the group ID. +func resendRequests(groupIdString string, gm *groupChat.Manager) { + groupID, _ := parseRecipient(groupIdString) + rids, status, err := gm.ResendRequest(groupID) + if err != nil { + jww.FATAL.Panicf("Failed to resend requests to group %s: %+v", + groupID, err) + } + + jww.INFO.Printf("Resending requests to group %s: %v, %s", groupID, rids, status) + fmt.Println("Resending group requests to group.") +} + +// joinGroup joins a group when a request is received on the group request +// channel. +func joinGroup(reqChan chan groupStore.Group, timeout time.Duration, gm *groupChat.Manager) { + jww.INFO.Print("Waiting for group request to be received.") + fmt.Println("Waiting for group request to be received.") + + select { + case grp := <-reqChan: + err := gm.JoinGroup(grp) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + + jww.INFO.Printf("Joined group: %s", grp.ID) + fmt.Printf("Joined group with name %q and message %q\n", + grp.Name, grp.InitMessage) + case <-time.NewTimer(timeout).C: + jww.INFO.Printf("Timed out after %s waiting for group request.", timeout) + fmt.Println("Timed out waiting for group request.") + return + } +} + +// leaveGroup leaves the group. +func leaveGroup(groupIdString string, gm *groupChat.Manager) { + groupID, _ := parseRecipient(groupIdString) + jww.INFO.Printf("Leaving group %s.", groupID) + + err := gm.LeaveGroup(groupID) + if err != nil { + jww.FATAL.Panicf("Failed to leave group %s: %+v", groupID, err) + } + + jww.INFO.Printf("Left group: %s", groupID) + fmt.Println("Left group.") +} + +// sendGroup send the message to the group. +func sendGroup(groupIdString string, msg []byte, gm *groupChat.Manager) { + groupID, _ := parseRecipient(groupIdString) + + jww.INFO.Printf("Sending to group %s message %q", groupID, msg) + + rid, err := gm.Send(groupID, msg) + if err != nil { + jww.FATAL.Panicf("Sending message to group %s: %+v", groupID, err) + } + + jww.INFO.Printf("Sent to group %s on round %d", groupID, rid) + fmt.Printf("Sent message %q to group.\n", msg) +} + +// messageWait waits for the given number of messages to be received on the +// groupChat.MessageReceive channel. +func messageWait(numMessages uint, timeout time.Duration, recChan chan groupChat.MessageReceive) { + jww.INFO.Printf("Waiting for %d group message(s) to be received.", numMessages) + fmt.Printf("Waiting for %d group message(s) to be received.\n", numMessages) + + for i := uint(0); i < numMessages; { + select { + case msg := <-recChan: + i++ + jww.INFO.Printf("Received group message %d/%d: %s", i, numMessages, msg) + fmt.Printf("Received group message: %q\n", msg.Payload) + case <-time.NewTimer(timeout).C: + jww.INFO.Printf("Timed out after %s waiting for group message.", timeout) + fmt.Printf("Timed out waiting for %d group message(s).\n", numMessages) + return + } + } +} + +// listGroups prints a list of all groups. +func listGroups(gm *groupChat.Manager) { + for i, gid := range gm.GetGroups() { + jww.INFO.Printf("Group %d: %s", i, gid) + } + + fmt.Printf("Printed list of %d groups.\n", gm.NumGroups()) +} + +// showGroup prints all the information of the group. +func showGroup(groupIdString string, gm *groupChat.Manager) { + groupID, _ := parseRecipient(groupIdString) + + grp, ok := gm.GetGroup(groupID) + if !ok { + jww.FATAL.Printf("Could not find group: %s", groupID) + } + + jww.INFO.Printf("Show group %#v", grp) + fmt.Printf("Got group with name %q and message %q\n", grp.Name, grp.InitMessage) +} + +// ReadLines returns each line in a file as a string. +func ReadLines(fileName string) []string { + file, err := os.Open(fileName) + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + defer file.Close() + + var res []string + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + res = append(res, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + jww.FATAL.Panicf(err.Error()) + } + return res +} + +func init() { + groupCmd.Flags().String("create", "", + "Create a group with from the list of contact file paths.") + err := viper.BindPFlag("create", groupCmd.Flags().Lookup("create")) + checkBindErr(err, "create") + + groupCmd.Flags().String("name", "Group Name", + "The name of the new group to create.") + err = viper.BindPFlag("name", groupCmd.Flags().Lookup("name")) + checkBindErr(err, "name") + + groupCmd.Flags().String("resend", "", + "Resend invites for all users in this group ID.") + err = viper.BindPFlag("resend", groupCmd.Flags().Lookup("resend")) + checkBindErr(err, "resend") + + groupCmd.Flags().Bool("join", false, + "Waits for group request joins the group.") + err = viper.BindPFlag("join", groupCmd.Flags().Lookup("join")) + checkBindErr(err, "join") + + groupCmd.Flags().String("leave", "", + "Leave this group ID.") + err = viper.BindPFlag("leave", groupCmd.Flags().Lookup("leave")) + checkBindErr(err, "leave") + + groupCmd.Flags().String("sendMessage", "", + "Send message to this group ID.") + err = viper.BindPFlag("sendMessage", groupCmd.Flags().Lookup("sendMessage")) + checkBindErr(err, "sendMessage") + + groupCmd.Flags().Uint("wait", 0, + "Waits for number of messages to be received.") + err = viper.BindPFlag("wait", groupCmd.Flags().Lookup("wait")) + checkBindErr(err, "wait") + + groupCmd.Flags().Duration("receiveTimeout", time.Minute, + "Amount of time to wait for a group request or message before timing out.") + err = viper.BindPFlag("receiveTimeout", groupCmd.Flags().Lookup("receiveTimeout")) + checkBindErr(err, "receiveTimeout") + + groupCmd.Flags().Bool("list", false, + "Prints list all groups to which this client belongs.") + err = viper.BindPFlag("list", groupCmd.Flags().Lookup("list")) + checkBindErr(err, "list") + + groupCmd.Flags().String("show", "", + "Prints the members of this group ID.") + err = viper.BindPFlag("show", groupCmd.Flags().Lookup("show")) + checkBindErr(err, "show") + + rootCmd.AddCommand(groupCmd) +} + +func checkBindErr(err error, key string) { + if err != nil { + jww.ERROR.Printf("viper.BindPFlag failed for %s: %+v", key, err) + } +} diff --git a/cmd/init.go b/cmd/init.go index b00f215475bfba0c513752b1d2fd01fd13cea5c8..9a10595e53848a70a14836d83cb345a87165b96a 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -11,8 +11,8 @@ package cmd import ( "fmt" "github.com/spf13/cobra" - "github.com/spf13/viper" jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" ) // initCmd creates a new user object with the given NDF @@ -31,7 +31,7 @@ var initCmd = &cobra.Command{ func init() { initCmd.Flags().StringP("userid-prefix", "", "", - "Desired prefix of userID to brute force when running init command. Prepend (?i) for case-insensitive. Only Base64 characters are valid.") + "Desired prefix of userID to brute force when running init command. Prepend (?i) for case-insensitive. Only Base64 characters are valid.") _ = viper.BindPFlag("userid-prefix", initCmd.Flags().Lookup("userid-prefix")) rootCmd.AddCommand(initCmd) diff --git a/cmd/root.go b/cmd/root.go index 5379cc08411e557fcc39e2d05996632eddfa259e..41cd73e10c7b68e224776771c31e8cfb1323fbe7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,7 +23,9 @@ import ( "gitlab.com/elixxir/crypto/contact" "gitlab.com/xx_network/primitives/id" "io/ioutil" + "log" "os" + "runtime/pprof" "strconv" "strings" "time" @@ -45,6 +47,14 @@ var rootCmd = &cobra.Command{ Short: "Runs a client for cMix anonymous communication platform", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { + profileOut := viper.GetString("profile-cpu") + if profileOut != "" { + f, err := os.Create(profileOut) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + pprof.StartCPUProfile(f) + } client := initClient() @@ -70,42 +80,20 @@ var rootCmd = &cobra.Command{ recipientContact = user.GetContact() } - // Set up reception handler - swboard := client.GetSwitchboard() - recvCh := make(chan message.Receive, 10000) - listenerID := swboard.RegisterChannel("DefaultCLIReceiver", - switchboard.AnyUser(), message.Text, recvCh) - jww.INFO.Printf("Message ListenerID: %v", listenerID) - - // Set up auth request handler, which simply prints the - // user id of the requester. - authMgr := client.GetAuthRegistrar() - authMgr.AddGeneralRequestCallback(printChanRequest) + confCh, recvCh := initClientCallbacks(client) - // If unsafe channels, add auto-acceptor + // The following block is used to check if the request from + // a channel authorization is from the recipient we intend in + // this run. authConfirmed := false - authMgr.AddGeneralConfirmCallback(func( - partner contact.Contact) { - jww.INFO.Printf("Channel Confirmed: %s", - partner.ID) - authConfirmed = recipientID.Cmp(partner.ID) - }) - if viper.GetBool("unsafe-channel-creation") { - authMgr.AddGeneralRequestCallback(func( - requestor contact.Contact, message string) { - jww.INFO.Printf("Channel Request: %s", - requestor.ID) - _, err := client.ConfirmAuthenticatedChannel( - requestor) - if err != nil { - jww.FATAL.Panicf("%+v", err) - } - authConfirmed = recipientID.Cmp( - requestor.ID) - }) - } + go func() { + for { + requestor := <-confCh + authConfirmed = recipientID.Cmp(requestor) + } + }() - _, err := client.StartNetworkFollower() + err := client.StartNetworkFollower(5 * time.Second) if err != nil { jww.FATAL.Panicf("%+v", err) } @@ -115,7 +103,7 @@ var rootCmd = &cobra.Command{ client.GetHealth().AddChannel(connected) waitUntilConnected(connected) - //err = client.RegisterForNotifications([]byte("dJwuGGX3KUyKldWK5PgQH8:APA91bFjuvimRc4LqOyMDiy124aLedifA8DhldtaB_b76ggphnFYQWJc_fq0hzQ-Jk4iYp2wPpkwlpE1fsOjs7XWBexWcNZoU-zgMiM0Mso9vTN53RhbXUferCbAiEylucEOacy9pniN")) + //err = client.RegisterForNotifications("dJwuGGX3KUyKldWK5PgQH8:APA91bFjuvimRc4LqOyMDiy124aLedifA8DhldtaB_b76ggphnFYQWJc_fq0hzQ-Jk4iYp2wPpkwlpE1fsOjs7XWBexWcNZoU-zgMiM0Mso9vTN53RhbXUferCbAiEylucEOacy9pniN") //if err != nil { // jww.FATAL.Panicf("Failed to register for notifications: %+v", err) //} @@ -186,6 +174,11 @@ var rootCmd = &cobra.Command{ " took %d seconds", scnt) } + // Delete this recipient + if viper.GetBool("delete-channel") { + deleteChannel(client, recipientID) + } + msg := message.Send{ Recipient: recipientID, Payload: []byte(msgBody), @@ -205,7 +198,7 @@ var rootCmd = &cobra.Command{ paramsUnsafe) roundTimeout = paramsUnsafe.Timeout } else { - roundIDs, _, err = client.SendE2E(msg, + roundIDs, _, _, err = client.SendE2E(msg, paramsE2E) roundTimeout = paramsE2E.Timeout } @@ -255,15 +248,57 @@ var rootCmd = &cobra.Command{ } fmt.Printf("Received %d\n", receiveCnt) - err = client.StopNetworkFollower(5 * time.Second) + err = client.StopNetworkFollower() if err != nil { jww.WARN.Printf( "Failed to cleanly close threads: %+v\n", err) } + if profileOut != "" { + pprof.StopCPUProfile() + } + }, } +func initClientCallbacks(client *api.Client) (chan *id.ID, + chan message.Receive) { + // Set up reception handler + swboard := client.GetSwitchboard() + recvCh := make(chan message.Receive, 10000) + listenerID := swboard.RegisterChannel("DefaultCLIReceiver", + switchboard.AnyUser(), message.Text, recvCh) + jww.INFO.Printf("Message ListenerID: %v", listenerID) + + // Set up auth request handler, which simply prints the + // user id of the requester. + authMgr := client.GetAuthRegistrar() + authMgr.AddGeneralRequestCallback(printChanRequest) + + // If unsafe channels, add auto-acceptor + authConfirmed := make(chan *id.ID, 10) + authMgr.AddGeneralConfirmCallback(func( + partner contact.Contact) { + jww.INFO.Printf("Channel Confirmed: %s", + partner.ID) + authConfirmed <- partner.ID + }) + if viper.GetBool("unsafe-channel-creation") { + authMgr.AddGeneralRequestCallback(func( + requestor contact.Contact, message string) { + jww.INFO.Printf("Channel Request: %s", + requestor.ID) + _, err := client.ConfirmAuthenticatedChannel( + requestor) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + authConfirmed <- requestor.ID + }) + } + return authConfirmed, recvCh +} + // Helper function which prints the round resuls func printRoundResults(allRoundsSucceeded, timedOut bool, rounds map[id.Round]api.RoundResult, roundIDs []id.Round, msg message.Send) { @@ -333,7 +368,6 @@ func createClient() *api.Client { err = api.NewClient(string(ndfJSON), storeDir, []byte(pass), regCode) } - } if err != nil { @@ -347,6 +381,8 @@ func createClient() *api.Client { netParams.E2EParams.NumRekeys = uint16( viper.GetUint("e2eNumReKeys")) netParams.ForceHistoricalRounds = viper.GetBool("forceHistoricalRounds") + netParams.FastPolling = !viper.GetBool("slowPolling") + netParams.ForceMessagePickupRetry = viper.GetBool("forceMessagePickupRetry") client, err := api.OpenClient(storeDir, []byte(pass), netParams) if err != nil { @@ -367,6 +403,13 @@ func initClient() *api.Client { netParams.E2EParams.NumRekeys = uint16( viper.GetUint("e2eNumReKeys")) netParams.ForceHistoricalRounds = viper.GetBool("forceHistoricalRounds") + netParams.FastPolling = viper.GetBool(" slowPolling") + netParams.ForceMessagePickupRetry = viper.GetBool("forceMessagePickupRetry") + if netParams.ForceMessagePickupRetry { + period := 3 * time.Second + jww.INFO.Printf("Setting Uncheck Round Period to %v", period) + netParams.UncheckRoundPeriod = period + } //load the client client, err := api.Login(storeDir, []byte(pass), netParams) @@ -418,6 +461,13 @@ func acceptChannel(client *api.Client, recipientID *id.ID) { } } +func deleteChannel(client *api.Client, partnerId *id.ID) { + err := client.DeleteContact(partnerId) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } +} + func printChanRequest(requestor contact.Contact, message string) { msg := fmt.Sprintf("Authentication channel request from: %s\n", requestor.ID) @@ -496,7 +546,7 @@ func waitUntilConnected(connected chan bool) { isConnected) break case <-timeoutTimer.C: - jww.FATAL.Panic("timeout on connection") + jww.FATAL.Panicf("timeout on connection after %s", waitTimeout*time.Second) } } @@ -611,34 +661,19 @@ func initLog(threshold uint, logPath string) { jww.INFO.Printf("log level set to: TRACE") jww.SetStdoutThreshold(jww.LevelTrace) jww.SetLogThreshold(jww.LevelTrace) + jww.SetFlags(log.LstdFlags | log.Lmicroseconds) } else if threshold == 1 { jww.INFO.Printf("log level set to: DEBUG") jww.SetStdoutThreshold(jww.LevelDebug) jww.SetLogThreshold(jww.LevelDebug) + jww.SetFlags(log.LstdFlags | log.Lmicroseconds) } else { - jww.INFO.Printf("log level set to: TRACE") + jww.INFO.Printf("log level set to: INFO") jww.SetStdoutThreshold(jww.LevelInfo) jww.SetLogThreshold(jww.LevelInfo) } } -func isValidUser(usr []byte) (bool, *id.ID) { - if len(usr) != id.ArrIDLen { - return false, nil - } - for _, b := range usr { - if b != 0 { - uid, err := id.Unmarshal(usr) - if err != nil { - jww.WARN.Printf("Could not unmarshal user: %s", err) - return false, nil - } - return true, uid - } - } - return false, nil -} - func askToCreateChannel(recipientID *id.ID) bool { for { fmt.Printf("This is the first time you have messaged %v, "+ @@ -746,6 +781,11 @@ func init() { viper.BindPFlag("accept-channel", rootCmd.Flags().Lookup("accept-channel")) + rootCmd.Flags().Bool("delete-channel", false, + "Delete the channel information for the corresponding recipient ID") + viper.BindPFlag("delete-channel", + rootCmd.Flags().Lookup("delete-channel")) + rootCmd.Flags().BoolP("send-auth-request", "", false, "Send an auth request to the specified destination and wait"+ "for confirmation") @@ -762,6 +802,17 @@ func init() { viper.BindPFlag("forceHistoricalRounds", rootCmd.Flags().Lookup("forceHistoricalRounds")) + // Network params + rootCmd.Flags().BoolP("slowPolling", "", false, + "Enables polling for unfiltered network updates with RSA signatures") + viper.BindPFlag("slowPolling", + rootCmd.Flags().Lookup("slowPolling")) + rootCmd.Flags().Bool("forceMessagePickupRetry", false, + "Enable a mechanism which forces a 50% chance of no message pickup, "+ + "instead triggering the message pickup retry mechanism") + viper.BindPFlag("forceMessagePickupRetry", + rootCmd.Flags().Lookup("forceMessagePickupRetry")) + // E2E Params defaultE2EParams := params.GetDefaultE2ESessionParams() rootCmd.Flags().UintP("e2eMinKeys", @@ -776,6 +827,10 @@ func init() { "", uint(defaultE2EParams.NumRekeys), "Number of rekeys reserved for rekey operations") viper.BindPFlag("e2eNumReKeys", rootCmd.Flags().Lookup("e2eNumReKeys")) + + rootCmd.Flags().String("profile-cpu", "", + "Enable cpu profiling to this file") + viper.BindPFlag("profile-cpu", rootCmd.Flags().Lookup("profile-cpu")) } // initConfig reads in config file and ENV variables if set. diff --git a/cmd/single.go b/cmd/single.go index 15f803b11bec0f775845fdfdfb9f8292c75ea2a7..85124107e7ddf46a07eef113ce158b2a15b5303e 100644 --- a/cmd/single.go +++ b/cmd/single.go @@ -62,7 +62,7 @@ var singleCmd = &cobra.Command{ }) } - _, err := client.StartNetworkFollower() + err := client.StartNetworkFollower(5 * time.Second) if err != nil { jww.FATAL.Panicf("%+v", err) } @@ -84,8 +84,10 @@ var singleCmd = &cobra.Command{ callbackChan <- responseCallbackChan{payload, c} } singleMng.RegisterCallback(tag, callback) - client.AddService(singleMng.StartProcesses) - + err = client.AddService(singleMng.StartProcesses) + if err != nil { + jww.FATAL.Panicf("Could not add single use process: %+v", err) + } timeout := viper.GetDuration("timeout") // If the send flag is set, then send a message diff --git a/cmd/ud.go b/cmd/ud.go index 38cbecb2cd1c6572cd140b158f435fe4291500cd..4dd6463cebea7a0f880c94977cda44ad058498e3 100644 --- a/cmd/ud.go +++ b/cmd/ud.go @@ -62,7 +62,7 @@ var udCmd = &cobra.Command{ }) } - _, err := client.StartNetworkFollower() + err := client.StartNetworkFollower(50 * time.Millisecond) if err != nil { jww.FATAL.Panicf("%+v", err) } @@ -74,7 +74,10 @@ var udCmd = &cobra.Command{ // Make single-use manager and start receiving process singleMng := single.NewManager(client) - client.AddService(singleMng.StartProcesses) + err = client.AddService(singleMng.StartProcesses) + if err != nil { + jww.FATAL.Panicf("Failed to add single use process: %+v", err) + } // Make user discovery manager userDiscoveryMgr, err := ud.NewManager(client, singleMng) @@ -86,6 +89,8 @@ var udCmd = &cobra.Command{ if userToRegister != "" { err = userDiscoveryMgr.Register(userToRegister) if err != nil { + fmt.Printf("Failed to register user %s: %s\n", + userToRegister, err.Error()) jww.FATAL.Panicf("Failed to register user %s: %+v", userToRegister, err) } } @@ -112,6 +117,8 @@ var udCmd = &cobra.Command{ for i := 0; i < len(newFacts); i++ { r, err := userDiscoveryMgr.SendRegisterFact(newFacts[i]) if err != nil { + fmt.Printf("Failed to register fact: %s\n", + newFacts[i]) jww.FATAL.Panicf("Failed to send register fact: %+v", err) } // TODO Store the code? @@ -123,6 +130,8 @@ var udCmd = &cobra.Command{ // TODO: Lookup code err = userDiscoveryMgr.SendConfirmFact(confirmID, confirmID) if err != nil { + fmt.Printf("Couldn't confirm fact: %s\n", + err.Error()) jww.FATAL.Panicf("%+v", err) } } @@ -175,8 +184,27 @@ var udCmd = &cobra.Command{ facts = append(facts, f) } + userToRemove := viper.GetString("remove") + if userToRemove != "" { + f, err := fact.NewFact(fact.Username, userToRemove) + if err != nil { + jww.FATAL.Panicf( + "Failed to create new fact: %+v", err) + } + err = userDiscoveryMgr.RemoveUser(f) + if err != nil { + fmt.Printf("Couldn't remove user %s\n", + userToRemove) + jww.FATAL.Panicf( + "Failed to remove user %s: %+v", + userToRemove, err) + } + fmt.Printf("Removed user from discovery: %s\n", + userToRemove) + } + if len(facts) == 0 { - err = client.StopNetworkFollower(10 * time.Second) + err = client.StopNetworkFollower() if err != nil { jww.WARN.Print(err) } @@ -195,8 +223,9 @@ var udCmd = &cobra.Command{ if err != nil { jww.FATAL.Panicf("%+v", err) } + time.Sleep(91 * time.Second) - err = client.StopNetworkFollower(90 * time.Second) + err = client.StopNetworkFollower() if err != nil { jww.WARN.Print(err) } @@ -209,6 +238,10 @@ func init() { "Register this user with user discovery.") _ = viper.BindPFlag("register", udCmd.Flags().Lookup("register")) + udCmd.Flags().StringP("remove", "", "", + "Remove this user with user discovery.") + _ = viper.BindPFlag("remove", udCmd.Flags().Lookup("remove")) + udCmd.Flags().String("addphone", "", "Add phone number to existing user registration.") _ = viper.BindPFlag("addphone", udCmd.Flags().Lookup("addphone")) @@ -240,10 +273,10 @@ func init() { } func printContact(c contact.Contact) { - jww.DEBUG.Printf("Printing client: %+v", c) + jww.DEBUG.Printf("Printing contact: %+v", c) cBytes := c.Marshal() if len(cBytes) == 0 { - jww.ERROR.Print("Marshaled client has a size of 0.") + jww.ERROR.Print("Marshaled contact has a size of 0.") } else { jww.DEBUG.Printf("Printing marshaled contact of size %d.", len(cBytes)) } diff --git a/cmd/version.go b/cmd/version.go index 67b545b53c3ed7e36e85abccbeadf817bc52dc25..f01e1f873e867f17b27ef7913ad1ead7e1d69315 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -18,7 +18,7 @@ import ( ) // Change this value to set the version for this build -const currentVersion = "2.5.0" +const currentVersion = "2.8.0" func Version() string { out := fmt.Sprintf("Elixxir Client v%s -- %s\n\n", api.SEMVER, diff --git a/go.mod b/go.mod index 7142c43f2d9fa81eacd6878befdc445e5d8f8a1f..ba23f84966c59216c9a53dc940e7a2a56da16e65 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 - github.com/golang/protobuf v1.4.3 + github.com/golang/protobuf v1.5.2 github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/magiconair/properties v1.8.4 // indirect github.com/mitchellh/mapstructure v1.4.0 // indirect @@ -17,20 +17,17 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/viper v1.7.1 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 - gitlab.com/elixxir/comms v0.0.4-0.20210506225017-37485f5ba063 - gitlab.com/elixxir/crypto v0.0.7-0.20210506223047-3196e4301110 + gitlab.com/elixxir/comms v0.0.4-0.20210914232530-b0e625b49552 + gitlab.com/elixxir/crypto v0.0.7-0.20210914232212-42464d16fff3 gitlab.com/elixxir/ekv v0.1.5 - gitlab.com/elixxir/primitives v0.0.3-0.20210504210415-34cf31c2816e - gitlab.com/xx_network/comms v0.0.4-0.20210505205155-48daa8448ad7 - gitlab.com/xx_network/crypto v0.0.5-0.20210504210244-9ddabbad25fd - gitlab.com/xx_network/primitives v0.0.4-0.20210504205835-db68f11de78a + gitlab.com/elixxir/primitives v0.0.3-0.20210914232041-6edc82b7e58e + gitlab.com/xx_network/comms v0.0.4-0.20210914232007-b82fc7baa23c + gitlab.com/xx_network/crypto v0.0.5-0.20210914231859-c309efac46c4 + gitlab.com/xx_network/primitives v0.0.4-0.20210913211733-42dc24dd47df golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 - golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect + golang.org/x/net v0.0.0-20210525063256-abc453219eb5 google.golang.org/genproto v0.0.0-20210105202744-fe13368bc0e1 // indirect - google.golang.org/grpc v1.34.0 // indirect - google.golang.org/protobuf v1.26.0-rc.1 + google.golang.org/protobuf v1.26.0 gopkg.in/ini.v1 v1.62.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) - -replace google.golang.org/grpc => github.com/grpc/grpc-go v1.27.1 diff --git a/go.sum b/go.sum index c02d426f81e92c2cecddcaabb483586a412caa9e..14da1adea1978dd4659c4b3ed18e93bc6c1f4ea7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= @@ -10,7 +11,6 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -27,6 +27,9 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -39,10 +42,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -56,7 +61,6 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4= github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -65,31 +69,31 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -99,8 +103,6 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc/grpc-go v1.27.1 h1:EluyjU5nlbuNJSEktNl600PIpzbO2OcvZWfWV1jFvKM= -github.com/grpc/grpc-go v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -144,7 +146,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3HnVhDalIyZ4RJAyVfDrbnfFeJoC4= github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea/go.mod h1:w4pGU9PkiX2hAWyF0yuHEHmYTQFAd6WHzp6+IY7JVjE= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -160,17 +161,13 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks= github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nyaruka/phonenumbers v1.0.60 h1:nnAcNwmZflhegiImm6MkvjlRRyoaSw1ox/jGPAewWTg= -github.com/nyaruka/phonenumbers v1.0.60/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -232,68 +229,58 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0= +github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= +github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4= +github.com/ttacon/libphonenumber v1.2.1/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/zeebo/assert v0.0.0-20181109011804-10f827ce2ed6/go.mod h1:yssERNPivllc1yU3BvpjYI5BUW+zglcz6QWqeVRL5t0= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/blake3 v0.0.4 h1:vtZ4X8B2lKXZFg2Xyg6Wo36mvmnJvc2VQYTtA4RDCkI= github.com/zeebo/blake3 v0.0.4/go.mod h1:YOZo8A49yNqM0X/Y+JmDUZshJWLt1laHsNSn5ny2i34= github.com/zeebo/blake3 v0.1.1 h1:Nbsts7DdKThRHHd+YNlqiGlRqGEF2bE2eXN+xQ1hsEs= github.com/zeebo/blake3 v0.1.1/go.mod h1:G9pM4qQwjRzF1/v7+vabMj/c5mWpGZ2Wzo3Eb4z0pb4= -github.com/zeebo/pcg v0.0.0-20181207190024-3cdc6b625a05 h1:4pW5fMvVkrgkMXdvIsVRRTs69DWYA8uNNQsu1stfVKU= github.com/zeebo/pcg v0.0.0-20181207190024-3cdc6b625a05/go.mod h1:Gr+78ptB0MwXxm//LBaEvBiaXY7hXJ6KGe2V32X2F6E= github.com/zeebo/pcg v1.0.0 h1:dt+dx+HvX8g7Un32rY9XWoYnd0NmKmrIzpHF7qiTDj0= github.com/zeebo/pcg v1.0.0/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 h1:Gi6rj4mAlK0BJIk1HIzBVMjWNjIUfstrsXC2VqLYPcA= gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k= -gitlab.com/elixxir/comms v0.0.4-0.20210505205202-1d4c18a7fcb2 h1:8aL4V7FaKkDb5iPdJ1rlFFhrHrLWUtbmBjw4BysXzEA= -gitlab.com/elixxir/comms v0.0.4-0.20210505205202-1d4c18a7fcb2/go.mod h1:VN0fNE7GFMrkZwRGnqA7fNNRAXDA4CCP6su/FQQ68RI= -gitlab.com/elixxir/comms v0.0.4-0.20210506161214-6371db79ce6f h1:0hvU+6Y+JGFnBu8ZSMk0ukNuYg+GAnVKD8Yo4VwSdao= -gitlab.com/elixxir/comms v0.0.4-0.20210506161214-6371db79ce6f/go.mod h1:7ff+A4Nom55mKiRW7qWsN7LDjGay4OZwiaaIVXZ4hdk= -gitlab.com/elixxir/comms v0.0.4-0.20210506225017-37485f5ba063 h1:9A2FT1IDzb9E0HaEEcRMAZEVRM4SMXpklYvS6owSyIk= -gitlab.com/elixxir/comms v0.0.4-0.20210506225017-37485f5ba063/go.mod h1:KHV+lNKhcsXoor1KQizUHhCuHugnquldrAR8UU5PNKU= -gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4 h1:28ftZDeYEko7xptCZzeFWS1Iam95dj46TWFVVlKmw6A= +gitlab.com/elixxir/comms v0.0.4-0.20210914232530-b0e625b49552 h1:RgyEauSNIAlGo8Ynec01N3GKWWNNmemPLaMW899sCw0= +gitlab.com/elixxir/comms v0.0.4-0.20210914232530-b0e625b49552/go.mod h1:aXUf9T/1ddQYZ1+/fyhqvqHHxzu2QWB0XdODygWqzi8= gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4/go.mod h1:ucm9SFKJo+K0N2GwRRpaNr+tKXMIOVWzmyUD0SbOu2c= -gitlab.com/elixxir/crypto v0.0.3 h1:znCt/x2bL4y8czTPaaFkwzdgSgW3BJc/1+dxyf1jqVw= gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA= -gitlab.com/elixxir/crypto v0.0.7-0.20210504210535-3077ddf9984d h1:E16E+gM2jJosFc8YmT2ISGxcfBThG2KAgsAcQXtxSIc= -gitlab.com/elixxir/crypto v0.0.7-0.20210504210535-3077ddf9984d/go.mod h1:pbq80k+R7XXvjyWDqanD2eCJi1ClfESdKS0K8NndoLs= -gitlab.com/elixxir/crypto v0.0.7-0.20210506223047-3196e4301110 h1:4KWUbx1RI5TABBM2omWl5MLW16dwySglz895X2rhSFQ= -gitlab.com/elixxir/crypto v0.0.7-0.20210506223047-3196e4301110/go.mod h1:pbq80k+R7XXvjyWDqanD2eCJi1ClfESdKS0K8NndoLs= +gitlab.com/elixxir/crypto v0.0.7-0.20210914232212-42464d16fff3 h1:IK6a8xloclNFxqYTzyCuiqP4cynmtxjzUuB8aNJtHy4= +gitlab.com/elixxir/crypto v0.0.7-0.20210914232212-42464d16fff3/go.mod h1:3ZGqugc0m5Y236n6mDyfsEy6j8C1HRlqTRj9I7t8k/w= gitlab.com/elixxir/ekv v0.1.5 h1:R8M1PA5zRU1HVnTyrtwybdABh7gUJSCvt1JZwUSeTzk= gitlab.com/elixxir/ekv v0.1.5/go.mod h1:e6WPUt97taFZe5PFLPb1Dupk7tqmDCTQu1kkstqJvw4= gitlab.com/elixxir/primitives v0.0.0-20200731184040-494269b53b4d/go.mod h1:OQgUZq7SjnE0b+8+iIAT2eqQF+2IFHn73tOo+aV11mg= gitlab.com/elixxir/primitives v0.0.0-20200804170709-a1896d262cd9/go.mod h1:p0VelQda72OzoUckr1O+vPW0AiFe0nyKQ6gYcmFSuF8= gitlab.com/elixxir/primitives v0.0.0-20200804182913-788f47bded40/go.mod h1:tzdFFvb1ESmuTCOl1z6+yf6oAICDxH2NPUemVgoNLxc= -gitlab.com/elixxir/primitives v0.0.1 h1:q61anawANlNAExfkeQEE1NCsNih6vNV1FFLoUQX6txQ= gitlab.com/elixxir/primitives v0.0.1/go.mod h1:kNp47yPqja2lHSiS4DddTvFpB/4D9dB2YKnw5c+LJCE= -gitlab.com/elixxir/primitives v0.0.3-0.20210504210415-34cf31c2816e h1:6Z5qAqI/xoWYPMVcItUDYEkOe84YWS1FJa+qjWGcJ2c= -gitlab.com/elixxir/primitives v0.0.3-0.20210504210415-34cf31c2816e/go.mod h1:4pNgiFEQQ11hHCXBRQJN1w9AIuKa1HTlPTxs9UYOXFA= +gitlab.com/elixxir/primitives v0.0.3-0.20210914232041-6edc82b7e58e h1:h7i+Ld2pTlQ0oT2/KLsF6pVBI8D5ir5ZAP4RQBd/CzM= +gitlab.com/elixxir/primitives v0.0.3-0.20210914232041-6edc82b7e58e/go.mod h1:vTeq2GfYvYydmjhiVC188XeA7GBuUzJ1EZYc1hRwLZs= gitlab.com/xx_network/comms v0.0.0-20200805174823-841427dd5023/go.mod h1:owEcxTRl7gsoM8c3RQ5KAm5GstxrJp5tn+6JfQ4z5Hw= -gitlab.com/xx_network/comms v0.0.4-0.20210505204621-a93ded09b1ff/go.mod h1:RkNZ0CjeXKRhEFdUeAdCAF6QuK8sO1j2bUg9oqK0OEA= -gitlab.com/xx_network/comms v0.0.4-0.20210505205155-48daa8448ad7 h1:0oQfe8YZ51kYKEj1w9UN2ls0Kp2AHRO6CUbkF/T/UH4= -gitlab.com/xx_network/comms v0.0.4-0.20210505205155-48daa8448ad7/go.mod h1:RkNZ0CjeXKRhEFdUeAdCAF6QuK8sO1j2bUg9oqK0OEA= +gitlab.com/xx_network/comms v0.0.4-0.20210914232007-b82fc7baa23c h1:qoSX9V9rdSIuXXUQgZfY7Hkim9bYsg8XORVaOI8XD5U= +gitlab.com/xx_network/comms v0.0.4-0.20210914232007-b82fc7baa23c/go.mod h1:Api5Gu+sx1I43THNGKtZOXItJpoGgCLT8KoP7vnLLSc= gitlab.com/xx_network/crypto v0.0.3/go.mod h1:DF2HYvvCw9wkBybXcXAgQMzX+MiGbFPjwt3t17VRqRE= -gitlab.com/xx_network/crypto v0.0.4 h1:lpKOL5mTJ2awWMfgBy30oD/UvJVrWZzUimSHlOdZZxo= gitlab.com/xx_network/crypto v0.0.4/go.mod h1:+lcQEy+Th4eswFgQDwT0EXKp4AXrlubxalwQFH5O0Mk= -gitlab.com/xx_network/crypto v0.0.5-0.20210504210244-9ddabbad25fd h1:jSY1ogxa2/MXthD8jadGr7IYBL4vXQID3VZp1g0GWec= -gitlab.com/xx_network/crypto v0.0.5-0.20210504210244-9ddabbad25fd/go.mod h1:bAqc5+q2K9OXWceHkZX+VnneSKlsSeg+G98O5S4Y2cA= +gitlab.com/xx_network/crypto v0.0.5-0.20210914231859-c309efac46c4 h1:33a7mKkqsZUxzmG35hA1CaVZzyPZYe23if1GxQ99tpI= +gitlab.com/xx_network/crypto v0.0.5-0.20210914231859-c309efac46c4/go.mod h1:g0Lr/aM0KZqneIvSsKEn7zfNmorhq0R7IQn8Yh8fqbs= gitlab.com/xx_network/primitives v0.0.0-20200803231956-9b192c57ea7c/go.mod h1:wtdCMr7DPePz9qwctNoAUzZtbOSHSedcK++3Df3psjA= -gitlab.com/xx_network/primitives v0.0.0-20200804183002-f99f7a7284da h1:CCVslUwNC7Ul7NG5nu3ThGTSVUt1TxNRX+47f5TUwnk= gitlab.com/xx_network/primitives v0.0.0-20200804183002-f99f7a7284da/go.mod h1:OK9xevzWCaPO7b1wiluVJGk7R5ZsuC7pHY5hteZFQug= -gitlab.com/xx_network/primitives v0.0.2 h1:r45yKenJ9e7PylI1ZXJ1Es09oYNaYXjxVy9+uYlwo7Y= gitlab.com/xx_network/primitives v0.0.2/go.mod h1:cs0QlFpdMDI6lAo61lDRH2JZz+3aVkHy+QogOB6F/qc= -gitlab.com/xx_network/primitives v0.0.4-0.20210504205835-db68f11de78a h1:Op+Dfm/Swtrs6Lgo/ro28SrCrftrfQtK9a3/EOoXYAo= -gitlab.com/xx_network/primitives v0.0.4-0.20210504205835-db68f11de78a/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE= -gitlab.com/xx_network/ring v0.0.2 h1:TlPjlbFdhtJrwvRgIg4ScdngMTaynx/ByHBRZiXCoL0= -gitlab.com/xx_network/ring v0.0.2/go.mod h1:aLzpP2TiZTQut/PVHR40EJAomzugDdHXetbieRClXIM= +gitlab.com/xx_network/primitives v0.0.4-0.20210913211733-42dc24dd47df h1:ukBhRj5o0gkbYos9+7SblXaxnMaty3ugQhWqb2BDcSE= +gitlab.com/xx_network/primitives v0.0.4-0.20210913211733-42dc24dd47df/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE= +gitlab.com/xx_network/ring v0.0.3-0.20210527191221-ce3f170aabd5 h1:FY+4Rh1Q2rgLyv10aKJjhWApuKRCR/054XhreudfAvw= +gitlab.com/xx_network/ring v0.0.3-0.20210527191221-ce3f170aabd5/go.mod h1:aLzpP2TiZTQut/PVHR40EJAomzugDdHXetbieRClXIM= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -306,16 +293,11 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200707235045-ab33eee955e0 h1:eIYIE7EC5/Wv5Kbz8bJPaq+TN3kq3W8S+LSm62vM0DY= golang.org/x/crypto v0.0.0-20200707235045-ab33eee955e0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= @@ -326,6 +308,7 @@ golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm0 golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -337,6 +320,7 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -349,19 +333,20 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -377,29 +362,26 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200917073148-efd3b9a0ff20/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201014080544-cc95f250f6bc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d h1:jbzgAvDZn8aEnytae+4ou0J0GwFZoHR0hOrTg4qH8GA= golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -416,7 +398,6 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -425,6 +406,7 @@ google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= @@ -440,24 +422,33 @@ google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210105202744-fe13368bc0e1 h1:Zk6zlGXdtYdcY5TL+VrbTfmifvk3VvsXopCpszsHPBA= google.golang.org/genproto v0.0.0-20210105202744-fe13368bc0e1/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -471,7 +462,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/groupChat/gcMessages.pb.go b/groupChat/gcMessages.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..37b23167e412866c8012fd9f8c95bd99adab88b3 --- /dev/null +++ b/groupChat/gcMessages.pb.go @@ -0,0 +1,117 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: groupChat/gcMessages.proto + +package groupChat + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +// Request to join the group sent from leader to all members. +type Request struct { + Name []byte `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + IdPreimage []byte `protobuf:"bytes,2,opt,name=idPreimage,proto3" json:"idPreimage,omitempty"` + KeyPreimage []byte `protobuf:"bytes,3,opt,name=keyPreimage,proto3" json:"keyPreimage,omitempty"` + Members []byte `protobuf:"bytes,4,opt,name=members,proto3" json:"members,omitempty"` + Message []byte `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Request) Reset() { *m = Request{} } +func (m *Request) String() string { return proto.CompactTextString(m) } +func (*Request) ProtoMessage() {} +func (*Request) Descriptor() ([]byte, []int) { + return fileDescriptor_49d0b7a6ffb7e279, []int{0} +} + +func (m *Request) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Request.Unmarshal(m, b) +} +func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Request.Marshal(b, m, deterministic) +} +func (m *Request) XXX_Merge(src proto.Message) { + xxx_messageInfo_Request.Merge(m, src) +} +func (m *Request) XXX_Size() int { + return xxx_messageInfo_Request.Size(m) +} +func (m *Request) XXX_DiscardUnknown() { + xxx_messageInfo_Request.DiscardUnknown(m) +} + +var xxx_messageInfo_Request proto.InternalMessageInfo + +func (m *Request) GetName() []byte { + if m != nil { + return m.Name + } + return nil +} + +func (m *Request) GetIdPreimage() []byte { + if m != nil { + return m.IdPreimage + } + return nil +} + +func (m *Request) GetKeyPreimage() []byte { + if m != nil { + return m.KeyPreimage + } + return nil +} + +func (m *Request) GetMembers() []byte { + if m != nil { + return m.Members + } + return nil +} + +func (m *Request) GetMessage() []byte { + if m != nil { + return m.Message + } + return nil +} + +func init() { + proto.RegisterType((*Request)(nil), "gcRequestMessages.Request") +} + +func init() { + proto.RegisterFile("groupChat/gcMessages.proto", fileDescriptor_49d0b7a6ffb7e279) +} + +var fileDescriptor_49d0b7a6ffb7e279 = []byte{ + // 186 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4a, 0x2f, 0xca, 0x2f, + 0x2d, 0x70, 0xce, 0x48, 0x2c, 0xd1, 0x4f, 0x4f, 0xf6, 0x4d, 0x2d, 0x2e, 0x4e, 0x4c, 0x4f, 0x2d, + 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x4c, 0x4f, 0x0e, 0x4a, 0x2d, 0x2c, 0x4d, 0x2d, + 0x2e, 0x81, 0x49, 0x28, 0x4d, 0x66, 0xe4, 0x62, 0x87, 0x8a, 0x09, 0x09, 0x71, 0xb1, 0xe4, 0x25, + 0xe6, 0xa6, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0xf0, 0x04, 0x81, 0xd9, 0x42, 0x72, 0x5c, 0x5c, 0x99, + 0x29, 0x01, 0x45, 0xa9, 0x99, 0xb9, 0x89, 0xe9, 0xa9, 0x12, 0x4c, 0x60, 0x19, 0x24, 0x11, 0x21, + 0x05, 0x2e, 0xee, 0xec, 0xd4, 0x4a, 0xb8, 0x02, 0x66, 0xb0, 0x02, 0x64, 0x21, 0x21, 0x09, 0x2e, + 0xf6, 0xdc, 0xd4, 0xdc, 0xa4, 0xd4, 0xa2, 0x62, 0x09, 0x16, 0xb0, 0x2c, 0x8c, 0x0b, 0x91, 0x01, + 0xbb, 0x43, 0x82, 0x15, 0x26, 0x03, 0xe6, 0x3a, 0xa9, 0x46, 0x29, 0xa7, 0x67, 0x96, 0xe4, 0x24, + 0x26, 0xe9, 0x25, 0xe7, 0xe7, 0xea, 0xa7, 0xe6, 0x64, 0x56, 0x54, 0x64, 0x16, 0xe9, 0x27, 0xe7, + 0x64, 0xa6, 0xe6, 0x95, 0xe8, 0xc3, 0x3d, 0x98, 0xc4, 0x06, 0xf6, 0x96, 0x31, 0x20, 0x00, 0x00, + 0xff, 0xff, 0x6e, 0x63, 0x77, 0xd1, 0xf4, 0x00, 0x00, 0x00, +} diff --git a/groupChat/gcMessages.proto b/groupChat/gcMessages.proto new file mode 100644 index 0000000000000000000000000000000000000000..7ce1f3b40c5f34f61367dcec2e477dd8040faae5 --- /dev/null +++ b/groupChat/gcMessages.proto @@ -0,0 +1,20 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +syntax = "proto3"; +package gcRequestMessages; +option go_package = "gitlab.com/elixxir/client/groupChat"; + + +// Request to join the group sent from leader to all members. +message Request { + bytes name = 1; + bytes idPreimage = 2; + bytes keyPreimage = 3; + bytes members = 4; + bytes message = 5; +} \ No newline at end of file diff --git a/groupChat/generateProto.sh b/groupChat/generateProto.sh new file mode 100644 index 0000000000000000000000000000000000000000..43968a4aa112270ffb38ea9a2c5da91e309871f1 --- /dev/null +++ b/groupChat/generateProto.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +protoc --go_out=paths=source_relative:. groupChat/gcMessages.proto diff --git a/groupChat/group.go b/groupChat/group.go new file mode 100644 index 0000000000000000000000000000000000000000..5d67d19f1e52f80cf1628f2deece083b4d8f4568 --- /dev/null +++ b/groupChat/group.go @@ -0,0 +1,70 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +// Group chat is used to communicate the same content with multiple clients over +// cMix. A group chat is controlled by a group leader who creates the group, +// defines all group keys, and is responsible for key rotation. To create a +// group, the group leader must have an authenticated channel with all members +// of the group. +// +// Once a group is created, neither the leader nor other members can add or +// remove users to the group. Only members can leave a group themselves. +// +// When a message is sent to the group, the sender will send an individual +// message to every member of the group. + +package groupChat + +import ( + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/xx_network/primitives/id" +) + +// GroupChat is used to send and receive cMix messages to/from multiple users. +type GroupChat interface { + // MakeGroup sends GroupChat requests to all members over an authenticated + // channel. The leader of a GroupChat must have an authenticated channel + // with each member of the GroupChat to add them to the GroupChat. It blocks + // until all the GroupChat requests are sent. Returns the new group and the + // round IDs the requests were sent on. Returns an error if at least one + // request to a member fails to send. Also returns the status of the sent + // requests. + MakeGroup(membership []*id.ID, name, message []byte) (gs.Group, []id.Round, + RequestStatus, error) + + // ResendRequest allows a GroupChat request to be sent again. It returns + // the rounds that the requests were sent on and the status of the send. + ResendRequest(groupID *id.ID) ([]id.Round, RequestStatus, error) + + // JoinGroup allows a user to accept a GroupChat request and stores the + // GroupChat as active to allow receiving and sending of messages from/to + // the GroupChat. A user can only join a GroupChat once. + JoinGroup(g gs.Group) error + + // LeaveGroup removes a group from a list of groups the user is a part of. + LeaveGroup(groupID *id.ID) error + + // Send sends a message to all GroupChat members using Client.SendManyCMIX. + // The send fails if the message is too long. + Send(groupID *id.ID, message []byte) (id.Round, error) + + // GetGroups returns a list of all registered GroupChat IDs. + GetGroups() []*id.ID + + // GetGroup returns the group with the matching ID or returns false if none + // exist. + GetGroup(groupID *id.ID) (gs.Group, bool) + + // NumGroups returns the number of groups the user is a part of. + NumGroups() int +} + +// RequestCallback is called when a GroupChat request is received. +type RequestCallback func(g gs.Group) + +// ReceiveCallback is called when a GroupChat message is received. +type ReceiveCallback func(msg MessageReceive) diff --git a/groupChat/groupStore/dhKeyList.go b/groupChat/groupStore/dhKeyList.go new file mode 100644 index 0000000000000000000000000000000000000000..50c405c426d7a27f2f99767208d2b2915a4f93b5 --- /dev/null +++ b/groupChat/groupStore/dhKeyList.go @@ -0,0 +1,139 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupStore + +import ( + "bytes" + "encoding/binary" + "github.com/pkg/errors" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/diffieHellman" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/xx_network/primitives/id" + "sort" + "strings" +) + +// Error messages. +const ( + idUnmarshalErr = "failed to unmarshal member ID: %+v" + dhKeyDecodeErr = "failed to decode member DH key: %+v" +) + +type DhKeyList map[id.ID]*cyclic.Int + +// GenerateDhKeyList generates the symmetric/DH key between the user and all +// group members. +func GenerateDhKeyList(userID *id.ID, privKey *cyclic.Int, + members group.Membership, grp *cyclic.Group) DhKeyList { + dkl := make(DhKeyList, len(members)-1) + + for _, m := range members { + if !userID.Cmp(m.ID) { + dkl.Add(privKey, m, grp) + } + } + + return dkl +} + +// Add generates DH key between the user and the group member. The +func (dkl DhKeyList) Add(privKey *cyclic.Int, m group.Member, grp *cyclic.Group) { + dkl[*m.ID] = diffieHellman.GenerateSessionKey(privKey, m.DhKey, grp) +} + +// DeepCopy returns a copy of the DhKeyList. +func (dkl DhKeyList) DeepCopy() DhKeyList { + newDkl := make(DhKeyList, len(dkl)) + for uid, key := range dkl { + newDkl[uid] = key.DeepCopy() + } + return newDkl +} + +// Serialize serializes the DhKeyList and returns the byte slice. +func (dkl DhKeyList) Serialize() []byte { + buff := bytes.NewBuffer(nil) + + for uid, key := range dkl { + // Write ID + buff.Write(uid.Marshal()) + + // Write DH key length + b := make([]byte, 8) + keyBytes := key.BinaryEncode() + binary.LittleEndian.PutUint64(b, uint64(len(keyBytes))) + buff.Write(b) + + // Write DH key + buff.Write(keyBytes) + } + + return buff.Bytes() +} + +// DeserializeDhKeyList deserializes the bytes into a DhKeyList. +func DeserializeDhKeyList(data []byte) (DhKeyList, error) { + if len(data) == 0 { + return nil, nil + } + + buff := bytes.NewBuffer(data) + dkl := make(DhKeyList) + + for n := buff.Next(id.ArrIDLen); len(n) == id.ArrIDLen; n = buff.Next(id.ArrIDLen) { + // Read and unmarshal ID + uid, err := id.Unmarshal(n) + if err != nil { + return nil, errors.Errorf(idUnmarshalErr, err) + } + + // Get length of DH key + keyLen := int(binary.LittleEndian.Uint64(buff.Next(8))) + + // Read and decode DH key + key := &cyclic.Int{} + err = key.BinaryDecode(buff.Next(keyLen)) + if err != nil { + return nil, errors.Errorf(dhKeyDecodeErr, err) + } + + dkl[*uid] = key + } + + return dkl, nil +} + +// GoString returns all the elements in the DhKeyList as text in sorted order. +// This functions satisfies the fmt.GoStringer interface. +func (dkl DhKeyList) GoString() string { + str := make([]string, 0, len(dkl)) + + unsorted := make([]struct { + uid *id.ID + key *cyclic.Int + }, 0, len(dkl)) + + for uid, key := range dkl { + unsorted = append(unsorted, struct { + uid *id.ID + key *cyclic.Int + }{uid: uid.DeepCopy(), key: key.DeepCopy()}) + } + + sort.Slice(unsorted, func(i, j int) bool { + return bytes.Compare(unsorted[i].uid.Bytes(), + unsorted[j].uid.Bytes()) == -1 + }) + + for _, val := range unsorted { + str = append(str, val.uid.String()+": "+val.key.Text(10)) + } + + return "{" + strings.Join(str, ", ") + "}" +} diff --git a/groupChat/groupStore/dhKeyList_test.go b/groupChat/groupStore/dhKeyList_test.go new file mode 100644 index 0000000000000000000000000000000000000000..df8d9f1dd4923970d1b97375b9e1bf48591378be --- /dev/null +++ b/groupChat/groupStore/dhKeyList_test.go @@ -0,0 +1,93 @@ +package groupStore + +import ( + "math/rand" + "reflect" + "strings" + "testing" +) + +// // Unit test of GenerateDhKeyList. +// func TestGenerateDhKeyList(t *testing.T) { +// prng := rand.New(rand.NewSource(42)) +// grp := getGroup() +// userID := id.NewIdFromString("userID", id.User, t) +// privKey := grp.NewInt(42) +// pubKey := grp.ExpG(privKey, grp.NewInt(1)) +// members := createMembership(prng, 10, t) +// members[2].ID = userID +// members[2].DhKey = pubKey +// +// dkl := GenerateDhKeyList(userID, privKey, members, grp) +// +// t.Log(dkl) +// } + +// Unit test of DhKeyList.DeepCopy. +func TestDhKeyList_DeepCopy(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + dkl := createDhKeyList(prng, 10, t) + newDkl := dkl.DeepCopy() + + if !reflect.DeepEqual(dkl, newDkl) { + t.Errorf("DeepCopy() failed to return a copy of the original."+ + "\nexpected: %#v\nrecevied: %#v", dkl, newDkl) + } + + if &dkl == &newDkl { + t.Errorf("DeepCopy returned a copy of the pointer."+ + "\nexpected: %p\nreceived: %p", &dkl, &newDkl) + } +} + +// Tests that a DhKeyList that is serialized and deserialized matches the +// original. +func TestDhKeyList_Serialize_DeserializeDhKeyList(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + dkl := createDhKeyList(prng, 10, t) + + data := dkl.Serialize() + newDkl, err := DeserializeDhKeyList(data) + if err != nil { + t.Errorf("DeserializeDhKeyList returned an error: %+v", err) + } + + if !reflect.DeepEqual(dkl, newDkl) { + t.Errorf("Failed to serialize and deserialize DhKeyList."+ + "\nexpected: %#v\nreceived: %#v", dkl, newDkl) + } +} + +// Error path: an error is returned when DeserializeDhKeyList encounters invalid +// cyclic int. +func TestDeserializeDhKeyList_DhKeyBinaryDecodeError(t *testing.T) { + expectedErr := strings.SplitN(dhKeyDecodeErr, "%", 2)[0] + + _, err := DeserializeDhKeyList(make([]byte, 41)) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("DeserializeDhKeyList failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of DhKeyList.GoString. +func TestDhKeyList_GoString(t *testing.T) { + grp := createTestGroup(rand.New(rand.NewSource(42)), t) + expected := "{Grcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0YD: 6342989043... in GRP: 6SsQ/HAHUn..., QCxg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiAD: 2579328386... in GRP: 6SsQ/HAHUn..., invD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHAD: 1688982497... in GRP: 6SsQ/HAHUn..., o54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQgD: 5552242738... in GRP: 6SsQ/HAHUn..., wRYCP6iJdLrAyv2a0FaSsTYZ5ziWTf3Hno1TQ3NmHP0D: 2812078897... in GRP: 6SsQ/HAHUn..., 15ufnw07pVsMwNYUTIiFNYQay+BwmwdYCD9h03W8ArQD: 2588260662... in GRP: 6SsQ/HAHUn..., 3RqsBM4ux44bC6+uiBuCp1EQikLtPJA8qkNGWnhiBhYD: 4967151805... in GRP: 6SsQ/HAHUn..., 55ai4SlwXic/BckjJoKOKwVuOBdljhBhSYlH/fNEQQ4D: 3187530437... in GRP: 6SsQ/HAHUn..., 9PkZKU50joHnnku9b+NM3LqEPujWPoxP/hzr6lRtj6wD: 4832738218... in GRP: 6SsQ/HAHUn...}" + + if grp.DhKeys.GoString() != expected { + t.Errorf("GoString failed to return the expected string."+ + "\nexpected: %s\nreceived: %s", expected, grp.DhKeys.GoString()) + } +} + +// Tests that DhKeyList.GoString. returns the expected string for a nil map. +func TestDhKeyList_GoString_NilMap(t *testing.T) { + dkl := DhKeyList{} + expected := "{}" + + if dkl.GoString() != expected { + t.Errorf("GoString failed to return the expected string."+ + "\nexpected: %s\nreceived: %s", expected, dkl.GoString()) + } +} diff --git a/groupChat/groupStore/group.go b/groupChat/groupStore/group.go new file mode 100644 index 0000000000000000000000000000000000000000..98d0b489de808cf1ed5ca31818f4bc7f203fe878 --- /dev/null +++ b/groupChat/groupStore/group.go @@ -0,0 +1,235 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupStore + +import ( + "bytes" + "encoding/binary" + "fmt" + "github.com/pkg/errors" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "strings" +) + +// Storage values. +const ( + // Key that is prepended to group ID to create a unique key to identify a + // Group in storage. + groupStorageKey = "GroupChat/" + groupStoreVersion = 0 +) + +// Error messages. +const ( + kvGetGroupErr = "failed to get group %s from storage: %+v" + membershipErr = "failed to deserialize member list: %+v" + dhKeyListErr = "failed to deserialize DH key list: %+v" +) + +// Group contains the membership list, the cryptographic information, and the +// identifying information of a group chat. +type Group struct { + Name []byte // Name of the group set by the user + ID *id.ID // Group ID + Key group.Key // Group key + IdPreimage group.IdPreimage // 256-bit value from CRNG + KeyPreimage group.KeyPreimage // 256-bit value from CRNG + InitMessage []byte // The original invite message + Members group.Membership // Sorted list of members in group + DhKeys DhKeyList // List of shared DH keys +} + +// NewGroup creates a new Group from copies of the given data. +func NewGroup(name []byte, groupID *id.ID, groupKey group.Key, + idPreimage group.IdPreimage, keyPreimage group.KeyPreimage, + initMessage []byte, members group.Membership, dhKeys DhKeyList) Group { + g := Group{ + Name: make([]byte, len(name)), + ID: groupID.DeepCopy(), + Key: groupKey, + IdPreimage: idPreimage, + KeyPreimage: keyPreimage, + InitMessage: make([]byte, len(initMessage)), + Members: members.DeepCopy(), + DhKeys: dhKeys, + } + + copy(g.Name, name) + copy(g.InitMessage, initMessage) + + return g +} + +// DeepCopy returns a copy of the Group. +func (g Group) DeepCopy() Group { + newGrp := Group{ + Name: make([]byte, len(g.Name)), + ID: g.ID.DeepCopy(), + Key: g.Key, + IdPreimage: g.IdPreimage, + KeyPreimage: g.KeyPreimage, + InitMessage: make([]byte, len(g.InitMessage)), + Members: g.Members.DeepCopy(), + DhKeys: make(map[id.ID]*cyclic.Int, len(g.Members)-1), + } + + copy(newGrp.Name, g.Name) + copy(newGrp.InitMessage, g.InitMessage) + + for uid, key := range g.DhKeys { + newGrp.DhKeys[uid] = key.DeepCopy() + } + + return newGrp +} + +// store saves an individual Group to storage keying on the group ID. +func (g Group) store(kv *versioned.KV) error { + obj := &versioned.Object{ + Version: groupStoreVersion, + Timestamp: netTime.Now(), + Data: g.Serialize(), + } + + return kv.Set(groupStoreKey(g.ID), groupStoreVersion, obj) +} + +// loadGroup returns the group with the corresponding ID from storage. +func loadGroup(groupID *id.ID, kv *versioned.KV) (Group, error) { + obj, err := kv.Get(groupStoreKey(groupID), groupStoreVersion) + if err != nil { + return Group{}, errors.Errorf(kvGetGroupErr, groupID, err) + } + + return DeserializeGroup(obj.Data) +} + +// removeGroup deletes the given group from storage. +func removeGroup(groupID *id.ID, kv *versioned.KV) error { + return kv.Delete(groupStoreKey(groupID), groupStoreVersion) +} + +// Serialize serializes the Group and returns the byte slice. +func (g Group) Serialize() []byte { + buff := bytes.NewBuffer(nil) + + // Write length of name and name + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(len(g.Name))) + buff.Write(b) + buff.Write(g.Name) + + // Write group ID + if g.ID != nil { + buff.Write(g.ID.Marshal()) + } else { + buff.Write(make([]byte, id.ArrIDLen)) + } + + // Write group key and preimages + buff.Write(g.Key[:]) + buff.Write(g.IdPreimage[:]) + buff.Write(g.KeyPreimage[:]) + + // Write length of InitMessage and InitMessage + b = make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(len(g.InitMessage))) + buff.Write(b) + buff.Write(g.InitMessage) + + // Write length of group membership and group membership + b = make([]byte, 8) + memberBytes := g.Members.Serialize() + binary.LittleEndian.PutUint64(b, uint64(len(memberBytes))) + buff.Write(b) + buff.Write(memberBytes) + + // Write DH key list + buff.Write(g.DhKeys.Serialize()) + + return buff.Bytes() +} + +// DeserializeGroup deserializes the bytes into a Group. +func DeserializeGroup(data []byte) (Group, error) { + buff := bytes.NewBuffer(data) + var g Group + var err error + + // Get name + nameLen := binary.LittleEndian.Uint64(buff.Next(8)) + if nameLen > 0 { + g.Name = buff.Next(int(nameLen)) + } + + // Get group ID + var groupID id.ID + copy(groupID[:], buff.Next(id.ArrIDLen)) + if groupID == [id.ArrIDLen]byte{} { + g.ID = nil + } else { + g.ID = &groupID + } + + // Get group key and preimages + copy(g.Key[:], buff.Next(group.KeyLen)) + copy(g.IdPreimage[:], buff.Next(group.IdPreimageLen)) + copy(g.KeyPreimage[:], buff.Next(group.KeyPreimageLen)) + + // Get InitMessage + initMessageLength := binary.LittleEndian.Uint64(buff.Next(8)) + if initMessageLength > 0 { + g.InitMessage = buff.Next(int(initMessageLength)) + } + + // Get member list + membersLength := binary.LittleEndian.Uint64(buff.Next(8)) + g.Members, err = group.DeserializeMembership(buff.Next(int(membersLength))) + if err != nil { + return Group{}, errors.Errorf(membershipErr, err) + } + + // Get DH key list + g.DhKeys, err = DeserializeDhKeyList(buff.Bytes()) + if err != nil { + return Group{}, errors.Errorf(dhKeyListErr, err) + } + + return g, err +} + +// groupStoreKey generates a unique key to save and load a Group to/from storage. +func groupStoreKey(groupID *id.ID) string { + return groupStorageKey + groupID.String() +} + +// GoString returns all the Group's fields as text. This functions satisfies the +// fmt.GoStringer interface. +func (g Group) GoString() string { + idString := "<nil>" + if g.ID != nil { + idString = g.ID.String() + } + + str := make([]string, 8) + + str[0] = "Name:" + fmt.Sprintf("%q", g.Name) + str[1] = "ID:" + idString + str[2] = "Key:" + g.Key.String() + str[3] = "IdPreimage:" + g.IdPreimage.String() + str[4] = "KeyPreimage:" + g.KeyPreimage.String() + str[5] = "InitMessage:" + fmt.Sprintf("%q", g.InitMessage) + str[6] = "Members:" + g.Members.String() + str[7] = "DhKeys:" + g.DhKeys.GoString() + + return "{" + strings.Join(str, ", ") + "}" +} diff --git a/groupChat/groupStore/group_test.go b/groupChat/groupStore/group_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ff855db08634dac15c33b918e14a3d394c90ef2b --- /dev/null +++ b/groupChat/groupStore/group_test.go @@ -0,0 +1,289 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupStore + +import ( + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "reflect" + "strings" + "testing" +) + +// Unit test of NewGroup. +func TestNewGroup(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + membership := createMembership(prng, 10, t) + dkl := GenerateDhKeyList(membership[0].ID, randCycInt(prng), membership, getGroup()) + + expectedGroup := Group{ + Name: []byte(groupName), + ID: id.NewIdFromUInt(uint64(42), id.Group, t), + Key: newKey(groupKey), + IdPreimage: newIdPreimage(groupIdPreimage), + KeyPreimage: newKeyPreimage(groupKeyPreimage), + InitMessage: []byte(initMessage), + Members: membership, + DhKeys: dkl, + } + + receivedGroup := NewGroup( + []byte(groupName), + id.NewIdFromUInt(uint64(42), id.Group, t), + newKey(groupKey), + newIdPreimage(groupIdPreimage), + newKeyPreimage(groupKeyPreimage), + []byte(initMessage), + membership, + dkl, + ) + + if !reflect.DeepEqual(receivedGroup, expectedGroup) { + t.Errorf("NewGroup did not return the expected Group."+ + "\nexpected: %#v\nreceived: %#v", expectedGroup, receivedGroup) + } +} + +// Unit test of Group.DeepCopy. +func TestGroup_DeepCopy(t *testing.T) { + grp := createTestGroup(rand.New(rand.NewSource(42)), t) + + newGrp := grp.DeepCopy() + + if !reflect.DeepEqual(grp, newGrp) { + t.Errorf("DeepCopy did not return a copy of the original Group."+ + "\nexpected: %#v\nreceived: %#v", grp, newGrp) + } + + if &grp.Name[0] == &newGrp.Name[0] { + t.Errorf("DeepCopy returned a copy of the pointer of Name."+ + "\nexpected: %p\nreceived: %p", &grp.Name[0], &newGrp.Name[0]) + } + + if &grp.ID[0] == &newGrp.ID[0] { + t.Errorf("DeepCopy returned a copy of the pointer of ID."+ + "\nexpected: %p\nreceived: %p", &grp.ID[0], &newGrp.ID[0]) + } + + if &grp.Key[0] == &newGrp.Key[0] { + t.Errorf("DeepCopy returned a copy of the pointer of Key."+ + "\nexpected: %p\nreceived: %p", &grp.Key[0], &newGrp.Key[0]) + } + + if &grp.IdPreimage[0] == &newGrp.IdPreimage[0] { + t.Errorf("DeepCopy returned a copy of the pointer of IdPreimage."+ + "\nexpected: %p\nreceived: %p", &grp.IdPreimage[0], &newGrp.IdPreimage[0]) + } + + if &grp.KeyPreimage[0] == &newGrp.KeyPreimage[0] { + t.Errorf("DeepCopy returned a copy of the pointer of KeyPreimage."+ + "\nexpected: %p\nreceived: %p", &grp.KeyPreimage[0], &newGrp.KeyPreimage[0]) + } + + if &grp.InitMessage[0] == &newGrp.InitMessage[0] { + t.Errorf("DeepCopy returned a copy of the pointer of InitMessage."+ + "\nexpected: %p\nreceived: %p", &grp.InitMessage[0], &newGrp.InitMessage[0]) + } + + if &grp.Members[0] == &newGrp.Members[0] { + t.Errorf("DeepCopy returned a copy of the pointer of Members."+ + "\nexpected: %p\nreceived: %p", &grp.Members[0], &newGrp.Members[0]) + } +} + +// Unit test of Group.store. +func TestGroup_store(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + g := createTestGroup(rand.New(rand.NewSource(42)), t) + + err := g.store(kv) + if err != nil { + t.Errorf("store returned an error: %+v", err) + } + + obj, err := kv.Get(groupStoreKey(g.ID), groupStoreVersion) + if err != nil { + t.Errorf("Failed to get group from storage: %+v", err) + } + + newGrp, err := DeserializeGroup(obj.Data) + if err != nil { + t.Errorf("Failed to deserialize group: %+v", err) + } + + if !reflect.DeepEqual(g, newGrp) { + t.Errorf("Failed to read correct group from storage."+ + "\nexpected: %#v\nreceived: %#v", g, newGrp) + } +} + +// Unit test of Group.loadGroup. +func Test_loadGroup(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + g := createTestGroup(rand.New(rand.NewSource(42)), t) + + err := g.store(kv) + if err != nil { + t.Errorf("store returned an error: %+v", err) + } + + newGrp, err := loadGroup(g.ID, kv) + if err != nil { + t.Errorf("loadGroup returned an error: %+v", err) + } + + if !reflect.DeepEqual(g, newGrp) { + t.Errorf("loadGroup failed to return the expected group."+ + "\nexpected: %#v\nreceived: %#v", g, newGrp) + } +} + +// Error path: an error is returned when no group with the ID exists in storage. +func Test_loadGroup_InvalidGroupIdError(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + g := createTestGroup(rand.New(rand.NewSource(42)), t) + expectedErr := strings.SplitN(kvGetGroupErr, "%", 2)[0] + + _, err := loadGroup(g.ID, kv) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("loadGroup failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of Group.removeGroup. +func Test_removeGroup(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + g := createTestGroup(rand.New(rand.NewSource(42)), t) + + err := g.store(kv) + if err != nil { + t.Errorf("store returned an error: %+v", err) + } + + err = removeGroup(g.ID, kv) + if err != nil { + t.Errorf("removeGroup returned an error: %+v", err) + } + + foundGrp, err := loadGroup(g.ID, kv) + if err == nil { + t.Errorf("loadGroup found group that should have been removed: %#v", + foundGrp) + } +} + +// Tests that a group that is serialized and deserialized matches the original. +func TestGroup_Serialize_DeserializeGroup(t *testing.T) { + grp := createTestGroup(rand.New(rand.NewSource(42)), t) + + grpBytes := grp.Serialize() + + newGrp, err := DeserializeGroup(grpBytes) + if err != nil { + t.Errorf("DeserializeGroup returned an error: %+v", err) + } + + if !reflect.DeepEqual(grp, newGrp) { + t.Errorf("Deserialized group does not match original."+ + "\nexpected: %#v\nreceived: %#v", grp, newGrp) + } +} + +// Tests that a group with nil fields that is serialized and deserialized +// matches the original. +func TestGroup_Serialize_DeserializeGroup_NilGroup(t *testing.T) { + grp := Group{Members: make(group.Membership, 3)} + + grpBytes := grp.Serialize() + + newGrp, err := DeserializeGroup(grpBytes) + if err != nil { + t.Errorf("DeserializeGroup returned an error: %+v", err) + } + + if !reflect.DeepEqual(grp, newGrp) { + t.Errorf("Deserialized group does not match original."+ + "\nexpected: %#v\nreceived: %#v", grp, newGrp) + } +} + +// Error path: error returned when the group membership is too small. +func TestDeserializeGroup_DeserializeMembershipError(t *testing.T) { + grp := Group{} + grpBytes := grp.Serialize() + expectedErr := strings.SplitN(membershipErr, "%", 2)[0] + + _, err := DeserializeGroup(grpBytes) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("DeserializeGroup failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +func Test_groupStoreKey(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + expectedKeys := []string{ + "GroupChat/U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID", + "GroupChat/15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD", + "GroupChat/YdN1vAK0HfT5GSnhj9qeb4LlTnSOgeeeS71v40zcuoQD", + "GroupChat/6NY+jE/+HOvqVG2PrBPdGqwEzi6ih3xVec+ix44bC68D", + "GroupChat/iBuCp1EQikLtPJA8qkNGWnhiBhaXiu0M48bE8657w+AD", + "GroupChat/W1cS/v2+DBAoh+EA2s0tiF9pLLYH2gChHBxwceeWotwD", + "GroupChat/wlpbdLLhKXBeJz8FySMmgo4rBW44F2WOEGFJiUf980QD", + "GroupChat/DtTBFgI/qONXa2/tJ/+JdLrAyv2a0FaSsTYZ5ziWTf0D", + "GroupChat/no1TQ3NmHP1m10/sHhuJSRq3I25LdSFikM8r60LDyicD", + "GroupChat/hWDxqsBnzqbov0bUqytGgEAsX7KCDohdMmDx3peCg9QD", + } + for i, expected := range expectedKeys { + newID, _ := id.NewRandomID(prng, id.User) + + key := groupStoreKey(newID) + + if key != expected { + t.Errorf("groupStoreKey did not return the expected key (%d)."+ + "\nexpected: %s\nreceived: %s", i, expected, key) + } + + // fmt.Printf("\"%s\",\n", key) + } +} + +// Unit test of Group.GoString. +func TestGroup_GoString(t *testing.T) { + grp := createTestGroup(rand.New(rand.NewSource(42)), t) + expected := "{Name:\"groupName\", ID:XMCYoCcs5+sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE, Key:a2V5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, IdPreimage:aWRQcmVpbWFnZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, KeyPreimage:a2V5UHJlaW1hZ2UAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, InitMessage:\"initMessage\", Members:{Leader: {U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID, 3534334367... in GRP: 6SsQ/HAHUn...}, Participants: 0: {Grcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0YD, 5274380952... in GRP: 6SsQ/HAHUn...}, 1: {QCxg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiAD, 1628829379... in GRP: 6SsQ/HAHUn...}, 2: {invD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHAD, 4157513341... in GRP: 6SsQ/HAHUn...}, 3: {o54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQgD, 6317064433... in GRP: 6SsQ/HAHUn...}, 4: {wRYCP6iJdLrAyv2a0FaSsTYZ5ziWTf3Hno1TQ3NmHP0D, 5785305945... in GRP: 6SsQ/HAHUn...}, 5: {15ufnw07pVsMwNYUTIiFNYQay+BwmwdYCD9h03W8ArQD, 2010156224... in GRP: 6SsQ/HAHUn...}, 6: {3RqsBM4ux44bC6+uiBuCp1EQikLtPJA8qkNGWnhiBhYD, 2643318057... in GRP: 6SsQ/HAHUn...}, 7: {55ai4SlwXic/BckjJoKOKwVuOBdljhBhSYlH/fNEQQ4D, 6482807720... in GRP: 6SsQ/HAHUn...}, 8: {9PkZKU50joHnnku9b+NM3LqEPujWPoxP/hzr6lRtj6wD, 6603068123... in GRP: 6SsQ/HAHUn...}}, DhKeys:{Grcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0YD: 6342989043... in GRP: 6SsQ/HAHUn..., QCxg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiAD: 2579328386... in GRP: 6SsQ/HAHUn..., invD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHAD: 1688982497... in GRP: 6SsQ/HAHUn..., o54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQgD: 5552242738... in GRP: 6SsQ/HAHUn..., wRYCP6iJdLrAyv2a0FaSsTYZ5ziWTf3Hno1TQ3NmHP0D: 2812078897... in GRP: 6SsQ/HAHUn..., 15ufnw07pVsMwNYUTIiFNYQay+BwmwdYCD9h03W8ArQD: 2588260662... in GRP: 6SsQ/HAHUn..., 3RqsBM4ux44bC6+uiBuCp1EQikLtPJA8qkNGWnhiBhYD: 4967151805... in GRP: 6SsQ/HAHUn..., 55ai4SlwXic/BckjJoKOKwVuOBdljhBhSYlH/fNEQQ4D: 3187530437... in GRP: 6SsQ/HAHUn..., 9PkZKU50joHnnku9b+NM3LqEPujWPoxP/hzr6lRtj6wD: 4832738218... in GRP: 6SsQ/HAHUn...}}" + + if grp.GoString() != expected { + t.Errorf("GoString failed to return the expected string."+ + "\nexpected: %s\nreceived: %s", expected, grp.GoString()) + } +} + +// Test that Group.GoString returns the expected string for a nil group. +func TestGroup_GoString_NilGroup(t *testing.T) { + grp := Group{} + expected := "{" + + "Name:\"\", " + + "ID:<nil>, " + + "Key:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, " + + "IdPreimage:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, " + + "KeyPreimage:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, " + + "InitMessage:\"\", " + + "Members:{<nil>}, " + + "DhKeys:{}" + + "}" + + if grp.GoString() != expected { + t.Errorf("GoString failed to return the expected string."+ + "\nexpected: %s\nreceived: %s", expected, grp.GoString()) + } +} diff --git a/groupChat/groupStore/store.go b/groupChat/groupStore/store.go new file mode 100644 index 0000000000000000000000000000000000000000..88e980603e323114b2a0c39a138f0e2977627053 --- /dev/null +++ b/groupChat/groupStore/store.go @@ -0,0 +1,302 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupStore + +import ( + "bytes" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "sync" + "testing" +) + +// Storage values. +const ( + // Key used to identify the list of Groups in storage. + groupStoragePrefix = "GroupChatListStore" + groupListStorageKey = "GroupChatList" + groupListVersion = 0 +) + +// Error messages. +const ( + kvGetGroupListErr = "failed to get list of group IDs from storage: %+v" + groupLoadErr = "failed to load group %d/%d: %+v" + groupSaveErr = "failed to save group %s to storage: %+v" + maxGroupsErr = "failed to add new group, max number of groups (%d) reached" + groupExistsErr = "group with ID %s already exists" + groupRemoveErr = "failed to remove group with ID %s, group not found in memory" + saveListRemoveErr = "failed to save new group ID list after removing group %s" + setUserPanic = "Store.SetUser is for testing only. Got %T" +) + +// The maximum number of group chats that a user can be a part of at once. +const MaxGroupChats = 64 + +// Store stores the list of Groups that a user is a part of. +type Store struct { + list map[id.ID]Group + user group.Member + kv *versioned.KV + mux sync.RWMutex +} + +// NewStore constructs a new Store object for the user and saves it to storage. +func NewStore(kv *versioned.KV, user group.Member) (*Store, error) { + s := &Store{ + list: make(map[id.ID]Group), + user: user.DeepCopy(), + kv: kv.Prefix(groupStoragePrefix), + } + + return s, s.save() +} + +// NewOrLoadStore loads the group store from storage or makes a new one if it +// does not exist. +func NewOrLoadStore(kv *versioned.KV, user group.Member) (*Store, error) { + prefixKv := kv.Prefix(groupStoragePrefix) + + // Load the list of group IDs from file if they exist + vo, err := prefixKv.Get(groupListStorageKey, groupListVersion) + if err == nil { + return loadStore(vo.Data, prefixKv, user) + } + + // If there is no group list saved, then make a new one + return NewStore(kv, user) +} + +// LoadStore loads all the Groups from storage into memory and return them in +// a Store object. +func LoadStore(kv *versioned.KV, user group.Member) (*Store, error) { + kv = kv.Prefix(groupStoragePrefix) + + // Load the list of group IDs from file + vo, err := kv.Get(groupListStorageKey, groupListVersion) + if err != nil { + return nil, errors.Errorf(kvGetGroupListErr, err) + } + + return loadStore(vo.Data, kv, user) +} + +// loadStore builds the list of group IDs and loads the groups from storage. +func loadStore(data []byte, kv *versioned.KV, user group.Member) (*Store, error) { + // Deserialize list of group IDs + groupIDs := deserializeGroupIdList(data) + + // Initialize the Store + s := &Store{ + list: make(map[id.ID]Group, len(groupIDs)), + user: user.DeepCopy(), + kv: kv, + } + + // Load each Group from storage into the map + for i, grpID := range groupIDs { + grp, err := loadGroup(grpID, kv) + if err != nil { + return nil, errors.Errorf(groupLoadErr, i, len(grpID), err) + } + s.list[*grpID] = grp + } + + return s, nil +} + +// saveGroupList saves a list of group IDs to storage. +func (s *Store) saveGroupList() error { + // Create the versioned object + obj := &versioned.Object{ + Version: groupListVersion, + Timestamp: netTime.Now(), + Data: serializeGroupIdList(s.list), + } + + // Save to storage + return s.kv.Set(groupListStorageKey, groupListVersion, obj) +} + +// serializeGroupIdList serializes the list of group IDs. +func serializeGroupIdList(list map[id.ID]Group) []byte { + buff := bytes.NewBuffer(nil) + buff.Grow(id.ArrIDLen * len(list)) + + // Create list of IDs from map + for grpId := range list { + buff.Write(grpId.Marshal()) + } + + return buff.Bytes() +} + +// deserializeGroupIdList deserializes data into a list of group IDs. +func deserializeGroupIdList(data []byte) []*id.ID { + idLen := id.ArrIDLen + groupIDs := make([]*id.ID, 0, len(data)/idLen) + buff := bytes.NewBuffer(data) + + // Copy each set of data into a new ID and append to list + for n := buff.Next(idLen); len(n) == idLen; n = buff.Next(idLen) { + var newID id.ID + copy(newID[:], n) + groupIDs = append(groupIDs, &newID) + } + + return groupIDs +} + +// save saves the group ID list and each group individually to storage. +func (s *Store) save() error { + // Store group ID list + err := s.saveGroupList() + if err != nil { + return err + } + + // Store individual groups + for grpID, grp := range s.list { + if err := grp.store(s.kv); err != nil { + return errors.Errorf(groupSaveErr, grpID, err) + } + } + + return nil +} + +// Len returns the number of groups stored. +func (s *Store) Len() int { + s.mux.RLock() + defer s.mux.RUnlock() + + return len(s.list) +} + +// Add adds a new group to the group list and saves it to storage. An error is +// returned if the user has the max number of groups (MaxGroupChats). +func (s *Store) Add(g Group) error { + s.mux.Lock() + defer s.mux.Unlock() + + // Check if the group list is full. + if len(s.list) >= MaxGroupChats { + return errors.Errorf(maxGroupsErr, MaxGroupChats) + } + + // Return an error if the group already exists in the map + if _, exists := s.list[*g.ID]; exists { + return errors.Errorf(groupExistsErr, g.ID) + } + + // Add the group to the map + s.list[*g.ID] = g.DeepCopy() + + // Update the group list in storage + err := s.saveGroupList() + if err != nil { + return err + } + + // Store the group to storage + return g.store(s.kv) +} + +// Remove removes the group with the corresponding ID from memory and storage. +// An error is returned if the group cannot be found in memory or storage. +func (s *Store) Remove(groupID *id.ID) error { + s.mux.Lock() + defer s.mux.Unlock() + + // Exit if the Group does not exist in memory + if _, exists := s.list[*groupID]; !exists { + return errors.Errorf(groupRemoveErr, groupID) + } + + // Delete Group from memory + delete(s.list, *groupID) + + // Remove group ID from list in memory + err := s.saveGroupList() + if err != nil { + return errors.Errorf(saveListRemoveErr, groupID) + } + + // Delete Group from storage + return removeGroup(groupID, s.kv) +} + +// GroupIDs returns a list of all group IDs. +func (s *Store) GroupIDs() []*id.ID { + s.mux.RLock() + defer s.mux.RUnlock() + + idList := make([]*id.ID, 0, len(s.list)) + for gid := range s.list { + idList = append(idList, gid.DeepCopy()) + } + + return idList +} + +// Get returns the Group for the given group ID. Returns false if no Group is +// found. +func (s *Store) Get(groupID *id.ID) (Group, bool) { + s.mux.RLock() + defer s.mux.RUnlock() + + grp, exists := s.list[*groupID] + if !exists { + return Group{}, false + } + + return grp.DeepCopy(), exists +} + +// GetByKeyFp returns the group with the matching key fingerprint and salt. +// Returns false if no group is found. +func (s *Store) GetByKeyFp(keyFp format.Fingerprint, salt [group.SaltLen]byte) (Group, bool) { + s.mux.RLock() + defer s.mux.RUnlock() + + // Iterate through each group to check if the key fingerprint matches + for _, grp := range s.list { + if group.CheckKeyFingerprint(keyFp, grp.Key, salt, s.user.ID) { + return grp.DeepCopy(), true + } + } + + return Group{}, false +} + +// GetUser returns the group member for the current user. +func (s *Store) GetUser() group.Member { + s.mux.RLock() + defer s.mux.RUnlock() + return s.user.DeepCopy() +} + +// SetUser allows a user to be set. This function is for testing purposes only. +// It panics if the interface is not of a testing type. +func (s *Store) SetUser(user group.Member, x interface{}) { + switch x.(type) { + case *testing.T, *testing.M, *testing.B, *testing.PB: + break + default: + jww.FATAL.Panicf(setUserPanic, x) + } + + s.mux.Lock() + defer s.mux.Unlock() + s.user = user.DeepCopy() +} diff --git a/groupChat/groupStore/store_test.go b/groupChat/groupStore/store_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0e0917ff38ce33b83cbecbfafb4f960ea8c38447 --- /dev/null +++ b/groupChat/groupStore/store_test.go @@ -0,0 +1,576 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupStore + +import ( + "bytes" + "fmt" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "reflect" + "sort" + "strings" + "testing" +) + +// Unit test of NewStore. +func TestNewStore(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + + expectedStore := &Store{ + list: make(map[id.ID]Group), + user: user, + kv: kv.Prefix(groupStoragePrefix), + } + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("NewStore returned an error: %+v", err) + } + + // Compare manually created object with NewUnknownRoundsStore + if !reflect.DeepEqual(expectedStore, store) { + t.Errorf("NewStore returned incorrect Store."+ + "\nexpected: %+v\nreceived: %+v", expectedStore, store) + } + + // Add information in store + testGroup := createTestGroup(prng, t) + + store.list[*testGroup.ID] = testGroup + + if err := store.save(); err != nil { + t.Fatalf("save() could not write to disk: %+v", err) + } + + groupIds := make([]id.ID, 0, len(store.list)) + for grpId := range store.list { + groupIds = append(groupIds, grpId) + } + + // Check that stored group Id list is expected value + expectedData := serializeGroupIdList(store.list) + + obj, err := store.kv.Get(groupListStorageKey, groupListVersion) + if err != nil { + t.Errorf("Could not get group list: %+v", err) + } + + // Check that the stored data is the data outputted by marshal + if !bytes.Equal(expectedData, obj.Data) { + t.Errorf("NewStore() returned incorrect Store."+ + "\nexpected: %+v\nreceived: %+v", expectedData, obj.Data) + } + + obj, err = store.kv.Get(groupStoreKey(testGroup.ID), groupListVersion) + if err != nil { + t.Errorf("Could not get group: %+v", err) + } + + newGrp, err := DeserializeGroup(obj.Data) + if err != nil { + t.Errorf("Failed to deserialize group: %+v", err) + } + + if !reflect.DeepEqual(testGroup, newGrp) { + t.Errorf("NewStore() returned incorrect Store."+ + "\nexpected: %#v\nreceived: %#v", testGroup, newGrp) + } +} + +func TestNewOrLoadStore(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + + store, err := NewOrLoadStore(kv, user) + if err != nil { + t.Fatalf("Failed to create new store: %+v", err) + } + + // Add group to store + testGroup := createTestGroup(prng, t) + if err = store.Add(testGroup); err != nil { + t.Fatalf("Failed to add test group: %+v", err) + } + + // Load the store from kv + receivedStore, err := NewOrLoadStore(kv, user) + if err != nil { + t.Fatalf("LoadStore returned an error: %+v", err) + } + + // Check that state in loaded store matches store that was saved + if len(receivedStore.list) != len(store.list) { + t.Errorf("LoadStore returned Store with incorrect number of groups."+ + "\nexpected len: %d\nreceived len: %d", + len(store.list), len(receivedStore.list)) + } + + if _, exists := receivedStore.list[*testGroup.ID]; !exists { + t.Fatalf("Failed to get group from loaded group map."+ + "\nexpected: %#v\nreceived: %#v", testGroup, receivedStore.list) + } +} + +// Unit test of LoadStore. +func TestLoadStore(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to create new store: %+v", err) + } + + // Add group to store + testGroup := createTestGroup(prng, t) + if err = store.Add(testGroup); err != nil { + t.Fatalf("Failed to add test group: %+v", err) + } + + // Load the store from kv + receivedStore, err := LoadStore(kv, user) + if err != nil { + t.Fatalf("LoadStore returned an error: %+v", err) + } + + // Check that state in loaded store matches store that was saved + if len(receivedStore.list) != len(store.list) { + t.Errorf("LoadStore returned Store with incorrect number of groups."+ + "\nexpected len: %d\nreceived len: %d", + len(store.list), len(receivedStore.list)) + } + + if _, exists := receivedStore.list[*testGroup.ID]; !exists { + t.Fatalf("Failed to get group from loaded group map."+ + "\nexpected: %#v\nreceived: %#v", testGroup, receivedStore.list) + } +} + +// Error path: show that LoadStore returns an error when no group store can be +// found in storage. +func TestLoadStore_GetError(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(rand.New(rand.NewSource(42))) + expectedErr := strings.SplitN(kvGetGroupListErr, "%", 2)[0] + + // Load the store from kv + _, err := LoadStore(kv, user) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("LoadStore did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: show that loadStore returns an error when no group can be found +// in storage. +func Test_loadStore_GetGroupError(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(rand.New(rand.NewSource(42))) + var idList []byte + for i := 0; i < 10; i++ { + idList = append(idList, id.NewIdFromUInt(uint64(i), id.Group, t).Marshal()...) + } + expectedErr := strings.SplitN(groupLoadErr, "%", 2)[0] + + // Load the groups from kv + _, err := loadStore(idList, kv, user) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("loadStore did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } + +} + +// Tests that a map of groups can be serialized and deserialized into a list +// that has the same group IDs. +func Test_serializeGroupIdList_deserializeGroupIdList(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + n := 10 + testMap := make(map[id.ID]Group, n) + expected := make([]*id.ID, n) + for i := 0; i < n; i++ { + grp := createTestGroup(prng, t) + expected[i] = grp.ID + testMap[*grp.ID] = grp + } + + // Serialize and deserialize map + data := serializeGroupIdList(testMap) + newList := deserializeGroupIdList(data) + + // Sort expected and received lists so they are in the same order + sort.Slice(expected, func(i, j int) bool { + return bytes.Compare(expected[i].Bytes(), expected[j].Bytes()) == -1 + }) + sort.Slice(newList, func(i, j int) bool { + return bytes.Compare(newList[i].Bytes(), newList[j].Bytes()) == -1 + }) + + // Check if they match + if !reflect.DeepEqual(expected, newList) { + t.Errorf("Failed to serialize and deserilize group map into list."+ + "\nexpected: %+v\nreceived: %+v", expected, newList) + } +} + +// Unit test of Store.Len. +func TestStore_Len(t *testing.T) { + s := Store{list: make(map[id.ID]Group)} + + if s.Len() != 0 { + t.Errorf("Len returned the wrong length.\nexpected: %d\nreceived: %d", + 0, s.Len()) + } + + n := 10 + for i := 0; i < n; i++ { + s.list[*id.NewIdFromUInt(uint64(i), id.Group, t)] = Group{} + } + + if s.Len() != n { + t.Errorf("Len returned the wrong length.\nexpected: %d\nreceived: %d", + n, s.Len()) + } +} + +// Unit test of Store.Add. +func TestStore_Add(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to create store: %+v", err) + } + + // Add maximum number of groups allowed + for i := 0; i < MaxGroupChats; i++ { + // Add group to store + grp := createTestGroup(prng, t) + err = store.Add(grp) + if err != nil { + t.Errorf("Add returned an error (%d): %v", i, err) + } + + if _, exists := store.list[*grp.ID]; !exists { + t.Errorf("Group %s was not added to the map (%d)", grp.ID, i) + } + } + + if len(store.list) != MaxGroupChats { + t.Errorf("Length of group map does not match number of groups added."+ + "\nexpected: %d\nreceived: %d", MaxGroupChats, len(store.list)) + } +} + +// Error path: shows that an error is returned when trying to add too many +// groups. +func TestStore_Add_MapFullError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + expectedErr := strings.SplitN(maxGroupsErr, "%", 2)[0] + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to create store: %+v", err) + } + + // Add maximum number of groups allowed + for i := 0; i < MaxGroupChats; i++ { + err = store.Add(createTestGroup(prng, t)) + if err != nil { + t.Errorf("Add returned an error (%d): %v", i, err) + } + } + + err = store.Add(createTestGroup(prng, t)) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("Add did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: show Store.Add returns an error when attempting to add a group +// that is already in the map. +func TestStore_Add_GroupExistsError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + expectedErr := strings.SplitN(groupExistsErr, "%", 2)[0] + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to create store: %+v", err) + } + + grp := createTestGroup(prng, t) + err = store.Add(grp) + if err != nil { + t.Errorf("Add returned an error: %+v", err) + } + + err = store.Add(grp) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("Add did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of Store.Remove. +func TestStore_Remove(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to create store: %+v", err) + } + + // Add maximum number of groups allowed + groups := make([]Group, MaxGroupChats) + for i := 0; i < MaxGroupChats; i++ { + groups[i] = createTestGroup(prng, t) + if err = store.Add(groups[i]); err != nil { + t.Errorf("Failed to add group (%d): %v", i, err) + } + } + + // Remove all groups + for i, grp := range groups { + err = store.Remove(grp.ID) + if err != nil { + t.Errorf("Remove returned an error (%d): %+v", i, err) + } + + if _, exists := store.list[*grp.ID]; exists { + t.Fatalf("Group %s still exists in map (%d).", grp.ID, i) + } + } + + // Check that the list is empty now + if len(store.list) != 0 { + t.Fatalf("Remove failed to remove all groups.."+ + "\nexpected: %d\nreceived: %d", 0, len(store.list)) + } +} + +// Error path: shows that Store.Remove returns an error when no group with the +// given ID is found in the map. +func TestStore_Remove_RemoveGroupNotInMemoryError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + expectedErr := strings.SplitN(groupRemoveErr, "%", 2)[0] + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to create store: %+v", err) + } + + grp := createTestGroup(prng, t) + err = store.Remove(grp.ID) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("Remove did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of Store.GroupIDs. +func TestStore_GroupIDs(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + n := 10 + store := Store{list: make(map[id.ID]Group, n)} + expected := make([]*id.ID, n) + for i := 0; i < n; i++ { + grp := createTestGroup(prng, t) + expected[i] = grp.ID + store.list[*grp.ID] = grp + } + + newList := store.GroupIDs() + + // Sort expected and received lists so they are in the same order + sort.Slice(expected, func(i, j int) bool { + return bytes.Compare(expected[i].Bytes(), expected[j].Bytes()) == -1 + }) + sort.Slice(newList, func(i, j int) bool { + return bytes.Compare(newList[i].Bytes(), newList[j].Bytes()) == -1 + }) + + // Check if they match + if !reflect.DeepEqual(expected, newList) { + t.Errorf("GroupIDs did not return the expected list."+ + "\nexpected: %+v\nreceived: %+v", expected, newList) + } +} + +// Unit test of Store.Get. +func TestStore_Get(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to make new Store: %+v", err) + } + + // Add group to store + grp := createTestGroup(prng, t) + if err = store.Add(grp); err != nil { + t.Errorf("Failed to add group to store: %+v", err) + } + + // Attempt to get group + retrieved, exists := store.Get(grp.ID) + if !exists { + t.Errorf("Get failed to return the expected group: %#v", grp) + } + + if !reflect.DeepEqual(grp, retrieved) { + t.Errorf("Get did not return the expected group."+ + "\nexpected: %#v\nreceived: %#v", grp, retrieved) + } +} + +// Error path: shows that Store.Get return false if no group is found. +func TestStore_Get_NoGroupError(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(rand.New(rand.NewSource(42))) + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to make new Store: %+v", err) + } + + // Attempt to get group + retrieved, exists := store.Get(id.NewIdFromString("testID", id.Group, t)) + if exists { + t.Errorf("Get returned a group that should not exist: %#v", retrieved) + } +} + +// Unit test of Store.GetByKeyFp. +func TestStore_GetByKeyFp(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to make new Store: %+v", err) + } + + // Add group to store + grp := createTestGroup(prng, t) + if err = store.Add(grp); err != nil { + t.Fatalf("Failed to add group: %+v", err) + } + + // Get group by fingerprint + salt := newSalt(groupSalt) + generatedFP := group.NewKeyFingerprint(grp.Key, salt, store.user.ID) + retrieved, exists := store.GetByKeyFp(generatedFP, salt) + if !exists { + t.Errorf("GetByKeyFp failed to find a group with the matching key "+ + "fingerprint: %#v", grp) + } + + // check that retrieved value match + if !reflect.DeepEqual(grp, retrieved) { + t.Errorf("GetByKeyFp failed to return the expected group."+ + "\nexpected: %#v\nreceived: %#v", grp, retrieved) + } +} + +// Error path: shows that Store.GetByKeyFp return false if no group is found. +func TestStore_GetByKeyFp_NoGroupError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(prng) + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to make new Store: %+v", err) + } + + // Get group by fingerprint + grp := createTestGroup(prng, t) + salt := newSalt(groupSalt) + generatedFP := group.NewKeyFingerprint(grp.Key, salt, store.user.ID) + retrieved, exists := store.GetByKeyFp(generatedFP, salt) + if exists { + t.Errorf("GetByKeyFp found a group when none should exist: %#v", + retrieved) + } +} + +// Unit test of Store.GetUser. +func TestStore_GetUser(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + user := randMember(rand.New(rand.NewSource(42))) + + store, err := NewStore(kv, user) + if err != nil { + t.Fatalf("Failed to make new Store: %+v", err) + } + + if !user.Equal(store.GetUser()) { + t.Errorf("GetUser() failed to return the expected member."+ + "\nexpected: %#v\nreceived: %#v", user, store.GetUser()) + } +} + +// Unit test of Store.SetUser. +func TestStore_SetUser(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + prng := rand.New(rand.NewSource(42)) + oldUser := randMember(prng) + newUser := randMember(prng) + + store, err := NewStore(kv, oldUser) + if err != nil { + t.Fatalf("Failed to make new Store: %+v", err) + } + + store.SetUser(newUser, t) + + if !newUser.Equal(store.user) { + t.Errorf("SetUser() failed to set the correct user."+ + "\nexpected: %#v\nreceived: %#v", newUser, store.user) + } +} + +// Panic path: show that Store.SetUser panics when the interface is not of a +// testing type. +func TestStore_SetUser_NonTestingInterfacePanic(t *testing.T) { + user := randMember(rand.New(rand.NewSource(42))) + store := &Store{} + nonTestingInterface := struct{}{} + expectedErr := fmt.Sprintf(setUserPanic, nonTestingInterface) + + defer func() { + if r := recover(); r == nil || r.(string) != expectedErr { + t.Errorf("SetUser failed to panic with the expected message."+ + "\nexpected: %s\nreceived: %+v", expectedErr, r) + } + }() + + store.SetUser(user, nonTestingInterface) +} diff --git a/groupChat/groupStore/utils_test.go b/groupChat/groupStore/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9a6460800498b11fdbcb8a4b9b5aa7d4e391b2f3 --- /dev/null +++ b/groupChat/groupStore/utils_test.go @@ -0,0 +1,139 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupStore + +import ( + "gitlab.com/elixxir/crypto/contact" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/xx_network/crypto/large" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "testing" +) + +const ( + groupName = "groupName" + groupSalt = "salt" + groupKey = "key" + groupIdPreimage = "idPreimage" + groupKeyPreimage = "keyPreimage" + initMessage = "initMessage" +) + +// createTestGroup generates a new group for testing. +func createTestGroup(rng *rand.Rand, t *testing.T) Group { + members := createMembership(rng, 10, t) + dkl := GenerateDhKeyList(members[0].ID, randCycInt(rng), members, getGroup()) + return NewGroup( + []byte(groupName), + id.NewIdFromUInt(rng.Uint64(), id.Group, t), + newKey(groupKey), + newIdPreimage(groupIdPreimage), + newKeyPreimage(groupKeyPreimage), + []byte(initMessage), + members, + dkl, + ) +} + +// createMembership creates a new membership with the specified number of +// randomly generated members. +func createMembership(rng *rand.Rand, size int, t *testing.T) group.Membership { + contacts := make([]contact.Contact, size) + for i := range contacts { + contacts[i] = randContact(rng) + } + + membership, err := group.NewMembership(contacts[0], contacts[1:]...) + if err != nil { + t.Errorf("Failed to create new membership: %+v", err) + } + + return membership +} + +// createDhKeyList creates a new DhKeyList with the specified number of randomly +// generated members. +func createDhKeyList(rng *rand.Rand, size int, _ *testing.T) DhKeyList { + dkl := make(DhKeyList, size) + for i := 0; i < size; i++ { + dkl[*randID(rng, id.User)] = randCycInt(rng) + } + + return dkl +} + +// randMember returns a Member with a random ID and DH public key. +func randMember(rng *rand.Rand) group.Member { + return group.Member{ + ID: randID(rng, id.User), + DhKey: randCycInt(rng), + } +} + +// randContact returns a contact with a random ID and DH public key. +func randContact(rng *rand.Rand) contact.Contact { + return contact.Contact{ + ID: randID(rng, id.User), + DhPubKey: randCycInt(rng), + } +} + +// randID returns a new random ID of the specified type. +func randID(rng *rand.Rand, t id.Type) *id.ID { + newID, _ := id.NewRandomID(rng, t) + return newID +} + +// randCycInt returns a random cyclic int. +func randCycInt(rng *rand.Rand) *cyclic.Int { + return getGroup().NewInt(rng.Int63()) +} + +func getGroup() *cyclic.Group { + return cyclic.NewGroup( + large.NewIntFromString("E2EE983D031DC1DB6F1A7A67DF0E9A8E5561DB8E8D4941"+ + "3394C049B7A8ACCEDC298708F121951D9CF920EC5D146727AA4AE535B0922C688"+ + "B55B3DD2AEDF6C01C94764DAB937935AA83BE36E67760713AB44A6337C20E7861"+ + "575E745D31F8B9E9AD8412118C62A3E2E29DF46B0864D0C951C394A5CBBDC6ADC"+ + "718DD2A3E041023DBB5AB23EBB4742DE9C1687B5B34FA48C3521632C4A530E8FF"+ + "B1BC51DADDF453B0B2717C2BC6669ED76B4BDD5C9FF558E88F26E5785302BEDBC"+ + "A23EAC5ACE92096EE8A60642FB61E8F3D24990B8CB12EE448EEF78E184C7242DD"+ + "161C7738F32BF29A841698978825B4111B4BC3E1E198455095958333D776D8B2B"+ + "EEED3A1A1A221A6E37E664A64B83981C46FFDDC1A45E3D5211AAF8BFBC072768C"+ + "4F50D7D7803D2D4F278DE8014A47323631D7E064DE81C0C6BFA43EF0E6998860F"+ + "1390B5D3FEACAF1696015CB79C3F9C2D93D961120CD0E5F12CBB687EAB045241F"+ + "96789C38E89D796138E6319BE62E35D87B1048CA28BE389B575E994DCA7554715"+ + "84A09EC723742DC35873847AEF49F66E43873", 16), + large.NewIntFromString("2", 16)) +} + +func newSalt(s string) [group.SaltLen]byte { + var salt [group.SaltLen]byte + copy(salt[:], s) + return salt +} + +func newKey(s string) group.Key { + var key group.Key + copy(key[:], s) + return key +} + +func newIdPreimage(s string) group.IdPreimage { + var preimage group.IdPreimage + copy(preimage[:], s) + return preimage +} + +func newKeyPreimage(s string) group.KeyPreimage { + var preimage group.KeyPreimage + copy(preimage[:], s) + return preimage +} diff --git a/groupChat/internalFormat.go b/groupChat/internalFormat.go new file mode 100644 index 0000000000000000000000000000000000000000..2502a9c8c29c9f940a93bebb1101c5d5a5ae8ef2 --- /dev/null +++ b/groupChat/internalFormat.go @@ -0,0 +1,156 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "encoding/binary" + "fmt" + "github.com/pkg/errors" + "gitlab.com/xx_network/primitives/id" + "strconv" + "time" +) + +// Sizes of marshaled data, in bytes. +const ( + timestampLen = 8 + idLen = id.ArrIDLen + internalPayloadSizeLen = 2 + internalMinLen = timestampLen + idLen + internalPayloadSizeLen +) + +// Error messages +const ( + newInternalSizeErr = "max message size %d < %d minimum required" + unmarshalInternalSizeErr = "size of data %d < %d minimum required" +) + +// internalMsg is the internal, unencrypted data in a group message. +// +// +-------------------------------------------+ +// | data | +// +-----------+----------+---------+----------+ +// | timestamp | senderID | size | payload | +// | 8 bytes | 32 bytes | 2 bytes | variable | +// +-----------+----------+---------+----------+ +type internalMsg struct { + data []byte // Serial of all the parts of the message + timestamp []byte // 64-bit Unix time timestamp stored in nanoseconds + senderID []byte // 264-bit sender ID + size []byte // Size of the payload + payload []byte // Message contents +} + +// newInternalMsg creates a new internalMsg of size maxDataSize. An error is +// returned if the maxDataSize is smaller than the minimum internalMsg size. +func newInternalMsg(maxDataSize int) (internalMsg, error) { + if maxDataSize < internalMinLen { + return internalMsg{}, + errors.Errorf(newInternalSizeErr, maxDataSize, internalMinLen) + } + + return mapInternalMsg(make([]byte, maxDataSize)), nil +} + +// mapInternalMsg maps all the parts of the internalMsg to the passed in data. +func mapInternalMsg(data []byte) internalMsg { + return internalMsg{ + data: data, + timestamp: data[:timestampLen], + senderID: data[timestampLen : timestampLen+idLen], + size: data[timestampLen+idLen : timestampLen+idLen+internalPayloadSizeLen], + payload: data[timestampLen+idLen+internalPayloadSizeLen:], + } +} + +// unmarshalInternalMsg unmarshal the data into an internalMsg. An error is +// returned if the data length is smaller than the minimum allowed size. +func unmarshalInternalMsg(data []byte) (internalMsg, error) { + if len(data) < internalMinLen { + return internalMsg{}, + errors.Errorf(unmarshalInternalSizeErr, len(data), internalMinLen) + } + + return mapInternalMsg(data), nil +} + +// Marshal returns the serial of the internalMsg. +func (im internalMsg) Marshal() []byte { + return im.data +} + +// GetTimestamp returns the timestamp as a time.Time. +func (im internalMsg) GetTimestamp() time.Time { + return time.Unix(0, int64(binary.LittleEndian.Uint64(im.timestamp))) +} + +// SetTimestamp converts the time.Time to Unix nano and save as bytes. +func (im internalMsg) SetTimestamp(t time.Time) { + binary.LittleEndian.PutUint64(im.timestamp, uint64(t.UnixNano())) +} + +// GetSenderID returns the sender ID bytes as a id.ID. +func (im internalMsg) GetSenderID() (*id.ID, error) { + return id.Unmarshal(im.senderID) +} + +// SetSenderID sets the sender ID. +func (im internalMsg) SetSenderID(sid *id.ID) { + copy(im.senderID, sid.Marshal()) +} + +// GetPayload returns the payload truncated to the correct size. +func (im internalMsg) GetPayload() []byte { + return im.payload[:im.GetPayloadSize()] +} + +// SetPayload sets the payload and saves it size. +func (im internalMsg) SetPayload(payload []byte) { + // Save size of payload + binary.LittleEndian.PutUint16(im.size, uint16(len(payload))) + + // Save payload + copy(im.payload, payload) +} + +// GetPayloadSize returns the length of the content in the payload. +func (im internalMsg) GetPayloadSize() int { + return int(binary.LittleEndian.Uint16(im.size)) +} + +// GetPayloadMaxSize returns the maximum size of the payload. +func (im internalMsg) GetPayloadMaxSize() int { + return len(im.payload) +} + +// String prints a string representation of internalMsg. This functions +// satisfies the fmt.Stringer interface. +func (im internalMsg) String() string { + timestamp := "<nil>" + if len(im.timestamp) > 0 { + timestamp = im.GetTimestamp().String() + } + + senderID := "<nil>" + if sid, _ := im.GetSenderID(); sid != nil { + senderID = sid.String() + } + + size := "<nil>" + if len(im.size) > 0 { + size = strconv.Itoa(im.GetPayloadSize()) + } + + payload := "<nil>" + if len(im.size) > 0 { + payload = fmt.Sprintf("%q", im.GetPayload()) + } + + return "{timestamp:" + timestamp + ", senderID:" + senderID + + ", size:" + size + ", payload:" + payload + "}" +} diff --git a/groupChat/internalFormat_test.go b/groupChat/internalFormat_test.go new file mode 100644 index 0000000000000000000000000000000000000000..984d11b8f35ec44935eaea48b5bbd76eec80bdb1 --- /dev/null +++ b/groupChat/internalFormat_test.go @@ -0,0 +1,211 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "bytes" + "encoding/binary" + "fmt" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "reflect" + "testing" + "time" +) + +// Unit test of newInternalMsg. +func Test_newInternalMsg(t *testing.T) { + maxDataSize := 2 * internalMinLen + im, err := newInternalMsg(maxDataSize) + if err != nil { + t.Errorf("newInternalMsg() returned an error: %+v", err) + } + + if len(im.data) != maxDataSize { + t.Errorf("newInternalMsg() set data to the wrong length."+ + "\nexpected: %d\nreceived: %d", maxDataSize, len(im.data)) + } +} + +// Error path: the maxDataSize is smaller than the minimum size. +func Test_newInternalMsg_PayloadSizeError(t *testing.T) { + maxDataSize := internalMinLen - 1 + expectedErr := fmt.Sprintf(newInternalSizeErr, maxDataSize, internalMinLen) + + _, err := newInternalMsg(maxDataSize) + if err == nil || err.Error() != expectedErr { + t.Errorf("newInternalMsg() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of mapInternalMsg. +func Test_mapInternalMsg(t *testing.T) { + // Create all the expected data + timestamp := make([]byte, timestampLen) + binary.LittleEndian.PutUint64(timestamp, uint64(netTime.Now().UnixNano())) + senderID := id.NewIdFromString("test sender ID", id.User, t).Marshal() + payload := []byte("Sample payload contents.") + size := make([]byte, internalPayloadSizeLen) + binary.LittleEndian.PutUint16(size, uint16(len(payload))) + + // Construct data into single slice + data := bytes.NewBuffer(nil) + data.Write(timestamp) + data.Write(senderID) + data.Write(size) + data.Write(payload) + + // Map data + im := mapInternalMsg(data.Bytes()) + + // Check that the mapped values match the expected values + if !bytes.Equal(timestamp, im.timestamp) { + t.Errorf("mapInternalMsg() did not correctly map timestamp."+ + "\nexpected: %+v\nreceived: %+v", timestamp, im.timestamp) + } + + if !bytes.Equal(senderID, im.senderID) { + t.Errorf("mapInternalMsg() did not correctly map senderID."+ + "\nexpected: %+v\nreceived: %+v", senderID, im.senderID) + } + + if !bytes.Equal(size, im.size) { + t.Errorf("mapInternalMsg() did not correctly map size."+ + "\nexpected: %+v\nreceived: %+v", size, im.size) + } + + if !bytes.Equal(payload, im.payload) { + t.Errorf("mapInternalMsg() did not correctly map payload."+ + "\nexpected: %+v\nreceived: %+v", payload, im.payload) + } +} + +// Tests that a marshaled and unmarshalled internalMsg matches the original. +func TestInternalMsg_Marshal_unmarshalInternalMsg(t *testing.T) { + im, _ := newInternalMsg(internalMinLen * 2) + im.SetTimestamp(netTime.Now()) + im.SetSenderID(id.NewIdFromString("test sender ID", id.User, t)) + im.SetPayload([]byte("Sample payload message.")) + + data := im.Marshal() + + newIm, err := unmarshalInternalMsg(data) + if err != nil { + t.Errorf("unmarshalInternalMsg() returned an error: %+v", err) + } + + if !reflect.DeepEqual(im, newIm) { + t.Errorf("unmarshalInternalMsg() did not return the expected internalMsg."+ + "\nexpected: %s\nreceived: %s", im, newIm) + } +} + +// Error path: error is returned when the data is too short. +func Test_unmarshalInternalMsg_DataLengthError(t *testing.T) { + expectedErr := fmt.Sprintf(unmarshalInternalSizeErr, 0, internalMinLen) + + _, err := unmarshalInternalMsg(nil) + if err == nil || err.Error() != expectedErr { + t.Errorf("unmarshalInternalMsg() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Happy path. +func TestInternalMsg_SetTimestamp_GetTimestamp(t *testing.T) { + im, _ := newInternalMsg(internalMinLen * 2) + timestamp := netTime.Now() + im.SetTimestamp(timestamp) + testTimestamp := im.GetTimestamp() + + if !timestamp.Equal(testTimestamp) { + t.Errorf("Failed to get original timestamp."+ + "\nexpected: %s\nreceived: %s", timestamp, testTimestamp) + } +} + +// Happy path. +func TestInternalMsg_SetSenderID_GetSenderID(t *testing.T) { + im, _ := newInternalMsg(internalMinLen * 2) + sid := id.NewIdFromString("testSenderID", id.User, t) + im.SetSenderID(sid) + testID, err := im.GetSenderID() + if err != nil { + t.Errorf("GetSenderID() returned an error: %+v", err) + } + + if !sid.Cmp(testID) { + t.Errorf("Failed to get original sender ID."+ + "\nexpected: %s\nreceived: %s", sid, testID) + } +} + +// Tests that the original payload matches the saved one. +func TestInternalMsg_SetPayload_GetPayload(t *testing.T) { + im, _ := newInternalMsg(internalMinLen * 2) + payload := []byte("Test payload message.") + im.SetPayload(payload) + testPayload := im.GetPayload() + + if !bytes.Equal(payload, testPayload) { + t.Errorf("Failed to get original sender payload."+ + "\nexpected: %s\nreceived: %s", payload, testPayload) + } +} + +// Happy path. +func TestInternalMsg_GetPayloadSize(t *testing.T) { + im, _ := newInternalMsg(internalMinLen * 2) + payload := []byte("Test payload message.") + im.SetPayload(payload) + + if len(payload) != im.GetPayloadSize() { + t.Errorf("GetPayloadSize() failed to return the correct size."+ + "\nexpected: %d\nreceived: %d", len(payload), im.GetPayloadSize()) + } +} + +// Happy path. +func TestInternalMsg_GetPayloadMaxSize(t *testing.T) { + im, _ := newInternalMsg(internalMinLen * 2) + + if internalMinLen != im.GetPayloadMaxSize() { + t.Errorf("GetPayloadSize() failed to return the correct size."+ + "\nexpected: %d\nreceived: %d", internalMinLen, im.GetPayloadMaxSize()) + } +} + +// Happy path. +func TestInternalMsg_String(t *testing.T) { + im, _ := newInternalMsg(internalMinLen * 2) + im.SetTimestamp(time.Date(1955, 11, 5, 12, 0, 0, 0, time.UTC)) + im.SetSenderID(id.NewIdFromString("test sender ID", id.User, t)) + payload := []byte("Sample payload message.") + payload = append(payload, 0, 1, 2) + im.SetPayload(payload) + + expected := `{timestamp:` + im.GetTimestamp().String() + `, senderID:dGVzdCBzZW5kZXIgSUQAAAAAAAAAAAAAAAAAAAAAAAAD, size:26, payload:"Sample payload message.\x00\x01\x02"}` + + if im.String() != expected { + t.Errorf("String() failed to return the expected value."+ + "\nexpected: %s\nreceived: %s", expected, im.String()) + } +} + +// Happy path: tests that String returns the expected string for a nil internalMsg. +func TestInternalMsg_String_NilInternalMessage(t *testing.T) { + im := internalMsg{} + + expected := "{timestamp:<nil>, senderID:<nil>, size:<nil>, payload:<nil>}" + + if im.String() != expected { + t.Errorf("String() failed to return the expected value."+ + "\nexpected: %s\nreceived: %s", expected, im.String()) + } +} diff --git a/groupChat/makeGroup.go b/groupChat/makeGroup.go new file mode 100644 index 0000000000000000000000000000000000000000..fa230fadcc99f61a4b4a44d0f62aa549ec9ec306 --- /dev/null +++ b/groupChat/makeGroup.go @@ -0,0 +1,190 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "github.com/pkg/errors" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/crypto/contact" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/xx_network/primitives/id" + "strconv" +) + +// Error messages. +const ( + maxInitMsgSizeErr = "new group request message length %d > %d maximum size" + getPrivKeyErr = "failed to get private key from partner: %+v" + minMembersErr = "length of membership list %d < %d minimum allowed" + maxMembersErr = "length of membership list %d > %d maximum allowed" + getPartnerErr = "failed to get partner %s: %+v" + makeMembershipErr = "failed to assemble group chat membership: %+v" + newIdPreimageErr = "failed to create group ID preimage: %+v" + newKeyPreimageErr = "failed to create group key preimage: %+v" + addGroupErr = "failed to save new group: %+v" +) + +// MaxInitMessageSize is the maximum allowable length of the initial message +// sent in a group request. +const MaxInitMessageSize = 256 + +// RequestStatus signals the status of the group requests on group creation. +type RequestStatus int + +const ( + NotSent RequestStatus = iota // Error occurred before sending requests + AllFail // Sending of all requests failed + PartialSent // Sending of some request failed + AllSent // Sending of all request succeeded +) + +// MakeGroup sends groupChat requests to all members over an authenticated +// channel. The leader of a groupChat must have an authenticated channel with +// each member of the groupChat to add them to the groupChat. It blocks until +// all the groupChat requests are sent. Returns an error if at least one request +// to a member fails to send. +func (m Manager) MakeGroup(membership []*id.ID, name, msg []byte) (gs.Group, + []id.Round, RequestStatus, error) { + // Return an error if the message is too long + if len(msg) > MaxInitMessageSize { + return gs.Group{}, nil, NotSent, + errors.Errorf(maxInitMsgSizeErr, len(msg), MaxInitMessageSize) + } + + // Build membership and DH key list from list of IDs + mem, dkl, err := m.buildMembership(membership) + if err != nil { + return gs.Group{}, nil, NotSent, err + } + + // Generate ID and key preimages + idPreimage, keyPreimage, err := getPreimages(m.rng) + if err != nil { + return gs.Group{}, nil, NotSent, err + } + + // Create new group ID and key + groupID := group.NewID(idPreimage, mem) + groupKey := group.NewKey(keyPreimage, mem) + + // Create new group and add to manager + g := gs.NewGroup(name, groupID, groupKey, idPreimage, keyPreimage, msg, mem, dkl) + if err := m.gs.Add(g); err != nil { + return gs.Group{}, nil, NotSent, errors.Errorf(addGroupErr, err) + } + + // Send all group requests + roundIDs, status, err := m.sendRequests(g) + + return g, roundIDs, status, err +} + +// buildMembership retrieves the contact object for each member ID and creates a +// new membership from them. The caller is set as the leader. For a member to be +// added, the group leader must have an authenticated channel with the member. +func (m Manager) buildMembership(members []*id.ID) (group.Membership, gs.DhKeyList, error) { + // Return an error if the membership list has too few or too many members + if len(members) < group.MinParticipants { + return nil, nil, + errors.Errorf(minMembersErr, len(members), group.MinParticipants) + } else if len(members) > group.MaxParticipants { + return nil, nil, + errors.Errorf(maxMembersErr, len(members), group.MaxParticipants) + } + + grp := m.store.E2e().GetGroup() + dkl := make(gs.DhKeyList, len(members)) + + // Lookup partner contact objects from their ID + contacts := make([]contact.Contact, len(members)) + var err error + for i, uid := range members { + partner, err := m.store.E2e().GetPartner(uid) + if err != nil { + return nil, nil, errors.Errorf(getPartnerErr, uid, err) + } + + contacts[i] = contact.Contact{ + ID: partner.GetPartnerID(), + DhPubKey: partner.GetPartnerOriginPublicKey(), + } + + dkl.Add(partner.GetMyOriginPrivateKey(), group.Member{ + ID: partner.GetPartnerID(), + DhKey: partner.GetPartnerOriginPublicKey(), + }, grp) + } + + // Create new Membership from contact list and client's own contact. + user := m.gs.GetUser() + leader := contact.Contact{ID: user.ID, DhPubKey: user.DhKey} + mem, err := group.NewMembership(leader, contacts...) + if err != nil { + return nil, nil, errors.Errorf(makeMembershipErr, err) + } + + return mem, dkl, nil +} + +// getPreimages generates and returns the group ID preimage and the group key +// preimage. This function allows the stream to +func getPreimages(streamGen *fastRNG.StreamGenerator) (group.IdPreimage, + group.KeyPreimage, error) { + + // Get new stream and defer its close + rng := streamGen.GetStream() + defer rng.Close() + + idPreimage, err := group.NewIdPreimage(rng) + if err != nil { + return group.IdPreimage{}, group.KeyPreimage{}, + errors.Errorf(newIdPreimageErr, err) + } + + keyPreimage, err := group.NewKeyPreimage(rng) + if err != nil { + return group.IdPreimage{}, group.KeyPreimage{}, + errors.Errorf(newKeyPreimageErr, err) + } + + return idPreimage, keyPreimage, nil +} + +// String prints the description of the status code. This functions satisfies +// the fmt.Stringer interface. +func (rs RequestStatus) String() string { + switch rs { + case NotSent: + return "NotSent" + case AllFail: + return "AllFail" + case PartialSent: + return "PartialSent" + case AllSent: + return "AllSent" + default: + return "INVALID STATUS" + } +} + +// Message prints a full description of the status code. +func (rs RequestStatus) Message() string { + switch rs { + case NotSent: + return "an error occurred before sending any group requests" + case AllFail: + return "all group requests failed to send" + case PartialSent: + return "some group requests failed to send" + case AllSent: + return "all groups requests successfully sent" + default: + return "INVALID STATUS " + strconv.Itoa(int(rs)) + } +} diff --git a/groupChat/makeGroup_test.go b/groupChat/makeGroup_test.go new file mode 100644 index 0000000000000000000000000000000000000000..004bf452664984c8ec8651442ab10a7a64d48fee --- /dev/null +++ b/groupChat/makeGroup_test.go @@ -0,0 +1,302 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "bytes" + "fmt" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "reflect" + "strconv" + "strings" + "testing" +) + +// Tests that Manager.MakeGroup adds a group and returns the expected status. +func TestManager_MakeGroup(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + memberIDs, members, dkl := addPartners(m, t) + name := []byte("groupName") + message := []byte("Invite message.") + + g, _, status, err := m.MakeGroup(memberIDs, name, message) + if err != nil { + t.Errorf("MakeGroup() returned an error: %+v", err) + } + + if status != AllSent { + t.Errorf("MakeGroup() did not return the expected status."+ + "\nexpected: %s\nreceived: %s", AllSent, status) + } + + _, exists := m.gs.Get(g.ID) + if !exists { + t.Errorf("Failed to get group %#v.", g) + } + + if !reflect.DeepEqual(members, g.Members) { + t.Errorf("New group does not have expected membership."+ + "\nexpected: %s\nreceived: %s", members, g.Members) + } + + if !reflect.DeepEqual(dkl, g.DhKeys) { + t.Errorf("New group does not have expected DH key list."+ + "\nexpected: %#v\nreceived: %#v", dkl, g.DhKeys) + } + + if !g.ID.Cmp(g.ID) { + t.Errorf("New group does not have expected ID."+ + "\nexpected: %s\nreceived: %s", g.ID, g.ID) + } + + if !bytes.Equal(name, g.Name) { + t.Errorf("New group does not have expected name."+ + "\nexpected: %q\nreceived: %q", name, g.Name) + } + + if !bytes.Equal(message, g.InitMessage) { + t.Errorf("New group does not have expected message."+ + "\nexpected: %q\nreceived: %q", message, g.InitMessage) + } +} + +// Error path: make sure an error and the correct status is returned when the +// message is too large. +func TestManager_MakeGroup_MaxMessageSizeError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + expectedErr := fmt.Sprintf(maxInitMsgSizeErr, MaxInitMessageSize+1, MaxInitMessageSize) + + _, _, status, err := m.MakeGroup(nil, nil, make([]byte, MaxInitMessageSize+1)) + if err == nil || err.Error() != expectedErr { + t.Errorf("MakeGroup() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } + + if status != NotSent { + t.Errorf("MakeGroup() did not return the expected status."+ + "\nexpected: %s\nreceived: %s", NotSent, status) + } +} + +// Error path: make sure an error and the correct status is returned when the +// membership list is too small. +func TestManager_MakeGroup_MembershipSizeError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + expectedErr := fmt.Sprintf(maxMembersErr, group.MaxParticipants+1, group.MaxParticipants) + + _, _, status, err := m.MakeGroup(make([]*id.ID, group.MaxParticipants+1), + nil, []byte{}) + if err == nil || err.Error() != expectedErr { + t.Errorf("MakeGroup() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } + + if status != NotSent { + t.Errorf("MakeGroup() did not return the expected status."+ + "\nexpected: %s\nreceived: %s", NotSent, status) + } +} + +// Error path: make sure an error and the correct status is returned when adding +// a group failed because the user is a part of too many groups already. +func TestManager_MakeGroup_AddGroupError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, gs.MaxGroupChats, 0, nil, nil, t) + memberIDs, _, _ := addPartners(m, t) + expectedErr := strings.SplitN(addGroupErr, "%", 2)[0] + + _, _, _, err := m.MakeGroup(memberIDs, []byte{}, []byte{}) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("MakeGroup() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of Manager.buildMembership. +func TestManager_buildMembership(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManager(prng, t) + memberIDs, expected, expectedDKL := addPartners(m, t) + + membership, dkl, err := m.buildMembership(memberIDs) + if err != nil { + t.Errorf("buildMembership() returned an error: %+v", err) + } + + if !reflect.DeepEqual(expected, membership) { + t.Errorf("buildMembership() failed to return the expected membership."+ + "\nexpected: %s\nrecieved: %s", expected, membership) + } + + if !reflect.DeepEqual(expectedDKL, dkl) { + t.Errorf("buildMembership() failed to return the expected DH key list."+ + "\nexpected: %#v\nrecieved: %#v", expectedDKL, dkl) + } +} + +// Error path: an error is returned when the number of members in the membership +// list is too few. +func TestManager_buildMembership_MinParticipantsError(t *testing.T) { + m, _ := newTestManager(rand.New(rand.NewSource(42)), t) + memberIDs := make([]*id.ID, group.MinParticipants-1) + expectedErr := fmt.Sprintf(minMembersErr, len(memberIDs), group.MinParticipants) + + _, _, err := m.buildMembership(memberIDs) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("buildMembership() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: an error is returned when the number of members in the membership +// list is too many. +func TestManager_buildMembership_MaxParticipantsError(t *testing.T) { + m, _ := newTestManager(rand.New(rand.NewSource(42)), t) + memberIDs := make([]*id.ID, group.MaxParticipants+1) + expectedErr := fmt.Sprintf(maxMembersErr, len(memberIDs), group.MaxParticipants) + + _, _, err := m.buildMembership(memberIDs) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("buildMembership() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: error returned when a partner cannot be found +func TestManager_buildMembership_GetPartnerContactError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManager(prng, t) + memberIDs, _, _ := addPartners(m, t) + expectedErr := strings.SplitN(getPartnerErr, "%", 2)[0] + + // Replace a partner ID + memberIDs[len(memberIDs)/2] = id.NewIdFromString("nonPartnerID", id.User, t) + + _, _, err := m.buildMembership(memberIDs) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("buildMembership() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: error returned when a member ID appears twice on the list. +func TestManager_buildMembership_DuplicateContactError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManager(prng, t) + memberIDs, _, _ := addPartners(m, t) + expectedErr := strings.SplitN(makeMembershipErr, "%", 2)[0] + + // Replace a partner ID + memberIDs[5] = memberIDs[4] + + _, _, err := m.buildMembership(memberIDs) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("buildMembership() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Test that getPreimages produces unique preimages. +func Test_getPreimages_Unique(t *testing.T) { + streamGen := fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG) + n := 100 + idPreimages := make(map[group.IdPreimage]bool, n) + keyPreimages := make(map[group.KeyPreimage]bool, n) + + for i := 0; i < n; i++ { + idPreimage, keyPreimage, err := getPreimages(streamGen) + if err != nil { + t.Errorf("getPreimages() returned an error: %+v", err) + } + + if idPreimages[idPreimage] { + t.Errorf("getPreimages() produced a duplicate idPreimage: %s", idPreimage) + } else { + idPreimages[idPreimage] = true + } + + if keyPreimages[keyPreimage] { + t.Errorf("getPreimages() produced a duplicate keyPreimage: %s", keyPreimage) + } else { + keyPreimages[keyPreimage] = true + } + } +} + +// Unit test of RequestStatus.String. +func TestRequestStatus_String(t *testing.T) { + statusCodes := map[RequestStatus]string{ + NotSent: "NotSent", + AllFail: "AllFail", + PartialSent: "PartialSent", + AllSent: "AllSent", + AllSent + 1: "INVALID STATUS", + } + + for status, expected := range statusCodes { + if status.String() != expected { + t.Errorf("String() failed to return the expected name."+ + "\nexpected: %s\nreceived: %s", expected, status.String()) + } + } +} + +// Unit test of RequestStatus.Message. +func TestRequestStatus_Message(t *testing.T) { + statusCodes := map[RequestStatus]string{ + NotSent: "an error occurred before sending any group requests", + AllFail: "all group requests failed to send", + PartialSent: "some group requests failed to send", + AllSent: "all groups requests successfully sent", + AllSent + 1: "INVALID STATUS " + strconv.Itoa(int(AllSent)+1), + } + + for status, expected := range statusCodes { + if status.Message() != expected { + t.Errorf("Message() failed to return the expected message."+ + "\nexpected: %s\nreceived: %s", expected, status.Message()) + } + } +} + +// addPartners returns a list of user IDs and their matching membership and adds +// them as partners. +func addPartners(m *Manager, t *testing.T) ([]*id.ID, group.Membership, gs.DhKeyList) { + memberIDs := make([]*id.ID, 10) + members := group.Membership{m.gs.GetUser()} + dkl := gs.DhKeyList{} + + for i := range memberIDs { + // Build member data + uid := id.NewIdFromUInt(uint64(i), id.User, t) + dhKey := m.store.E2e().GetGroup().NewInt(int64(i + 42)) + + // Add to lists + memberIDs[i] = uid + members = append(members, group.Member{ID: uid, DhKey: dhKey}) + dkl.Add(dhKey, group.Member{ID: uid, DhKey: dhKey}, m.store.E2e().GetGroup()) + + // Add partner + err := m.store.E2e().AddPartner(uid, dhKey, dhKey, + params.GetDefaultE2ESessionParams(), params.GetDefaultE2ESessionParams()) + if err != nil { + t.Errorf("Failed to add partner %d: %+v", i, err) + } + } + + return memberIDs, members, dkl +} diff --git a/groupChat/manager.go b/groupChat/manager.go new file mode 100644 index 0000000000000000000000000000000000000000..2e2d18fadee2e5e246cf3ea42951305e14053963 --- /dev/null +++ b/groupChat/manager.go @@ -0,0 +1,156 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/client/api" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/stoppable" + "gitlab.com/elixxir/client/storage" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/xx_network/primitives/id" +) + +const ( + rawMessageBuffSize = 100 + receiveStoppableName = "GroupChatReceive" + receiveListenerName = "GroupChatReceiveListener" + requestStoppableName = "GroupChatRequest" + requestListenerName = "GroupChatRequestListener" + groupStoppableName = "GroupChat" +) + +// Error messages. +const ( + newGroupStoreErr = "failed to create new group store: %+v" + joinGroupErr = "failed to join new group %s: %+v" + leaveGroupErr = "failed to leave group %s: %+v" +) + +// Manager handles the list of groups a user is a part of. +type Manager struct { + client *api.Client + store *storage.Session + swb interfaces.Switchboard + net interfaces.NetworkManager + rng *fastRNG.StreamGenerator + gs *gs.Store + + requestFunc RequestCallback + receiveFunc ReceiveCallback +} + +// NewManager generates a new group chat manager. This functions satisfies the +// GroupChat interface. +func NewManager(client *api.Client, requestFunc RequestCallback, + receiveFunc ReceiveCallback) (*Manager, error) { + return newManager( + client, + client.GetUser().ReceptionID.DeepCopy(), + client.GetStorage().E2e().GetDHPublicKey(), + client.GetStorage(), + client.GetSwitchboard(), + client.GetNetworkInterface(), + client.GetRng(), + client.GetStorage().GetKV(), + requestFunc, + receiveFunc, + ) +} + +// newManager creates a new group chat manager from api.Client parts for easier +// testing. +func newManager(client *api.Client, userID *id.ID, userDhKey *cyclic.Int, + store *storage.Session, swb interfaces.Switchboard, + net interfaces.NetworkManager, rng *fastRNG.StreamGenerator, + kv *versioned.KV, requestFunc RequestCallback, + receiveFunc ReceiveCallback) (*Manager, error) { + + // Load the group chat storage or create one if one does not exist + gStore, err := gs.NewOrLoadStore(kv, group.Member{ID: userID, DhKey: userDhKey}) + if err != nil { + return nil, errors.Errorf(newGroupStoreErr, err) + } + + return &Manager{ + client: client, + store: store, + swb: swb, + net: net, + rng: rng, + gs: gStore, + requestFunc: requestFunc, + receiveFunc: receiveFunc, + }, nil +} + +// StartProcesses starts the reception worker. +func (m *Manager) StartProcesses() (stoppable.Stoppable, error) { + // Start group reception worker + receiveStop := stoppable.NewSingle(receiveStoppableName) + receiveChan := make(chan message.Receive, rawMessageBuffSize) + m.swb.RegisterChannel(receiveListenerName, &id.ID{}, + message.Raw, receiveChan) + go m.receive(receiveChan, receiveStop) + + // Start group request worker + requestStop := stoppable.NewSingle(requestStoppableName) + requestChan := make(chan message.Receive, rawMessageBuffSize) + m.swb.RegisterChannel(requestListenerName, &id.ID{}, + message.GroupCreationRequest, requestChan) + go m.receiveRequest(requestChan, requestStop) + + // Create a multi stoppable + multiStoppable := stoppable.NewMulti(groupStoppableName) + multiStoppable.Add(receiveStop) + multiStoppable.Add(requestStop) + + return multiStoppable, nil +} + +// JoinGroup adds the group to the list of group chats the user is a part of. +// An error is returned if the user is already part of the group or if the +// maximum number of groups have already been joined. +func (m Manager) JoinGroup(g gs.Group) error { + if err := m.gs.Add(g); err != nil { + return errors.Errorf(joinGroupErr, g.ID, err) + } + + return nil +} + +// LeaveGroup removes a group from a list of groups the user is a part of. +func (m Manager) LeaveGroup(groupID *id.ID) error { + if err := m.gs.Remove(groupID); err != nil { + return errors.Errorf(leaveGroupErr, groupID, err) + } + + return nil +} + +// GetGroups returns a list of all registered groupChat IDs. +func (m Manager) GetGroups() []*id.ID { + return m.gs.GroupIDs() +} + +// GetGroup returns the group with the matching ID or returns false if none +// exist. +func (m Manager) GetGroup(groupID *id.ID) (gs.Group, bool) { + return m.gs.Get(groupID) +} + +// NumGroups returns the number of groups the user is a part of. +func (m Manager) NumGroups() int { + return m.gs.Len() +} diff --git a/groupChat/manager_test.go b/groupChat/manager_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0ea0f5341018fac4a24e72b999603d2a93086ed2 --- /dev/null +++ b/groupChat/manager_test.go @@ -0,0 +1,386 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "reflect" + "strings" + "testing" + "time" +) + +// Unit test of Manager.newManager. +func Test_newManager(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + user := group.Member{ + ID: id.NewIdFromString("userID", id.User, t), + DhKey: randCycInt(rand.New(rand.NewSource(42))), + } + requestChan := make(chan gs.Group) + requestFunc := func(g gs.Group) { requestChan <- g } + receiveChan := make(chan MessageReceive) + receiveFunc := func(msg MessageReceive) { receiveChan <- msg } + m, err := newManager(nil, user.ID, user.DhKey, nil, nil, nil, nil, kv, requestFunc, receiveFunc) + if err != nil { + t.Errorf("newManager() returned an error: %+v", err) + } + + if !m.gs.GetUser().Equal(user) { + t.Errorf("newManager() failed to create a store with the correct user."+ + "\nexpected: %s\nreceived: %s", user, m.gs.GetUser()) + } + + if m.gs.Len() != 0 { + t.Errorf("newManager() failed to create an empty store."+ + "\nexpected: %d\nreceived: %d", 0, m.gs.Len()) + } + + // Check if requestFunc works + go m.requestFunc(gs.Group{}) + select { + case <-requestChan: + case <-time.NewTimer(5 * time.Millisecond).C: + t.Errorf("Timed out waiting for requestFunc to be called.") + } + + // Check if receiveFunc works + go m.receiveFunc(MessageReceive{}) + select { + case <-receiveChan: + case <-time.NewTimer(5 * time.Millisecond).C: + t.Errorf("Timed out waiting for receiveFunc to be called.") + } +} + +// Tests that Manager.newManager loads a group storage when it exists. +func Test_newManager_LoadStorage(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := group.Member{ + ID: id.NewIdFromString("userID", id.User, t), + DhKey: randCycInt(rand.New(rand.NewSource(42))), + } + + gStore, err := gs.NewStore(kv, user) + if err != nil { + t.Errorf("Failed to create new group storage: %+v", err) + } + + for i := 0; i < 10; i++ { + err := gStore.Add(newTestGroup(getGroup(), getGroup().NewInt(42), prng, t)) + if err != nil { + t.Errorf("Failed to add group %d: %+v", i, err) + } + } + + m, err := newManager(nil, user.ID, user.DhKey, nil, nil, nil, nil, kv, nil, nil) + if err != nil { + t.Errorf("newManager() returned an error: %+v", err) + } + + if !reflect.DeepEqual(gStore, m.gs) { + t.Errorf("newManager() failed to load the expected storage."+ + "\nexpected: %+v\nreceived: %+v", gStore, m.gs) + } +} + +// Error path: an error is returned when a group cannot be loaded from storage. +func Test_newManager_LoadError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + kv := versioned.NewKV(make(ekv.Memstore)) + user := group.Member{ + ID: id.NewIdFromString("userID", id.User, t), + DhKey: randCycInt(rand.New(rand.NewSource(42))), + } + + gStore, err := gs.NewStore(kv, user) + if err != nil { + t.Errorf("Failed to create new group storage: %+v", err) + } + + g := newTestGroup(getGroup(), getGroup().NewInt(42), prng, t) + err = gStore.Add(g) + if err != nil { + t.Errorf("Failed to add group: %+v", err) + } + _ = kv.Prefix("GroupChatListStore").Delete("GroupChat/"+g.ID.String(), 0) + + expectedErr := strings.SplitN(newGroupStoreErr, "%", 2)[0] + + _, err = newManager(nil, user.ID, user.DhKey, nil, nil, nil, nil, kv, nil, nil) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("newManager() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// +// func TestManager_StartProcesses(t *testing.T) { +// jww.SetLogThreshold(jww.LevelTrace) +// jww.SetStdoutThreshold(jww.LevelTrace) +// prng := rand.New(rand.NewSource(42)) +// requestChan1 := make(chan gs.Group) +// requestFunc1 := func(g gs.Group) { requestChan1 <- g } +// receiveChan1 := make(chan MessageReceive) +// receiveFunc1 := func(msg MessageReceive) { receiveChan1 <- msg } +// requestChan2 := make(chan gs.Group) +// requestFunc2 := func(g gs.Group) { requestChan2 <- g } +// receiveChan2 := make(chan MessageReceive) +// receiveFunc2 := func(msg MessageReceive) { receiveChan2 <- msg } +// requestChan3 := make(chan gs.Group) +// requestFunc3 := func(g gs.Group) { requestChan3 <- g } +// receiveChan3 := make(chan MessageReceive) +// receiveFunc3 := func(msg MessageReceive) { receiveChan3 <- msg } +// +// m1, _ := newTestManagerWithStore(prng, 10, 0, requestFunc1, receiveFunc1, t) +// m2, _ := newTestManagerWithStore(prng, 10, 0, requestFunc2, receiveFunc2, t) +// m3, _ := newTestManagerWithStore(prng, 10, 0, requestFunc3, receiveFunc3, t) +// +// membership, err := group.NewMembership(m1.store.GetUser().GetContact(), +// m2.store.GetUser().GetContact(), m3.store.GetUser().GetContact()) +// if err != nil { +// t.Errorf("Failed to generate new membership: %+v", err) +// } +// +// dhKeys := gs.GenerateDhKeyList(m1.gs.GetUser().ID, +// m1.store.GetUser().E2eDhPrivateKey, membership, m1.store.E2e().GetGroup()) +// +// grp1 := newTestGroup(m1.store.E2e().GetGroup(), m1.store.GetUser().E2eDhPrivateKey, prng, t) +// grp1.Members = membership +// grp1.DhKeys = dhKeys +// grp1.ID = group.NewID(grp1.IdPreimage, grp1.Members) +// grp1.Key = group.NewKey(grp1.KeyPreimage, grp1.Members) +// grp2 := grp1.DeepCopy() +// grp2.DhKeys = gs.GenerateDhKeyList(m2.gs.GetUser().ID, +// m2.store.GetUser().E2eDhPrivateKey, membership, m2.store.E2e().GetGroup()) +// grp3 := grp1.DeepCopy() +// grp3.DhKeys = gs.GenerateDhKeyList(m3.gs.GetUser().ID, +// m3.store.GetUser().E2eDhPrivateKey, membership, m3.store.E2e().GetGroup()) +// +// err = m1.gs.Add(grp1) +// if err != nil { +// t.Errorf("Failed to add group to member 1: %+v", err) +// } +// err = m2.gs.Add(grp2) +// if err != nil { +// t.Errorf("Failed to add group to member 2: %+v", err) +// } +// err = m3.gs.Add(grp3) +// if err != nil { +// t.Errorf("Failed to add group to member 3: %+v", err) +// } +// +// _ = m1.StartProcesses() +// _ = m2.StartProcesses() +// _ = m3.StartProcesses() +// +// // Build request message +// requestMarshaled, err := proto.Marshal(&Request{ +// Name: grp1.Name, +// IdPreimage: grp1.IdPreimage.Bytes(), +// KeyPreimage: grp1.KeyPreimage.Bytes(), +// Members: grp1.Members.Serialize(), +// Message: grp1.InitMessage, +// }) +// if err != nil { +// t.Errorf("Failed to proto marshal message: %+v", err) +// } +// msg := message.Receive{ +// Payload: requestMarshaled, +// MessageType: message.GroupCreationRequest, +// Sender: m1.gs.GetUser().ID, +// } +// +// m2.swb.(*switchboard.Switchboard).Speak(msg) +// m3.swb.(*switchboard.Switchboard).Speak(msg) +// +// select { +// case received := <-requestChan2: +// if !reflect.DeepEqual(grp2, received) { +// t.Errorf("Failed to receive expected group on requestChan."+ +// "\nexpected: %#v\nreceived: %#v", grp2, received) +// } +// case <-time.NewTimer(5 * time.Millisecond).C: +// t.Error("Timed out waiting for request callback.") +// } +// +// select { +// case received := <-requestChan3: +// if !reflect.DeepEqual(grp3, received) { +// t.Errorf("Failed to receive expected group on requestChan."+ +// "\nexpected: %#v\nreceived: %#v", grp3, received) +// } +// case <-time.NewTimer(5 * time.Millisecond).C: +// t.Error("Timed out waiting for request callback.") +// } +// +// contents := []byte("Test group message.") +// timestamp := netTime.Now() +// +// // Create cMix message and get public message +// cMixMsg, err := m1.newCmixMsg(grp1, contents, timestamp, m2.gs.GetUser(), prng) +// if err != nil { +// t.Errorf("Failed to create new cMix message: %+v", err) +// } +// +// internalMsg, _ := newInternalMsg(cMixMsg.ContentsSize() - publicMinLen) +// internalMsg.SetTimestamp(timestamp) +// internalMsg.SetSenderID(m1.gs.GetUser().ID) +// internalMsg.SetPayload(contents) +// expectedMsgID := group.NewMessageID(grp1.ID, internalMsg.Marshal()) +// +// expectedMsg := MessageReceive{ +// GroupID: grp1.ID, +// ID: expectedMsgID, +// Payload: contents, +// SenderID: m1.gs.GetUser().ID, +// RoundTimestamp: timestamp.Local(), +// } +// +// msg = message.Receive{ +// Payload: cMixMsg.Marshal(), +// MessageType: message.Raw, +// Sender: m1.gs.GetUser().ID, +// RoundTimestamp: timestamp.Local(), +// } +// m2.swb.(*switchboard.Switchboard).Speak(msg) +// +// select { +// case received := <-receiveChan2: +// if !reflect.DeepEqual(expectedMsg, received) { +// t.Errorf("Failed to receive expected group on receiveChan."+ +// "\nexpected: %+v\nreceived: %+v", expectedMsg, received) +// } +// case <-time.NewTimer(5 * time.Millisecond).C: +// t.Error("Timed out waiting for receive callback.") +// } +// } + +// Unit test of Manager.JoinGroup. +func TestManager_JoinGroup(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + g := newTestGroup(m.store.E2e().GetGroup(), m.store.GetUser().E2eDhPrivateKey, prng, t) + + err := m.JoinGroup(g) + if err != nil { + t.Errorf("JoinGroup() returned an error: %+v", err) + } + + if _, exists := m.gs.Get(g.ID); !exists { + t.Errorf("JoinGroup() failed to add the group %s.", g.ID) + } +} + +// Error path: an error is returned when a group is joined twice. +func TestManager_JoinGroup_AddErr(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + expectedErr := strings.SplitN(joinGroupErr, "%", 2)[0] + + err := m.JoinGroup(g) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("JoinGroup() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of Manager.LeaveGroup. +func TestManager_LeaveGroup(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + err := m.LeaveGroup(g.ID) + if err != nil { + t.Errorf("LeaveGroup() returned an error: %+v", err) + } + + if _, exists := m.GetGroup(g.ID); exists { + t.Error("LeaveGroup() failed to delete the group.") + } +} + +// Error path: an error is returned when no group with the ID exists +func TestManager_LeaveGroup_NoGroupError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + expectedErr := strings.SplitN(leaveGroupErr, "%", 2)[0] + + err := m.LeaveGroup(id.NewIdFromString("invalidID", id.Group, t)) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("LeaveGroup() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of Manager.GetGroups. +func TestManager_GetGroups(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + list := m.GetGroups() + for i, gid := range list { + if err := m.gs.Remove(gid); err != nil { + t.Errorf("Group %s does not exist (%d): %+v", gid, i, err) + } + } + + if m.gs.Len() != 0 { + t.Errorf("GetGroups() returned %d IDs, which is %d less than is in "+ + "memory.", len(list), m.gs.Len()) + } +} + +// Unit test of Manager.GetGroup. +func TestManager_GetGroup(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + testGrp, exists := m.GetGroup(g.ID) + if !exists { + t.Error("GetGroup() failed to find a group that should exist.") + } + + if !reflect.DeepEqual(g, testGrp) { + t.Errorf("GetGroup() failed to return the expected group."+ + "\nexpected: %#v\nreceived: %#v", g, testGrp) + } + + testGrp, exists = m.GetGroup(id.NewIdFromString("invalidID", id.Group, t)) + if exists { + t.Errorf("GetGroup() returned a group that should not exist: %#v", testGrp) + } +} + +// Unit test of Manager.NumGroups. First a manager is created with 10 groups +// and the initial number is checked. Then the number of groups is checked after +// leaving each until the number left is 0. +func TestManager_NumGroups(t *testing.T) { + expectedNum := 10 + m, _ := newTestManagerWithStore(rand.New(rand.NewSource(42)), expectedNum, + 0, nil, nil, t) + + groups := append([]*id.ID{{}}, m.GetGroups()...) + + for i, gid := range groups { + _ = m.LeaveGroup(gid) + + if m.NumGroups() != expectedNum-i { + t.Errorf("NumGroups() failed to return the expected number of "+ + "groups (%d).\nexpected: %d\nreceived: %d", + i, expectedNum-i, m.NumGroups()) + } + } + +} diff --git a/groupChat/messageReceive.go b/groupChat/messageReceive.go new file mode 100644 index 0000000000000000000000000000000000000000..e607e7f01fcd1aa2e6bb4cee11356e3ea71814f5 --- /dev/null +++ b/groupChat/messageReceive.go @@ -0,0 +1,69 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "fmt" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "strconv" + "strings" + "time" +) + +// MessageReceive contains the GroupChat message and associated data that a user +// receives when getting a group message. +type MessageReceive struct { + GroupID *id.ID + ID group.MessageID + Payload []byte + SenderID *id.ID + RecipientID *id.ID + EphemeralID ephemeral.Id + Timestamp time.Time + RoundID id.Round + RoundTimestamp time.Time +} + +// String returns the MessageReceive as readable text. This functions satisfies +// the fmt.Stringer interface. +func (mr MessageReceive) String() string { + groupID := "<nil>" + if mr.GroupID != nil { + groupID = mr.GroupID.String() + } + + payload := "<nil>" + if mr.Payload != nil { + payload = fmt.Sprintf("%q", mr.Payload) + } + + senderID := "<nil>" + if mr.SenderID != nil { + senderID = mr.SenderID.String() + } + + recipientID := "<nil>" + if mr.RecipientID != nil { + recipientID = mr.RecipientID.String() + } + + str := make([]string, 0, 9) + str = append(str, "GroupID:"+groupID) + str = append(str, "ID:"+mr.ID.String()) + str = append(str, "Payload:"+payload) + str = append(str, "SenderID:"+senderID) + str = append(str, "RecipientID:"+recipientID) + str = append(str, "EphemeralID:"+strconv.FormatInt(mr.EphemeralID.Int64(), 10)) + str = append(str, "Timestamp:"+mr.Timestamp.String()) + str = append(str, "RoundID:"+strconv.FormatUint(uint64(mr.RoundID), 10)) + str = append(str, "RoundTimestamp:"+mr.RoundTimestamp.String()) + + return "{" + strings.Join(str, " ") + "}" +} diff --git a/groupChat/messageReceive_test.go b/groupChat/messageReceive_test.go new file mode 100644 index 0000000000000000000000000000000000000000..343f53774a08e59caed53a37d31a1a35f7047cd7 --- /dev/null +++ b/groupChat/messageReceive_test.go @@ -0,0 +1,70 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// +package groupChat + +import ( + "gitlab.com/elixxir/crypto/group" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "testing" + "time" +) + +// Unit test of MessageReceive.String. +func TestMessageReceive_String(t *testing.T) { + msg := MessageReceive{ + GroupID: id.NewIdFromString("GroupID", id.Group, t), + ID: group.MessageID{0, 1, 2, 3}, + Payload: []byte("Group message."), + SenderID: id.NewIdFromString("SenderID", id.User, t), + RecipientID: id.NewIdFromString("RecipientID", id.User, t), + EphemeralID: ephemeral.Id{0, 1, 2, 3}, + Timestamp: time.Date(1955, 11, 5, 12, 0, 0, 0, time.UTC), + RoundID: 42, + RoundTimestamp: time.Date(1955, 11, 5, 12, 1, 0, 0, time.UTC), + } + + expected := "{" + + "GroupID:R3JvdXBJRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE " + + "ID:AAECAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= " + + "Payload:\"Group message.\" " + + "SenderID:U2VuZGVySUQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD " + + "RecipientID:UmVjaXBpZW50SUQAAAAAAAAAAAAAAAAAAAAAAAAAAAAD " + + "EphemeralID:141843442434048 " + + "Timestamp:" + msg.Timestamp.String() + " " + + "RoundID:42 " + + "RoundTimestamp:" + msg.RoundTimestamp.String() + + "}" + + if msg.String() != expected { + t.Errorf("String() returned the incorrect string."+ + "\nexpected: %s\nreceived: %s", expected, msg.String()) + } +} + +// Tests that MessageReceive.String returns the expected value for a message +// with nil values. +func TestMessageReceive_String_NilMessageReceive(t *testing.T) { + msg := MessageReceive{} + + expected := "{" + + "GroupID:<nil> " + + "ID:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= " + + "Payload:<nil> " + + "SenderID:<nil> " + + "RecipientID:<nil> " + + "EphemeralID:0 " + + "Timestamp:0001-01-01 00:00:00 +0000 UTC " + + "RoundID:0 " + + "RoundTimestamp:0001-01-01 00:00:00 +0000 UTC" + + "}" + + if msg.String() != expected { + t.Errorf("String() returned the incorrect string."+ + "\nexpected: %s\nreceived: %s", expected, msg.String()) + } +} diff --git a/groupChat/publicFormat.go b/groupChat/publicFormat.go new file mode 100644 index 0000000000000000000000000000000000000000..ab88d9e09f9c7e5110404fca5fc473070b45c088 --- /dev/null +++ b/groupChat/publicFormat.go @@ -0,0 +1,120 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "encoding/base64" + "fmt" + "github.com/pkg/errors" + "gitlab.com/elixxir/crypto/group" +) + +// Sizes of marshaled data, in bytes. +const ( + saltLen = group.SaltLen + publicMinLen = saltLen +) + +// Error messages +const ( + newPublicSizeErr = "max message size %d < %d minimum required" + unmarshalPublicSizeErr = "size of data %d < %d minimum required" +) + +// publicMsg is contains the salt and encrypted data in a group message. +// +// +---------------------+ +// | data | +// +----------+----------+ +// | salt | payload | +// | 32 bytes | variable | +// +----------+----------+ +type publicMsg struct { + data []byte // Serial of all the parts of the message + salt []byte // 256-bit sender salt + payload []byte // Encrypted internalMsg +} + +// newPublicMsg creates a new publicMsg of size maxDataSize. An error is +// returned if the maxDataSize is smaller than the minimum newPublicMsg size. +func newPublicMsg(maxDataSize int) (publicMsg, error) { + if maxDataSize < publicMinLen { + return publicMsg{}, + errors.Errorf(newPublicSizeErr, maxDataSize, publicMinLen) + } + + return mapPublicMsg(make([]byte, maxDataSize)), nil +} + +// mapPublicMsg maps all the parts of the publicMsg to the passed in data. +func mapPublicMsg(data []byte) publicMsg { + return publicMsg{ + data: data, + salt: data[:saltLen], + payload: data[saltLen:], + } +} + +// unmarshalPublicMsg unmarshal the data into an publicMsg. An error is +// returned if the data length is smaller than the minimum allowed size. +func unmarshalPublicMsg(data []byte) (publicMsg, error) { + if len(data) < publicMinLen { + return publicMsg{}, + errors.Errorf(unmarshalPublicSizeErr, len(data), publicMinLen) + } + + return mapPublicMsg(data), nil +} + +// Marshal returns the serial of the publicMsg. +func (pm publicMsg) Marshal() []byte { + return pm.data +} + +// GetSalt returns the 256-bit salt. +func (pm publicMsg) GetSalt() [group.SaltLen]byte { + var salt [group.SaltLen]byte + copy(salt[:], pm.salt) + return salt +} + +// SetSalt sets the 256-bit salt. +func (pm publicMsg) SetSalt(salt [group.SaltLen]byte) { + copy(pm.salt, salt[:]) +} + +// GetPayload returns the payload truncated to the correct size. +func (pm publicMsg) GetPayload() []byte { + return pm.payload +} + +// SetPayload sets the payload and saves it size. +func (pm publicMsg) SetPayload(payload []byte) { + copy(pm.payload, payload) +} + +// GetPayloadSize returns the maximum size of the payload. +func (pm publicMsg) GetPayloadSize() int { + return len(pm.payload) +} + +// String prints a string representation of publicMsg. This functions satisfies +// the fmt.Stringer interface. +func (pm publicMsg) String() string { + salt := "<nil>" + if len(pm.salt) > 0 { + salt = base64.StdEncoding.EncodeToString(pm.salt) + } + + payload := "<nil>" + if len(pm.payload) > 0 { + payload = fmt.Sprintf("%q", pm.GetPayload()) + } + + return "{salt:" + salt + ", payload:" + payload + "}" +} diff --git a/groupChat/publicFormat_test.go b/groupChat/publicFormat_test.go new file mode 100644 index 0000000000000000000000000000000000000000..69884ff76856562e0d6f9ee3af03ad63e6eecb74 --- /dev/null +++ b/groupChat/publicFormat_test.go @@ -0,0 +1,162 @@ +package groupChat + +import ( + "bytes" + "fmt" + "math/rand" + "reflect" + "testing" +) + +// Unit test of newPublicMsg. +func Test_newPublicMsg(t *testing.T) { + maxDataSize := 2 * publicMinLen + im, err := newPublicMsg(maxDataSize) + if err != nil { + t.Errorf("newPublicMsg() returned an error: %+v", err) + } + + if len(im.data) != maxDataSize { + t.Errorf("newPublicMsg() set data to the wrong length."+ + "\nexpected: %d\nreceived: %d", maxDataSize, len(im.data)) + } +} + +// Error path: the maxDataSize is smaller than the minimum size. +func Test_newPublicMsg_PayloadSizeError(t *testing.T) { + maxDataSize := publicMinLen - 1 + expectedErr := fmt.Sprintf(newPublicSizeErr, maxDataSize, publicMinLen) + + _, err := newPublicMsg(maxDataSize) + if err == nil || err.Error() != expectedErr { + t.Errorf("newPublicMsg() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of mapPublicMsg. +func Test_mapPublicMsg(t *testing.T) { + // Create all the expected data + var salt [saltLen]byte + rand.New(rand.NewSource(42)).Read(salt[:]) + payload := []byte("Sample payload contents.") + + // Construct data into single slice + data := bytes.NewBuffer(nil) + data.Write(salt[:]) + data.Write(payload) + + // Map data + im := mapPublicMsg(data.Bytes()) + + // Check that the mapped values match the expected values + if !bytes.Equal(salt[:], im.salt) { + t.Errorf("mapPublicMsg() did not correctly map salt."+ + "\nexpected: %+v\nreceived: %+v", salt, im.salt) + } + + if !bytes.Equal(payload, im.payload) { + t.Errorf("mapPublicMsg() did not correctly map payload."+ + "\nexpected: %+v\nreceived: %+v", payload, im.payload) + } +} + +// Tests that a marshaled and unmarshalled publicMsg matches the original. +func Test_publicMsg_Marshal_unmarshalPublicMsg(t *testing.T) { + pm, _ := newPublicMsg(publicMinLen * 2) + var salt [saltLen]byte + rand.New(rand.NewSource(42)).Read(salt[:]) + pm.SetSalt(salt) + pm.SetPayload([]byte("Sample payload message.")) + + data := pm.Marshal() + + newPm, err := unmarshalPublicMsg(data) + if err != nil { + t.Errorf("unmarshalPublicMsg() returned an error: %+v", err) + } + + if !reflect.DeepEqual(pm, newPm) { + t.Errorf("unmarshalPublicMsg() did not return the expected publicMsg."+ + "\nexpected: %s\nreceived: %s", pm, newPm) + } +} + +// Error path: error is returned when the data is too short. +func Test_unmarshalPublicMsg(t *testing.T) { + expectedErr := fmt.Sprintf(unmarshalPublicSizeErr, 0, publicMinLen) + + _, err := unmarshalPublicMsg(nil) + if err == nil || err.Error() != expectedErr { + t.Errorf("unmarshalPublicMsg() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Happy path. +func Test_publicMsg_SetSalt_GetSalt(t *testing.T) { + pm, _ := newPublicMsg(publicMinLen * 2) + var salt [saltLen]byte + rand.New(rand.NewSource(42)).Read(salt[:]) + pm.SetSalt(salt) + + testSalt := pm.GetSalt() + if salt != testSalt { + t.Errorf("Failed to get original salt."+ + "\nexpected: %+v\nreceived: %+v", salt, testSalt) + } +} + +// Tests that the original payload matches the saved one. +func Test_publicMsg_SetPayload_GetPayload(t *testing.T) { + pm, _ := newPublicMsg(publicMinLen * 2) + payload := make([]byte, pm.GetPayloadSize()) + copy(payload, "Test payload message.") + pm.SetPayload(payload) + testPayload := pm.GetPayload() + + if !bytes.Equal(payload, testPayload) { + t.Errorf("Failed to get original sender payload."+ + "\nexpected: %q\nreceived: %q", payload, testPayload) + } +} + +// Happy path. +func Test_publicMsg_GetPayloadSize(t *testing.T) { + pm, _ := newPublicMsg(publicMinLen * 2) + + if publicMinLen != pm.GetPayloadSize() { + t.Errorf("GetPayloadSize() failed to return the correct size."+ + "\nexpected: %d\nreceived: %d", publicMinLen, pm.GetPayloadSize()) + } +} + +// Happy path. +func Test_publicMsg_String(t *testing.T) { + pm, _ := newPublicMsg(publicMinLen * 2) + var salt [saltLen]byte + rand.New(rand.NewSource(42)).Read(salt[:]) + pm.SetSalt(salt) + payload := []byte("Sample payload message.") + payload = append(payload, 0, 1, 2) + pm.SetPayload(payload) + + expected := `{salt:U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=, payload:"Sample payload message.\x00\x01\x02\x00\x00\x00\x00\x00\x00"}` + + if pm.String() != expected { + t.Errorf("String() failed to return the expected value."+ + "\nexpected: %s\nreceived: %s", expected, pm.String()) + } +} + +// Happy path: tests that String returns the expected string for a nil publicMsg. +func Test_publicMsg_String_NilInternalMessage(t *testing.T) { + pm := publicMsg{} + + expected := "{salt:<nil>, payload:<nil>}" + + if pm.String() != expected { + t.Errorf("String() failed to return the expected value."+ + "\nexpected: %s\nreceived: %s", expected, pm.String()) + } +} diff --git a/groupChat/receive.go b/groupChat/receive.go new file mode 100644 index 0000000000000000000000000000000000000000..64cf10b789b3d07bf9d1dbd82d6d76b15c91fe53 --- /dev/null +++ b/groupChat/receive.go @@ -0,0 +1,168 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/stoppable" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/xx_network/primitives/id" + "time" +) + +// Error messages. +const ( + newDecryptKeyErr = "failed to generate key for decrypting group payload: %+v" + unmarshalInternalMsgErr = "failed to unmarshal group internal message: %+v" + unmarshalSenderIdErr = "failed to unmarshal sender ID: %+v" + unmarshalPublicMsgErr = "failed to unmarshal group cMix message contents: %+v" + findGroupKeyFpErr = "failed to find group with key fingerprint matching %s" + genCryptKeyMacErr = "failed to generate encryption key for group " + + "cMix message because MAC verification failed (epoch %d could be off)" +) + +// receive starts the group message reception worker that waits for new group +// messages to arrive. +func (m Manager) receive(rawMsgs chan message.Receive, stop *stoppable.Single) { + jww.DEBUG.Print("Starting group message reception worker.") + + for { + select { + case <-stop.Quit(): + jww.DEBUG.Print("Stopping group message reception worker.") + stop.ToStopped() + return + case receiveMsg := <-rawMsgs: + jww.DEBUG.Print("Group message reception received cMix message.") + + // Attempt to read the message + g, msgID, timestamp, senderID, msg, err := m.readMessage(receiveMsg) + if err != nil { + jww.WARN.Printf("Group message reception failed to read cMix "+ + "message: %+v", err) + continue + } + + // If the message was read correctly, send it to the callback + go m.receiveFunc(MessageReceive{ + GroupID: g.ID, + ID: msgID, + Payload: msg, + SenderID: senderID, + RecipientID: receiveMsg.RecipientID, + EphemeralID: receiveMsg.EphemeralID, + Timestamp: receiveMsg.Timestamp, + RoundID: receiveMsg.RoundId, + RoundTimestamp: timestamp, + }) + } + } +} + +// readMessage returns the group, message ID, timestamp, sender ID, and message +// of a group message. The encrypted group message data is unmarshaled from a +// cMix message in the message.Receive and then decrypted and the MAC is +// verified. The group is found by finding the group with a matching key +// fingerprint. +func (m *Manager) readMessage(msg message.Receive) (gs.Group, group.MessageID, + time.Time, *id.ID, []byte, error) { + // Unmarshal payload into cMix message + cMixMsg := format.Unmarshal(msg.Payload) + + // Unmarshal cMix message contents to get public message format + publicMsg, err := unmarshalPublicMsg(cMixMsg.GetContents()) + if err != nil { + return gs.Group{}, group.MessageID{}, time.Time{}, nil, nil, + errors.Errorf(unmarshalPublicMsgErr, err) + } + + // Get the group from storage via key fingerprint lookup + g, exists := m.gs.GetByKeyFp(cMixMsg.GetKeyFP(), publicMsg.GetSalt()) + if !exists { + return gs.Group{}, group.MessageID{}, time.Time{}, nil, nil, + errors.Errorf(findGroupKeyFpErr, cMixMsg.GetKeyFP()) + } + + // Decrypt the payload and return the messages timestamp, sender ID, and + // message contents + messageID, timestamp, senderID, contents, err := m.decryptMessage( + g, cMixMsg, publicMsg, msg.RoundTimestamp) + return g, messageID, timestamp, senderID, contents, err +} + +// decryptMessage decrypts the group message payload and returns its message ID, +// timestamp, sender ID, and message contents. +func (m *Manager) decryptMessage(g gs.Group, cMixMsg format.Message, + publicMsg publicMsg, roundTimestamp time.Time) (group.MessageID, time.Time, + *id.ID, []byte, error) { + + key, err := getCryptKey(g.Key, publicMsg.GetSalt(), cMixMsg.GetMac(), + publicMsg.GetPayload(), g.DhKeys, roundTimestamp) + if err != nil { + return group.MessageID{}, time.Time{}, nil, nil, err + } + + // Decrypt internal message + decryptedPayload := group.Decrypt(key, cMixMsg.GetKeyFP(), + publicMsg.GetPayload()) + + // Unmarshal internal message + internalMsg, err := unmarshalInternalMsg(decryptedPayload) + if err != nil { + return group.MessageID{}, time.Time{}, nil, nil, + errors.Errorf(unmarshalInternalMsgErr, err) + } + + // Unmarshal sender ID + senderID, err := internalMsg.GetSenderID() + if err != nil { + return group.MessageID{}, time.Time{}, nil, nil, + errors.Errorf(unmarshalSenderIdErr, err) + } + + messageID := group.NewMessageID(g.ID, internalMsg.Marshal()) + + return messageID, internalMsg.GetTimestamp(), senderID, + internalMsg.GetPayload(), nil +} + +// getCryptKey generates the decryption key for a group internal message. The +// key is generated using the group key, an epoch, and a salt. The epoch is +// based off the round timestamp. So, to avoid missing the correct epoch, the +// current, past, and next epochs are checked until one of them produces a key +// that matches the message's MAC. The DH key is also unknown, so each member's +// DH key is tried until there is a match. +func getCryptKey(key group.Key, salt [group.SaltLen]byte, mac, payload []byte, + dhKeys gs.DhKeyList, roundTimestamp time.Time) (group.CryptKey, error) { + // Compute the current epoch + epoch := group.ComputeEpoch(roundTimestamp) + + for _, dhKey := range dhKeys { + + // Create a key with the correct epoch + for _, epoch := range []uint32{epoch, epoch - 1, epoch + 1} { + // Generate key + cryptKey, err := group.NewKdfKey(key, epoch, salt) + if err != nil { + return group.CryptKey{}, errors.Errorf(newDecryptKeyErr, err) + } + + // Return the key if the MAC matches + if group.CheckMAC(mac, cryptKey, payload, dhKey) { + return cryptKey, nil + } + } + } + + // Return an error if none of the epochs worked + return group.CryptKey{}, errors.Errorf(genCryptKeyMacErr, epoch) +} diff --git a/groupChat/receiveRequest.go b/groupChat/receiveRequest.go new file mode 100644 index 0000000000000000000000000000000000000000..e5c7576f174f793d29ef337e092c96e4d782c371 --- /dev/null +++ b/groupChat/receiveRequest.go @@ -0,0 +1,111 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/stoppable" + "gitlab.com/elixxir/crypto/group" +) + +// Error message. +const ( + sendMessageTypeErr = "message not of type GroupCreationRequest" + protoUnmarshalErr = "failed to unmarshal request: %+v" + deserializeMembershipErr = "failed to deserialize membership: %+v" +) + +// receiveRequest starts the group request reception worker that waits for new +// group requests to arrive. +func (m Manager) receiveRequest(rawMsgs chan message.Receive, stop *stoppable.Single) { + jww.DEBUG.Print("Starting group message request reception worker.") + + for { + select { + case <-stop.Quit(): + jww.DEBUG.Print("Stopping group message request reception worker.") + stop.ToStopped() + return + case sendMsg := <-rawMsgs: + jww.DEBUG.Print("Group message request received send message.") + + // Generate the group from the request message + g, err := m.readRequest(sendMsg) + if err != nil { + jww.WARN.Printf("Failed to read message as group request: %+v", + err) + continue + } + + // Call request callback with the new group if it does not already + // exist + if _, exists := m.GetGroup(g.ID); !exists { + go m.requestFunc(g) + } + } + } +} + +// readRequest returns the group describes in the group request message. An +// error is returned if the request is of the wrong type or cannot be read. +func (m *Manager) readRequest(msg message.Receive) (gs.Group, error) { + // Return an error if the message is not of the right type + if msg.MessageType != message.GroupCreationRequest { + return gs.Group{}, errors.New(sendMessageTypeErr) + } + + // Unmarshal the request message + request := &Request{} + err := proto.Unmarshal(msg.Payload, request) + if err != nil { + return gs.Group{}, errors.Errorf(protoUnmarshalErr, err) + } + + // Deserialize membership list + membership, err := group.DeserializeMembership(request.Members) + if err != nil { + return gs.Group{}, errors.Errorf(deserializeMembershipErr, err) + } + + // Get the relationship with the group leader + partner, err := m.store.E2e().GetPartner(membership[0].ID) + if err != nil { + return gs.Group{}, errors.Errorf(getPrivKeyErr, err) + } + + // Replace leader's public key with the one from the partnership + leaderPubKey := membership[0].DhKey.DeepCopy() + membership[0].DhKey = partner.GetPartnerOriginPublicKey() + + // Generate the DH keys with each group member + privKey := partner.GetMyOriginPrivateKey() + grp := m.store.E2e().GetGroup() + dkl := gs.GenerateDhKeyList(m.gs.GetUser().ID, privKey, membership, grp) + + // Restore the original public key for the leader so that the membership + // digest generated later is correct + membership[0].DhKey = leaderPubKey + + // Copy preimages + var idPreimage group.IdPreimage + copy(idPreimage[:], request.IdPreimage) + var keyPreimage group.KeyPreimage + copy(keyPreimage[:], request.KeyPreimage) + + // Create group ID and key + groupID := group.NewID(idPreimage, membership) + groupKey := group.NewKey(keyPreimage, membership) + + // Return the new group + return gs.NewGroup(request.Name, groupID, groupKey, idPreimage, keyPreimage, + request.Message, membership, dkl), nil +} diff --git a/groupChat/receiveRequest_test.go b/groupChat/receiveRequest_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6925853a325948b005c7c38e11f2a28621ab2b11 --- /dev/null +++ b/groupChat/receiveRequest_test.go @@ -0,0 +1,241 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "github.com/golang/protobuf/proto" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/stoppable" + "math/rand" + "strings" + "testing" + "time" +) + +// // Tests that the correct group is received from the request. +// func TestManager_receiveRequest(t *testing.T) { +// prng := rand.New(rand.NewSource(42)) +// requestChan := make(chan gs.Group) +// requestFunc := func(g gs.Group) { requestChan <- g } +// m, _ := newTestManagerWithStore(prng, 10, 0, requestFunc, nil, t) +// g := newTestGroupWithUser(m.store.E2e().GetGroup(), +// m.store.GetUser().ReceptionID, m.store.GetUser().E2eDhPublicKey, +// m.store.GetUser().E2eDhPrivateKey, prng, t) +// +// requestMarshaled, err := proto.Marshal(&Request{ +// Name: g.Name, +// IdPreimage: g.IdPreimage.Bytes(), +// KeyPreimage: g.KeyPreimage.Bytes(), +// Members: g.Members.Serialize(), +// Message: g.InitMessage, +// }) +// if err != nil { +// t.Errorf("Failed to marshal proto message: %+v", err) +// } +// +// msg := message.Receive{ +// Payload: requestMarshaled, +// MessageType: message.GroupCreationRequest, +// } +// +// rawMessages := make(chan message.Receive) +// quit := make(chan struct{}) +// go m.receiveRequest(rawMessages, quit) +// rawMessages <- msg +// +// select { +// case receivedGrp := <-requestChan: +// if !reflect.DeepEqual(g, receivedGrp) { +// t.Errorf("receiveRequest() failed to return the expected group."+ +// "\nexpected: %#v\nreceived: %#v", g, receivedGrp) +// } +// case <-time.NewTimer(5 * time.Millisecond).C: +// t.Error("Timed out while waiting for callback.") +// } +// } + +// Tests that the callback is not called when the group already exists in the +// manager. +func TestManager_receiveRequest_GroupExists(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + requestChan := make(chan gs.Group) + requestFunc := func(g gs.Group) { requestChan <- g } + m, g := newTestManagerWithStore(prng, 10, 0, requestFunc, nil, t) + + requestMarshaled, err := proto.Marshal(&Request{ + Name: g.Name, + IdPreimage: g.IdPreimage.Bytes(), + KeyPreimage: g.KeyPreimage.Bytes(), + Members: g.Members.Serialize(), + Message: g.InitMessage, + }) + if err != nil { + t.Errorf("Failed to marshal proto message: %+v", err) + } + + msg := message.Receive{ + Payload: requestMarshaled, + MessageType: message.GroupCreationRequest, + } + + rawMessages := make(chan message.Receive) + stop := stoppable.NewSingle("testStoppable") + go m.receiveRequest(rawMessages, stop) + rawMessages <- msg + + select { + case <-requestChan: + t.Error("receiveRequest() called the callback when the group already " + + "exists in the list.") + case <-time.NewTimer(5 * time.Millisecond).C: + } +} + +// Tests that the quit channel quits the worker. +func TestManager_receiveRequest_QuitChan(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + requestChan := make(chan gs.Group) + requestFunc := func(g gs.Group) { requestChan <- g } + m, _ := newTestManagerWithStore(prng, 10, 0, requestFunc, nil, t) + + rawMessages := make(chan message.Receive) + stop := stoppable.NewSingle("testStoppable") + done := make(chan struct{}) + go func() { + m.receiveRequest(rawMessages, stop) + done <- struct{}{} + }() + if err := stop.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } + + select { + case <-done: + case <-time.NewTimer(5 * time.Millisecond).C: + t.Error("receiveRequest() failed to close when the quit.") + } +} + +// Tests that the callback is not called when the send message is not of the +// correct type. +func TestManager_receiveRequest_SendMessageTypeError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + requestChan := make(chan gs.Group) + requestFunc := func(g gs.Group) { requestChan <- g } + m, _ := newTestManagerWithStore(prng, 10, 0, requestFunc, nil, t) + + msg := message.Receive{ + MessageType: message.NoType, + } + + rawMessages := make(chan message.Receive) + stop := stoppable.NewSingle("singleStoppable") + go m.receiveRequest(rawMessages, stop) + rawMessages <- msg + + select { + case receivedGrp := <-requestChan: + t.Errorf("Callback called when the message should have been skipped: %#v", + receivedGrp) + case <-time.NewTimer(5 * time.Millisecond).C: + } +} + +// // Unit test of readRequest. +// func TestManager_readRequest(t *testing.T) { +// m, g := newTestManager(rand.New(rand.NewSource(42)), t) +// _ = m.store.E2e().AddPartner( +// g.Members[0].ID, +// g.Members[0].DhKey, +// m.store.E2e().GetGroup().NewInt(43), +// params.GetDefaultE2ESessionParams(), +// params.GetDefaultE2ESessionParams(), +// ) +// +// requestMarshaled, err := proto.Marshal(&Request{ +// Name: g.Name, +// IdPreimage: g.IdPreimage.Bytes(), +// KeyPreimage: g.KeyPreimage.Bytes(), +// Members: g.Members.Serialize(), +// Message: g.InitMessage, +// }) +// if err != nil { +// t.Errorf("Failed to marshal proto message: %+v", err) +// } +// +// msg := message.Receive{ +// Payload: requestMarshaled, +// MessageType: message.GroupCreationRequest, +// } +// +// newGrp, err := m.readRequest(msg) +// if err != nil { +// t.Errorf("readRequest() returned an error: %+v", err) +// } +// +// if !reflect.DeepEqual(g, newGrp) { +// t.Errorf("readRequest() returned the wrong group."+ +// "\nexpected: %#v\nreceived: %#v", g, newGrp) +// } +// } + +// Error path: an error is returned if the message type is incorrect. +func TestManager_readRequest_MessageTypeError(t *testing.T) { + m, _ := newTestManager(rand.New(rand.NewSource(42)), t) + expectedErr := sendMessageTypeErr + msg := message.Receive{ + MessageType: message.NoType, + } + + _, err := m.readRequest(msg) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("readRequest() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: an error is returned if the proto message cannot be unmarshalled. +func TestManager_readRequest_ProtoUnmarshalError(t *testing.T) { + expectedErr := strings.SplitN(deserializeMembershipErr, "%", 2)[0] + m, _ := newTestManager(rand.New(rand.NewSource(42)), t) + + requestMarshaled, err := proto.Marshal(&Request{ + Members: []byte("Invalid membership serial."), + }) + if err != nil { + t.Errorf("Failed to marshal proto message: %+v", err) + } + + msg := message.Receive{ + Payload: requestMarshaled, + MessageType: message.GroupCreationRequest, + } + + _, err = m.readRequest(msg) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("readRequest() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: an error is returned if the membership cannot be deserialized. +func TestManager_readRequest_DeserializeMembershipError(t *testing.T) { + m, _ := newTestManager(rand.New(rand.NewSource(42)), t) + expectedErr := strings.SplitN(protoUnmarshalErr, "%", 2)[0] + msg := message.Receive{ + Payload: []byte("Invalid message."), + MessageType: message.GroupCreationRequest, + } + + _, err := m.readRequest(msg) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("readRequest() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} diff --git a/groupChat/receive_test.go b/groupChat/receive_test.go new file mode 100644 index 0000000000000000000000000000000000000000..36ea8ed2dbad4c10630630f198d07d4b6a96bf54 --- /dev/null +++ b/groupChat/receive_test.go @@ -0,0 +1,409 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "bytes" + "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/stoppable" + "gitlab.com/elixxir/crypto/e2e" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/xx_network/primitives/netTime" + "math/rand" + "reflect" + "strings" + "testing" + "time" +) + +// Tests that Manager.receive returns the correct message on the callback. +func TestManager_receive(t *testing.T) { + // Setup callback + msgChan := make(chan MessageReceive) + receiveFunc := func(msg MessageReceive) { msgChan <- msg } + + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, receiveFunc, t) + + // Create test parameters + contents := []byte("Test group message.") + timestamp := netTime.Now() + sender := m.gs.GetUser() + + expectedMsg := MessageReceive{ + GroupID: g.ID, + ID: group.MessageID{0, 1, 2, 3}, + Payload: contents, + SenderID: sender.ID, + RoundTimestamp: timestamp.Local(), + } + + // Create cMix message and get public message + cMixMsg, err := m.newCmixMsg(g, contents, timestamp, g.Members[4], prng) + if err != nil { + t.Errorf("Failed to create new cMix message: %+v", err) + } + + internalMsg, _ := newInternalMsg(cMixMsg.ContentsSize() - publicMinLen) + internalMsg.SetTimestamp(timestamp) + internalMsg.SetSenderID(m.gs.GetUser().ID) + internalMsg.SetPayload(contents) + expectedMsg.ID = group.NewMessageID(g.ID, internalMsg.Marshal()) + + receiveChan := make(chan message.Receive, 1) + stop := stoppable.NewSingle("singleStoppable") + + m.gs.SetUser(g.Members[4], t) + go m.receive(receiveChan, stop) + + receiveChan <- message.Receive{ + Payload: cMixMsg.Marshal(), + RoundTimestamp: timestamp, + } + + select { + case msg := <-msgChan: + if !reflect.DeepEqual(expectedMsg, msg) { + t.Errorf("Failed to received expected message."+ + "\nexpected: %+v\nreceived: %+v", expectedMsg, msg) + } + case <-time.NewTimer(10 * time.Millisecond).C: + t.Errorf("Timed out waiting to receive group message.") + } +} + +// Tests that the callback is not called when the message cannot be read. +func TestManager_receive_ReadMessageError(t *testing.T) { + // Setup callback + msgChan := make(chan MessageReceive) + receiveFunc := func(msg MessageReceive) { msgChan <- msg } + + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 0, nil, receiveFunc, t) + + receiveChan := make(chan message.Receive, 1) + stop := stoppable.NewSingle("singleStoppable") + + go m.receive(receiveChan, stop) + + receiveChan <- message.Receive{ + Payload: make([]byte, format.MinimumPrimeSize*2), + } + + select { + case <-msgChan: + t.Error("Callback called when message should have errored.") + case <-time.NewTimer(5 * time.Millisecond).C: + } +} + +// Tests that the quit channel exits the function. +func TestManager_receive_QuitChan(t *testing.T) { + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + receiveChan := make(chan message.Receive, 1) + stop := stoppable.NewSingle("singleStoppable") + doneChan := make(chan struct{}) + + go func() { + m.receive(receiveChan, stop) + doneChan <- struct{}{} + }() + + if err := stop.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } + + select { + case <-doneChan: + case <-time.NewTimer(10 * time.Millisecond).C: + t.Errorf("Timed out waiting for thread to quit.") + } +} + +// Tests that Manager.readMessage returns the message data for the correct group. +func TestManager_readMessage(t *testing.T) { + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, expectedGrp := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + // Create test parameters + expectedContents := []byte("Test group message.") + expectedTimestamp := netTime.Now() + sender := m.gs.GetUser() + + // Create cMix message and get public message + cMixMsg, err := m.newCmixMsg(expectedGrp, expectedContents, + expectedTimestamp, expectedGrp.Members[4], prng) + if err != nil { + t.Errorf("Failed to create new cMix message: %+v", err) + } + + internalMsg, _ := newInternalMsg(cMixMsg.ContentsSize() - publicMinLen) + internalMsg.SetTimestamp(expectedTimestamp) + internalMsg.SetSenderID(sender.ID) + internalMsg.SetPayload(expectedContents) + expectedMsgID := group.NewMessageID(expectedGrp.ID, internalMsg.Marshal()) + + // Build message.Receive + receiveMsg := message.Receive{ + ID: e2e.MessageID{}, + Payload: cMixMsg.Marshal(), + RoundTimestamp: expectedTimestamp, + } + + m.gs.SetUser(expectedGrp.Members[4], t) + g, messageID, timestamp, senderID, contents, err := m.readMessage(receiveMsg) + if err != nil { + t.Errorf("readMessage() returned an error: %+v", err) + } + + if !reflect.DeepEqual(expectedGrp, g) { + t.Errorf("readMessage() returned incorrect group."+ + "\nexpected: %#v\nreceived: %#v", expectedGrp, g) + } + + if expectedMsgID != messageID { + t.Errorf("readMessage() returned incorrect message ID."+ + "\nexpected: %s\nreceived: %s", expectedMsgID, messageID) + } + + if !expectedTimestamp.Equal(timestamp) { + t.Errorf("readMessage() returned incorrect timestamp."+ + "\nexpected: %s\nreceived: %s", expectedTimestamp, timestamp) + } + + if !sender.ID.Cmp(senderID) { + t.Errorf("readMessage() returned incorrect sender ID."+ + "\nexpected: %s\nreceived: %s", sender.ID, senderID) + } + + if !bytes.Equal(expectedContents, contents) { + t.Errorf("readMessage() returned incorrect message."+ + "\nexpected: %s\nreceived: %s", expectedContents, contents) + } +} + +// Error path: an error is returned when a group with a matching group +// fingerprint cannot be found. +func TestManager_readMessage_FindGroupKpError(t *testing.T) { + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + // Create test parameters + expectedContents := []byte("Test group message.") + expectedTimestamp := netTime.Now() + + // Create cMix message and get public message + cMixMsg, err := m.newCmixMsg(g, expectedContents, expectedTimestamp, g.Members[4], prng) + if err != nil { + t.Errorf("Failed to create new cMix message: %+v", err) + } + + cMixMsg.SetKeyFP(format.NewFingerprint([]byte("invalid Fingerprint"))) + + // Build message.Receive + receiveMsg := message.Receive{ + ID: e2e.MessageID{}, + Payload: cMixMsg.Marshal(), + RoundTimestamp: expectedTimestamp, + } + + expectedErr := strings.SplitN(findGroupKeyFpErr, "%", 2)[0] + + m.gs.SetUser(g.Members[4], t) + _, _, _, _, _, err = m.readMessage(receiveMsg) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("readMessage() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Tests that a cMix message created by Manager.newCmixMsg can be read by +// Manager.readMessage. +func TestManager_decryptMessage(t *testing.T) { + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, g := newTestManager(prng, t) + + // Create test parameters + expectedContents := []byte("Test group message.") + expectedTimestamp := netTime.Now() + + // Create cMix message and get public message + msg, err := m.newCmixMsg(g, expectedContents, expectedTimestamp, g.Members[4], prng) + if err != nil { + t.Errorf("Failed to create new cMix message: %+v", err) + } + publicMsg, err := unmarshalPublicMsg(msg.GetContents()) + if err != nil { + t.Errorf("Failed to unmarshal publicMsg: %+v", err) + } + + internalMsg, _ := newInternalMsg(publicMsg.GetPayloadSize()) + internalMsg.SetTimestamp(expectedTimestamp) + internalMsg.SetSenderID(m.gs.GetUser().ID) + internalMsg.SetPayload(expectedContents) + expectedMsgID := group.NewMessageID(g.ID, internalMsg.Marshal()) + + // Read message and check if the outputs are correct + messageID, timestamp, senderID, contents, err := m.decryptMessage(g, msg, + publicMsg, expectedTimestamp) + if err != nil { + t.Errorf("decryptMessage() returned an error: %+v", err) + } + + if expectedMsgID != messageID { + t.Errorf("decryptMessage() returned incorrect message ID."+ + "\nexpected: %s\nreceived: %s", expectedMsgID, messageID) + } + + if !expectedTimestamp.Equal(timestamp) { + t.Errorf("decryptMessage() returned incorrect timestamp."+ + "\nexpected: %s\nreceived: %s", expectedTimestamp, timestamp) + } + + if !m.gs.GetUser().ID.Cmp(senderID) { + t.Errorf("decryptMessage() returned incorrect sender ID."+ + "\nexpected: %s\nreceived: %s", m.gs.GetUser().ID, senderID) + } + + if !bytes.Equal(expectedContents, contents) { + t.Errorf("decryptMessage() returned incorrect message."+ + "\nexpected: %s\nreceived: %s", expectedContents, contents) + } +} + +// Error path: an error is returned when the wrong timestamp is passed in and +// the decryption key cannot be generated because of the wrong epoch. +func TestManager_decryptMessage_GetCryptKeyError(t *testing.T) { + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, g := newTestManager(prng, t) + + // Create test parameters + contents := []byte("Test group message.") + timestamp := netTime.Now() + + // Create cMix message and get public message + msg, err := m.newCmixMsg(g, contents, timestamp, g.Members[4], prng) + if err != nil { + t.Errorf("Failed to create new cMix message: %+v", err) + } + publicMsg, err := unmarshalPublicMsg(msg.GetContents()) + if err != nil { + t.Errorf("Failed to unmarshal publicMsg: %+v", err) + } + + // Check if error is correct + expectedErr := strings.SplitN(genCryptKeyMacErr, "%", 2)[0] + _, _, _, _, err = m.decryptMessage(g, msg, publicMsg, timestamp.Add(time.Hour)) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("decryptMessage() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: an error is returned when the decrypted payload cannot be +// unmarshaled. +func TestManager_decryptMessage_UnmarshalInternalMsgError(t *testing.T) { + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, g := newTestManager(prng, t) + + // Create test parameters + contents := []byte("Test group message.") + timestamp := netTime.Now() + + // Create cMix message and get public message + msg, err := m.newCmixMsg(g, contents, timestamp, g.Members[4], prng) + if err != nil { + t.Errorf("Failed to create new cMix message: %+v", err) + } + publicMsg, err := unmarshalPublicMsg(msg.GetContents()) + if err != nil { + t.Errorf("Failed to unmarshal publicMsg: %+v", err) + } + + // Modify publicMsg to have invalid payload + publicMsg = mapPublicMsg(publicMsg.Marshal()[:33]) + key, err := group.NewKdfKey(g.Key, group.ComputeEpoch(timestamp), publicMsg.GetSalt()) + if err != nil { + t.Errorf("failed to create new key: %+v", err) + } + msg.SetMac(group.NewMAC(key, publicMsg.GetPayload(), g.DhKeys[*g.Members[4].ID])) + + // Check if error is correct + expectedErr := strings.SplitN(unmarshalInternalMsgErr, "%", 2)[0] + _, _, _, _, err = m.decryptMessage(g, msg, publicMsg, timestamp) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("decryptMessage() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of getCryptKey. +func Test_getCryptKey(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + g := newTestGroup(getGroup(), getGroup().NewInt(42), prng, t) + salt, err := newSalt(prng) + if err != nil { + t.Errorf("failed to create new salt: %+v", err) + } + payload := []byte("payload") + ts := netTime.Now() + + expectedKey, err := group.NewKdfKey(g.Key, group.ComputeEpoch(ts.Add(5*time.Minute)), salt) + if err != nil { + t.Errorf("failed to create new key: %+v", err) + } + mac := group.NewMAC(expectedKey, payload, g.DhKeys[*g.Members[4].ID]) + + key, err := getCryptKey(g.Key, salt, mac, payload, g.DhKeys, ts) + if err != nil { + t.Errorf("getCryptKey() returned an error: %+v", err) + } + + if expectedKey != key { + t.Errorf("getCryptKey() did not return the expected key."+ + "\nexpected: %v\nreceived: %v", expectedKey, key) + } +} + +// Error path: return an error when the MAC cannot be verified because the +// timestamp is incorrect and generates the wrong epoch. +func Test_getCryptKey_EpochError(t *testing.T) { + expectedErr := strings.SplitN(genCryptKeyMacErr, "%", 2)[0] + + prng := rand.New(rand.NewSource(42)) + g := newTestGroup(getGroup(), getGroup().NewInt(42), prng, t) + salt, err := newSalt(prng) + if err != nil { + t.Errorf("failed to create new salt: %+v", err) + } + payload := []byte("payload") + ts := netTime.Now() + + key, err := group.NewKdfKey(g.Key, group.ComputeEpoch(ts), salt) + if err != nil { + t.Errorf("getCryptKey() returned an error: %+v", err) + } + mac := group.NewMAC(key, payload, g.Members[4].DhKey) + + _, err = getCryptKey(g.Key, salt, mac, payload, g.DhKeys, ts.Add(time.Hour)) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("getCryptKey() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} diff --git a/groupChat/send.go b/groupChat/send.go new file mode 100644 index 0000000000000000000000000000000000000000..f2baa0953555b13f335111af4d1dd7ae0e01731e --- /dev/null +++ b/groupChat/send.go @@ -0,0 +1,222 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "github.com/pkg/errors" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "io" + "time" +) + +// Error messages. +const ( + newCmixMsgErr = "failed to generate cMix messages for group chat: %+v" + sendManyCmixErr = "failed to send group chat message from member %s to group %s: %+v" + newCmixErr = "failed to generate cMix message for member %d with ID %s in group %s: %+v" + messageLenErr = "message length %d is greater than maximum message space %d" + newNoGroupErr = "failed to create message for group %s that cannot be found" + newKeyErr = "failed to generate key for encrypting group payload" + newPublicMsgErr = "failed to create new public group message for cMix message: %+v" + newInternalMsgErr = "failed to create new internal group message for cMix message: %+v" + saltReadErr = "failed to generate salt for group message: %+v" + saltReadLengthErr = "length of generated salt %d != %d required" +) + +// Send sends a message to all group members using Client.SendManyCMIX. The +// send fails if the message is too long. +func (m *Manager) Send(groupID *id.ID, message []byte) (id.Round, error) { + + // Create a cMix message for each group member + messages, err := m.createMessages(groupID, message) + if err != nil { + return 0, errors.Errorf(newCmixMsgErr, err) + } + + rid, _, err := m.net.SendManyCMIX(messages, params.GetDefaultCMIX()) + if err != nil { + return 0, errors.Errorf(sendManyCmixErr, m.gs.GetUser().ID, groupID, err) + } + + return rid, nil +} + +// createMessages generates a list of cMix messages and a list of corresponding +// recipient IDs. +func (m *Manager) createMessages(groupID *id.ID, msg []byte) (map[id.ID]format.Message, error) { + timeNow := netTime.Now() + + g, exists := m.gs.Get(groupID) + if !exists { + return map[id.ID]format.Message{}, errors.Errorf(newNoGroupErr, groupID) + } + + return m.newMessages(g, msg, timeNow) +} + +// newMessages is a private function that allows the passing in of a timestamp +// and streamGen instead of a fastRNG.StreamGenerator for easier testing. +func (m *Manager) newMessages(g gs.Group, msg []byte, + timestamp time.Time) (map[id.ID]format.Message, error) { + // Create list of cMix messages + messages := make(map[id.ID]format.Message) + + // Create channels to receive messages and errors on + type msgInfo struct { + msg format.Message + id *id.ID + } + msgChan := make(chan msgInfo, len(g.Members)-1) + errChan := make(chan error, len(g.Members)-1) + + // Create cMix messages in parallel + for i, member := range g.Members { + // Do not send to the sender + if m.gs.GetUser().ID.Cmp(member.ID) { + continue + } + + // Start thread to build cMix message + go func(member group.Member, i int) { + // Create new stream + rng := m.rng.GetStream() + defer rng.Close() + + // Add cMix message to list + cMixMsg, err := m.newCmixMsg(g, msg, timestamp, member, rng) + if err != nil { + errChan <- errors.Errorf(newCmixErr, i, member.ID, g.ID, err) + } + msgChan <- msgInfo{cMixMsg, member.ID} + + }(member, i) + } + + // Wait for messages or errors + for len(messages) < len(g.Members)-1 { + select { + case err := <-errChan: + // Return on the first error that occurs + return nil, err + case info := <-msgChan: + messages[*info.id] = info.msg + } + } + + return messages, nil +} + +// newCmixMsg generates a new cMix message to be sent to a group member. +func (m *Manager) newCmixMsg(g gs.Group, msg []byte, timestamp time.Time, + mem group.Member, rng io.Reader) (format.Message, error) { + + // Create three message layers + cmixMsg := format.NewMessage(m.store.Cmix().GetGroup().GetP().ByteLen()) + publicMsg, internalMsg, err := newMessageParts(cmixMsg.ContentsSize()) + if err != nil { + return cmixMsg, err + } + + // Return an error if the message is too large to fit in the payload + if internalMsg.GetPayloadMaxSize() < len(msg) { + return cmixMsg, errors.Errorf(messageLenErr, len(msg), + internalMsg.GetPayloadMaxSize()) + } + + // Generate 256-bit salt + salt, err := newSalt(rng) + if err != nil { + return cmixMsg, err + } + + // Generate key fingerprint + keyFp := group.NewKeyFingerprint(g.Key, salt, mem.ID) + + // Generate key + key, err := group.NewKdfKey(g.Key, group.ComputeEpoch(timestamp), salt) + if err != nil { + return cmixMsg, errors.WithMessage(err, newKeyErr) + } + + // Generate internal message + payload := setInternalPayload(internalMsg, timestamp, m.gs.GetUser().ID, msg) + + // Encrypt internal message + encryptedPayload := group.Encrypt(key, keyFp, payload) + + // Generate public message + publicPayload := setPublicPayload(publicMsg, salt, encryptedPayload) + + // Generate MAC + mac := group.NewMAC(key, encryptedPayload, g.DhKeys[*mem.ID]) + + // Construct cMix message + cmixMsg.SetContents(publicPayload) + cmixMsg.SetKeyFP(keyFp) + cmixMsg.SetMac(mac) + + return cmixMsg, nil +} + +// newMessageParts generates a public payload message and the internal payload +// message. An error is returned if the messages cannot fit in the payloadSize. +func newMessageParts(payloadSize int) (publicMsg, internalMsg, error) { + publicMsg, err := newPublicMsg(payloadSize) + if err != nil { + return publicMsg, internalMsg{}, errors.Errorf(newPublicMsgErr, err) + } + + internalMsg, err := newInternalMsg(publicMsg.GetPayloadSize()) + if err != nil { + return publicMsg, internalMsg, errors.Errorf(newInternalMsgErr, err) + } + + return publicMsg, internalMsg, nil +} + +// newSalt generates a new salt of the specified size. +func newSalt(rng io.Reader) ([group.SaltLen]byte, error) { + var salt [group.SaltLen]byte + n, err := rng.Read(salt[:]) + if err != nil { + return salt, errors.Errorf(saltReadErr, err) + } else if n != group.SaltLen { + return salt, errors.Errorf(saltReadLengthErr, group.SaltLen, n) + } + + return salt, nil +} + +// setInternalPayload sets the timestamp, sender ID, and message of the +// internalMsg and returns the marshal bytes. +func setInternalPayload(internalMsg internalMsg, timestamp time.Time, + sender *id.ID, msg []byte) []byte { + // Set timestamp, sender ID, and message to the internalMsg + internalMsg.SetTimestamp(timestamp) + internalMsg.SetSenderID(sender) + internalMsg.SetPayload(msg) + + // Return the payload marshaled + return internalMsg.Marshal() +} + +// setPublicPayload sets the salt and encrypted payload of the publicMsg and +// returns the marshal bytes. +func setPublicPayload(publicMsg publicMsg, salt [group.SaltLen]byte, + encryptedPayload []byte) []byte { + // Set salt and payload + publicMsg.SetSalt(salt) + publicMsg.SetPayload(encryptedPayload) + + return publicMsg.Marshal() +} diff --git a/groupChat/sendRequests.go b/groupChat/sendRequests.go new file mode 100644 index 0000000000000000000000000000000000000000..70c5501ec7c6492a1b33f5a5309b2602c38ad06e --- /dev/null +++ b/groupChat/sendRequests.go @@ -0,0 +1,129 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/xx_network/primitives/id" + "strings" +) + +// Error messages. +const ( + resendGroupIdErr = "cannot resend request to nonexistent group with ID %s" + protoMarshalErr = "failed to form outgoing group chat request: %+v" + sendE2eErr = "failed to send group request via E2E to member %s: %+v" + sendRequestAllErr = "failed to send all %d group request messages: %s" + sendRequestPartialErr = "failed to send %d/%d group request messages: %s" +) + +// ResendRequest allows a groupChat request to be sent again. +func (m Manager) ResendRequest(groupID *id.ID) ([]id.Round, RequestStatus, error) { + g, exists := m.gs.Get(groupID) + if !exists { + return nil, NotSent, errors.Errorf(resendGroupIdErr, groupID) + } + + return m.sendRequests(g) +} + +// sendRequests sends group requests to each member in the group except for the +// leader/sender +func (m Manager) sendRequests(g gs.Group) ([]id.Round, RequestStatus, error) { + // Build request message + requestMarshaled, err := proto.Marshal(&Request{ + Name: g.Name, + IdPreimage: g.IdPreimage.Bytes(), + KeyPreimage: g.KeyPreimage.Bytes(), + Members: g.Members.Serialize(), + Message: g.InitMessage, + }) + if err != nil { + return nil, NotSent, errors.Errorf(protoMarshalErr, err) + } + + // Create channel to return the results of each send on + n := len(g.Members) - 1 + type sendResults struct { + rounds []id.Round + err error + } + resultsChan := make(chan sendResults, n) + + // Send request to each member in the group except the leader/sender + for _, member := range g.Members[1:] { + go func(member group.Member) { + rounds, err := m.sendRequest(member.ID, requestMarshaled) + resultsChan <- sendResults{rounds, err} + }(member) + } + + // Block until each send returns + roundIDs := make(map[id.Round]struct{}) + var errs []string + for i := 0; i < n; { + select { + case results := <-resultsChan: + for _, rid := range results.rounds { + roundIDs[rid] = struct{}{} + } + if results.err != nil { + errs = append(errs, results.err.Error()) + } + i++ + } + } + + // If all sends returned an error, then return AllFail with a list of errors + if len(errs) == n { + return nil, AllFail, + errors.Errorf(sendRequestAllErr, len(errs), strings.Join(errs, "\n")) + } + + // If some sends returned an error, then return a list of round IDs for the + // successful sends and a list of errors for the failed ones + if len(errs) > 0 { + return roundIdMap2List(roundIDs), PartialSent, + errors.Errorf(sendRequestPartialErr, len(errs), n, + strings.Join(errs, "\n")) + } + + // If all sends succeeded, return a list of roundIDs + return roundIdMap2List(roundIDs), AllSent, nil +} + +// sendRequest sends the group request to the user via E2E. +func (m Manager) sendRequest(memberID *id.ID, request []byte) ([]id.Round, error) { + sendMsg := message.Send{ + Recipient: memberID, + Payload: request, + MessageType: message.GroupCreationRequest, + } + + rounds, _, _, err := m.net.SendE2E(sendMsg, params.GetDefaultE2E(), nil) + if err != nil { + return nil, errors.Errorf(sendE2eErr, memberID, err) + } + + return rounds, nil +} + +// roundIdMap2List converts the map of round IDs to a list of round IDs. +func roundIdMap2List(m map[id.Round]struct{}) []id.Round { + roundIDs := make([]id.Round, 0, len(m)) + for rid := range m { + roundIDs = append(roundIDs, rid) + } + + return roundIDs +} diff --git a/groupChat/sendRequests_test.go b/groupChat/sendRequests_test.go new file mode 100644 index 0000000000000000000000000000000000000000..56ca284fbc66cb78622388bd11d0454db3280bce --- /dev/null +++ b/groupChat/sendRequests_test.go @@ -0,0 +1,274 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "fmt" + "github.com/golang/protobuf/proto" + "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "reflect" + "sort" + "strings" + "testing" +) + +// Tests that Manager.ResendRequest sends all expected requests successfully. +func TestManager_ResendRequest(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + expected := &Request{ + Name: g.Name, + IdPreimage: g.IdPreimage.Bytes(), + KeyPreimage: g.KeyPreimage.Bytes(), + Members: g.Members.Serialize(), + Message: g.InitMessage, + } + + _, status, err := m.ResendRequest(g.ID) + if err != nil { + t.Errorf("ResendRequest() returned an error: %+v", err) + } + + if status != AllSent { + t.Errorf("ResendRequest() failed to return the expected status."+ + "\nexpected: %s\nreceived: %s", AllSent, status) + } + + if len(m.net.(*testNetworkManager).e2eMessages) < len(g.Members)-1 { + t.Errorf("ResendRequest() failed to send the correct number of requests."+ + "\nexpected: %d\nreceived: %d", len(g.Members)-1, + len(m.net.(*testNetworkManager).e2eMessages)) + } + + for i := 0; i < len(m.net.(*testNetworkManager).e2eMessages); i++ { + msg := m.net.(*testNetworkManager).GetE2eMsg(i) + + // Check if the message recipient is a member in the group + matchesMember := false + for j, m := range g.Members { + if msg.Recipient.Cmp(m.ID) { + matchesMember = true + g.Members = append(g.Members[:j], g.Members[j+1:]...) + break + } + } + if !matchesMember { + t.Errorf("Message %d has recipient ID %s that is not in membership.", + i, msg.Recipient) + } + + testRequest := &Request{} + err = proto.Unmarshal(msg.Payload, testRequest) + if err != nil { + t.Errorf("Failed to unmarshal proto message (%d): %+v", i, err) + } + + if expected.String() != testRequest.String() { + t.Errorf("Message %d has unexpected payload."+ + "\nexpected: %s\nreceived: %s", i, expected, testRequest) + } + } +} + +// Error path: an error is returned when no group with the corresponding group +// ID exists. +func TestManager_ResendRequest_GetGroupError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + expectedErr := strings.SplitN(resendGroupIdErr, "%", 2)[0] + + _, status, err := m.ResendRequest(id.NewIdFromString("invalidID", id.Group, t)) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("ResendRequest() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } + + if status != NotSent { + t.Errorf("ResendRequest() failed to return the expected status."+ + "\nexpected: %s\nreceived: %s", NotSent, status) + } +} + +// Tests that Manager.sendRequests sends all expected requests successfully. +func TestManager_sendRequests(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + expected := &Request{ + Name: g.Name, + IdPreimage: g.IdPreimage.Bytes(), + KeyPreimage: g.KeyPreimage.Bytes(), + Members: g.Members.Serialize(), + Message: g.InitMessage, + } + + _, status, err := m.sendRequests(g) + if err != nil { + t.Errorf("sendRequests() returned an error: %+v", err) + } + + if status != AllSent { + t.Errorf("sendRequests() failed to return the expected status."+ + "\nexpected: %s\nreceived: %s", AllSent, status) + } + + if len(m.net.(*testNetworkManager).e2eMessages) < len(g.Members)-1 { + t.Errorf("sendRequests() failed to send the correct number of requests."+ + "\nexpected: %d\nreceived: %d", len(g.Members)-1, + len(m.net.(*testNetworkManager).e2eMessages)) + } + + for i := 0; i < len(m.net.(*testNetworkManager).e2eMessages); i++ { + msg := m.net.(*testNetworkManager).GetE2eMsg(i) + + // Check if the message recipient is a member in the group + matchesMember := false + for j, m := range g.Members { + if msg.Recipient.Cmp(m.ID) { + matchesMember = true + g.Members = append(g.Members[:j], g.Members[j+1:]...) + break + } + } + if !matchesMember { + t.Errorf("Message %d has recipient ID %s that is not in membership.", + i, msg.Recipient) + } + + testRequest := &Request{} + err = proto.Unmarshal(msg.Payload, testRequest) + if err != nil { + t.Errorf("Failed to unmarshal proto message (%d): %+v", i, err) + } + + if expected.String() != testRequest.String() { + t.Errorf("Message %d has unexpected payload."+ + "\nexpected: %s\nreceived: %s", i, expected, testRequest) + } + } +} + +// Tests that Manager.sendRequests returns the correct status when all sends +// fail. +func TestManager_sendRequests_SendAllFail(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 1, nil, nil, t) + expectedErr := fmt.Sprintf(sendRequestAllErr, len(g.Members)-1, "") + + rounds, status, err := m.sendRequests(g) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("sendRequests() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } + + if status != AllFail { + t.Errorf("sendRequests() failed to return the expected status."+ + "\nexpected: %s\nreceived: %s", AllFail, status) + } + + if rounds != nil { + t.Errorf("sendRequests() returned rounds on failure."+ + "\nexpected: %v\nreceived: %v", nil, rounds) + } + + if len(m.net.(*testNetworkManager).e2eMessages) != 0 { + t.Errorf("sendRequests() sent %d messages when sending should have failed.", + len(m.net.(*testNetworkManager).e2eMessages)) + } +} + +// Tests that Manager.sendRequests returns the correct status when some of the +// sends fail. +func TestManager_sendRequests_SendPartialSent(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 2, nil, nil, t) + expectedErr := fmt.Sprintf(sendRequestPartialErr, (len(g.Members)-1)/2, + len(g.Members)-1, "") + + _, status, err := m.sendRequests(g) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("sendRequests() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } + + if status != PartialSent { + t.Errorf("sendRequests() failed to return the expected status."+ + "\nexpected: %s\nreceived: %s", PartialSent, status) + } + + if len(m.net.(*testNetworkManager).e2eMessages) != (len(g.Members)-1)/2+1 { + t.Errorf("sendRequests() sent %d out of %d expected messages.", + len(m.net.(*testNetworkManager).e2eMessages), (len(g.Members)-1)/2+1) + } +} + +// Unit test of Manager.sendRequest. +func TestManager_sendRequest(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + expected := message.Send{ + Recipient: g.Members[0].ID, + Payload: []byte("request message"), + MessageType: message.GroupCreationRequest, + } + _, err := m.sendRequest(expected.Recipient, expected.Payload) + if err != nil { + t.Errorf("sendRequest() returned an error: %+v", err) + } + + received := m.net.(*testNetworkManager).GetE2eMsg(0) + + if !reflect.DeepEqual(expected, received) { + t.Errorf("sendRequest() did not send the correct message."+ + "\nexpected: %+v\nreceived: %+v", expected, received) + } +} + +// Error path: an error is returned when SendE2E fails +func TestManager_sendRequest_SendE2eError(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 1, nil, nil, t) + expectedErr := strings.SplitN(sendE2eErr, "%", 2)[0] + + _, err := m.sendRequest(id.NewIdFromString("memberID", id.User, t), nil) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("sendRequest() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Unit test of roundIdMap2List. +func Test_roundIdMap2List(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + + // Construct map and expected list + n := 100 + expected := make([]id.Round, n) + ridMap := make(map[id.Round]struct{}, n) + for i := 0; i < n; i++ { + expected[i] = id.Round(prng.Uint64()) + ridMap[expected[i]] = struct{}{} + } + + // Create list of IDs from map + ridList := roundIdMap2List(ridMap) + + // Sort expected and received slices to see if they match + sort.Slice(expected, func(i, j int) bool { return expected[i] < expected[j] }) + sort.Slice(ridList, func(i, j int) bool { return ridList[i] < ridList[j] }) + + if !reflect.DeepEqual(expected, ridList) { + t.Errorf("roundIdMap2List() failed to return the expected list."+ + "\nexpected: %v\nreceived: %v", expected, ridList) + } + +} diff --git a/groupChat/send_test.go b/groupChat/send_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1db5cec791e7790bf519ab98030a088f423b076e --- /dev/null +++ b/groupChat/send_test.go @@ -0,0 +1,557 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "bytes" + "encoding/base64" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/client/storage" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "math/rand" + "strings" + "testing" + "time" +) + +// Unit test of Manager.Send. +func TestManager_Send(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + message := []byte("Group chat message.") + sender := m.gs.GetUser().DeepCopy() + + _, err := m.Send(g.ID, message) + if err != nil { + t.Errorf("Send() returned an error: %+v", err) + } + + // Get messages sent with or return an error if no messages were sent + var messages map[id.ID]format.Message + if len(m.net.(*testNetworkManager).messages) > 0 { + messages = m.net.(*testNetworkManager).GetMsgMap(0) + } else { + t.Error("No group cMix messages received.") + } + + timeNow := netTime.Now() + + // Loop through each message and make sure the recipient ID matches a member + // in the group and that each message can be decrypted and have the expected + // values + for rid, msg := range messages { + // Check if recipient ID is in member list + var foundMember group.Member + for _, mem := range g.Members { + if rid.Cmp(mem.ID) { + foundMember = mem + } + } + + // Error if the recipient ID is not found in the member list + if foundMember == (group.Member{}) { + t.Errorf("Failed to find ID %s in memorship list.", rid) + continue + } + + publicMsg, err := unmarshalPublicMsg(msg.GetContents()) + if err != nil { + t.Errorf("Failed to unmarshal publicMsg: %+v", err) + } + // Attempt to read the message + messageID, timestamp, senderID, readMsg, err := m.decryptMessage( + g, msg, publicMsg, timeNow) + if err != nil { + t.Errorf("Failed to read message for %s: %+v", rid.String(), err) + } + + internalMsg, _ := newInternalMsg(publicMsg.GetPayloadSize()) + internalMsg.SetTimestamp(timestamp) + internalMsg.SetSenderID(m.gs.GetUser().ID) + internalMsg.SetPayload(message) + expectedMsgID := group.NewMessageID(g.ID, internalMsg.Marshal()) + + if expectedMsgID != messageID { + t.Errorf("Message ID received for %s too different from expected."+ + "\nexpected: %s\nreceived: %s", &rid, expectedMsgID, messageID) + } + + if !timestamp.Round(5 * time.Second).Equal(timeNow.Round(5 * time.Second)) { + t.Errorf("Timestamp received for %s too different from expected."+ + "\nexpected: %s\nreceived: %s", &rid, timeNow, timestamp) + } + + if !senderID.Cmp(sender.ID) { + t.Errorf("Sender ID received for %s incorrect."+ + "\nexpected: %s\nreceived: %s", &rid, sender.ID, senderID) + } + + if !bytes.Equal(readMsg, message) { + t.Errorf("Message received for %s incorrect."+ + "\nexpected: %q\nreceived: %q", &rid, message, readMsg) + } + } +} + +// Error path: error is returned when the message is too large. +func TestManager_Send_CmixMessageError(t *testing.T) { + // Set up new test manager that will make SendManyCMIX error + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + expectedErr := strings.SplitN(newCmixMsgErr, "%", 2)[0] + + // Send message + _, err := m.Send(g.ID, make([]byte, 400)) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("Send() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: SendManyCMIX returns an error. +func TestManager_Send_SendManyCMIXError(t *testing.T) { + // Set up new test manager that will make SendManyCMIX error + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 1, nil, nil, t) + expectedErr := strings.SplitN(sendManyCmixErr, "%", 2)[0] + + // Send message + _, err := m.Send(g.ID, []byte("message")) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("Send() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } + + // If messages were added, then error + if len(m.net.(*testNetworkManager).messages) > 0 { + t.Error("Group cMix messages received when SendManyCMIX errors.") + } +} + +// Tests that Manager.createMessages generates the messages for the correct group. +func TestManager_createMessages(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + message := []byte("Test group message.") + sender := m.gs.GetUser() + messages, err := m.createMessages(g.ID, message) + if err != nil { + t.Errorf("createMessages() returned an error: %+v", err) + } + + recipients := append(g.Members[:2], g.Members[3:]...) + + i := 0 + for rid, msg := range messages { + for _, recipient := range recipients { + if !rid.Cmp(recipient.ID) { + continue + } + + publicMsg, err := unmarshalPublicMsg(msg.GetContents()) + if err != nil { + t.Errorf("Failed to unmarshal publicMsg: %+v", err) + } + + messageID, timestamp, testSender, testMessage, err := m.decryptMessage( + g, msg, publicMsg, netTime.Now()) + if err != nil { + t.Errorf("Failed to find member to read message %d: %+v", i, err) + } + + internalMsg, _ := newInternalMsg(publicMsg.GetPayloadSize()) + internalMsg.SetTimestamp(timestamp) + internalMsg.SetSenderID(m.gs.GetUser().ID) + internalMsg.SetPayload(message) + expectedMsgID := group.NewMessageID(g.ID, internalMsg.Marshal()) + + if messageID != expectedMsgID { + t.Errorf("Failed to read correct message ID for message %d."+ + "\nexpected: %s\nreceived: %s", i, expectedMsgID, messageID) + } + + if !sender.ID.Cmp(testSender) { + t.Errorf("Failed to read correct sender ID for message %d."+ + "\nexpected: %s\nreceived: %s", i, sender.ID, testSender) + } + + if !bytes.Equal(message, testMessage) { + t.Errorf("Failed to read correct message for message %d."+ + "\nexpected: %s\nreceived: %s", i, message, testMessage) + } + } + i++ + } +} + +// Error path: test that an error is returned when the group ID does not match a +// group in storage. +func TestManager_createMessages_InvalidGroupIdError(t *testing.T) { + expectedErr := strings.SplitN(newNoGroupErr, "%", 2)[0] + + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t) + + // Read message and make sure the error is expected + _, err := m.createMessages(id.NewIdFromString("invalidID", id.Group, t), nil) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("createMessages() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Tests that Manager.newMessage returns messages with correct data. +func TestGroup_newMessages(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + m, g := newTestManager(prng, t) + + message := []byte("Test group message.") + sender := m.gs.GetUser() + timestamp := netTime.Now() + messages, err := m.newMessages(g, message, timestamp) + if err != nil { + t.Errorf("newMessages() returned an error: %+v", err) + } + + recipients := append(g.Members[:2], g.Members[3:]...) + + i := 0 + for rid, msg := range messages { + for _, recipient := range recipients { + if !rid.Cmp(recipient.ID) { + continue + } + + publicMsg, err := unmarshalPublicMsg(msg.GetContents()) + if err != nil { + t.Errorf("Failed to unmarshal publicMsg: %+v", err) + } + + messageID, testTimestamp, testSender, testMessage, err := m.decryptMessage( + g, msg, publicMsg, netTime.Now()) + if err != nil { + t.Errorf("Failed to find member to read message %d.", i) + } + + internalMsg, _ := newInternalMsg(publicMsg.GetPayloadSize()) + internalMsg.SetTimestamp(timestamp) + internalMsg.SetSenderID(m.gs.GetUser().ID) + internalMsg.SetPayload(message) + expectedMsgID := group.NewMessageID(g.ID, internalMsg.Marshal()) + + if messageID != expectedMsgID { + t.Errorf("Failed to read correct message ID for message %d."+ + "\nexpected: %s\nreceived: %s", i, expectedMsgID, messageID) + } + + if !timestamp.Equal(testTimestamp) { + t.Errorf("Failed to read correct timeout for message %d."+ + "\nexpected: %s\nreceived: %s", i, timestamp, testTimestamp) + } + + if !sender.ID.Cmp(testSender) { + t.Errorf("Failed to read correct sender ID for message %d."+ + "\nexpected: %s\nreceived: %s", i, sender.ID, testSender) + } + + if !bytes.Equal(message, testMessage) { + t.Errorf("Failed to read correct message for message %d."+ + "\nexpected: %s\nreceived: %s", i, message, testMessage) + } + } + i++ + } +} + +// Error path: an error is returned when Manager.neCmixMsg returns an error. +func TestGroup_newMessages_NewCmixMsgError(t *testing.T) { + expectedErr := strings.SplitN(newCmixErr, "%", 2)[0] + prng := rand.New(rand.NewSource(42)) + m, g := newTestManager(prng, t) + + _, err := m.newMessages(g, make([]byte, 1000), netTime.Now()) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("newMessages() failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Tests that the message returned by newCmixMsg has all the expected parts. +func TestGroup_newCmixMsg(t *testing.T) { + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, g := newTestManager(prng, t) + + // Create test parameters + message := []byte("Test group message.") + mem := g.Members[3] + timeNow := netTime.Now() + + // Create cMix message + prng = rand.New(rand.NewSource(42)) + msg, err := m.newCmixMsg(g, message, timeNow, mem, prng) + if err != nil { + t.Errorf("newCmixMsg() returned an error: %+v", err) + } + + // Create expected salt + prng = rand.New(rand.NewSource(42)) + var salt [group.SaltLen]byte + prng.Read(salt[:]) + + // Create expected key + key, _ := group.NewKdfKey(g.Key, group.ComputeEpoch(timeNow), salt) + + // Create expected messages + cmixMsg := format.NewMessage(m.store.Cmix().GetGroup().GetP().ByteLen()) + publicMsg, _ := newPublicMsg(cmixMsg.ContentsSize()) + internalMsg, _ := newInternalMsg(publicMsg.GetPayloadSize()) + internalMsg.SetTimestamp(timeNow) + internalMsg.SetSenderID(m.gs.GetUser().ID) + internalMsg.SetPayload(message) + payload := internalMsg.Marshal() + + // Check if key fingerprint is correct + expectedFp := group.NewKeyFingerprint(g.Key, salt, mem.ID) + if expectedFp != msg.GetKeyFP() { + t.Errorf("newCmixMsg() returned message with wrong key fingerprint."+ + "\nexpected: %s\nreceived: %s", expectedFp, msg.GetKeyFP()) + } + + // Check if key MAC is correct + encryptedPayload := group.Encrypt(key, expectedFp, payload) + expectedMAC := group.NewMAC(key, encryptedPayload, g.DhKeys[*mem.ID]) + if !bytes.Equal(expectedMAC, msg.GetMac()) { + t.Errorf("newCmixMsg() returned message with wrong MAC."+ + "\nexpected: %+v\nreceived: %+v", expectedMAC, msg.GetMac()) + } + + // Attempt to unmarshal public group message + publicMsg, err = unmarshalPublicMsg(msg.GetContents()) + if err != nil { + t.Errorf("Failed to unmarshal cMix message contents: %+v", err) + } + + // Attempt to decrypt payload + decryptedPayload := group.Decrypt(key, expectedFp, publicMsg.GetPayload()) + internalMsg, err = unmarshalInternalMsg(decryptedPayload) + if err != nil { + t.Errorf("Failed to unmarshal decrypted payload contents: %+v", err) + } + + // Check for expected values in internal message + if !internalMsg.GetTimestamp().Equal(timeNow) { + t.Errorf("Internal message has wrong timestamp."+ + "\nexpected: %s\nreceived: %s", timeNow, internalMsg.GetTimestamp()) + } + sid, err := internalMsg.GetSenderID() + if err != nil { + t.Fatalf("Failed to get sender ID from internal message: %+v", err) + } + if !sid.Cmp(m.gs.GetUser().ID) { + t.Errorf("Internal message has wrong sender ID."+ + "\nexpected: %s\nreceived: %s", m.gs.GetUser().ID, sid) + } + if !bytes.Equal(internalMsg.GetPayload(), message) { + t.Errorf("Internal message has wrong payload."+ + "\nexpected: %s\nreceived: %s", message, internalMsg.GetPayload()) + } +} + +// Error path: reader returns an error. +func TestGroup_newCmixMsg_SaltReaderError(t *testing.T) { + expectedErr := strings.SplitN(saltReadErr, "%", 2)[0] + m := &Manager{store: storage.InitTestingSession(t)} + + _, err := m.newCmixMsg(gs.Group{}, []byte{}, time.Time{}, group.Member{}, strings.NewReader("")) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("newCmixMsg() failed to return the expected error"+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: size of message is too large for the internalMsg. +func TestGroup_newCmixMsg_InternalMsgSizeError(t *testing.T) { + expectedErr := strings.SplitN(messageLenErr, "%", 2)[0] + + // Create new test Manager and Group + prng := rand.New(rand.NewSource(42)) + m, g := newTestManager(prng, t) + + // Create test parameters + message := make([]byte, 341) + mem := group.Member{ID: id.NewIdFromString("memberID", id.User, t)} + + // Create cMix message + prng = rand.New(rand.NewSource(42)) + _, err := m.newCmixMsg(g, message, netTime.Now(), mem, prng) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("newCmixMsg() failed to return the expected error"+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: payload size too small to fit publicMsg. +func Test_newMessageParts_PublicMsgSizeErr(t *testing.T) { + expectedErr := strings.SplitN(newPublicMsgErr, "%", 2)[0] + + _, _, err := newMessageParts(publicMinLen - 1) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("newMessageParts() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: payload size too small to fit internalMsg. +func Test_newMessageParts_InternalMsgSizeErr(t *testing.T) { + expectedErr := strings.SplitN(newInternalMsgErr, "%", 2)[0] + + _, _, err := newMessageParts(publicMinLen) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("newMessageParts() did not return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Tests the consistency of newSalt. +func Test_newSalt_Consistency(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + expectedSalts := []string{ + "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=", + "39ebTXZCm2F6DJ+fDTulWwzA1hRMiIU1hBrL4HCbB1g=", + "CD9h03W8ArQd9PkZKeGP2p5vguVOdI6B555LvW/jTNw=", + "uoQ+6NY+jE/+HOvqVG2PrBPdGqwEzi6ih3xVec+ix44=", + "GwuvrogbgqdREIpC7TyQPKpDRlp4YgYWl4rtDOPGxPM=", + "rnvD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHA=", + "ceeWotwtwlpbdLLhKXBeJz8FySMmgo4rBW44F2WOEGE=", + "SYlH/fNEQQ7UwRYCP6jjV2tv7Sf/iXS6wMr9mtBWkrE=", + "NhnnOJZN/ceejVNDc2Yc/WbXT+weG4lJGrcjbkt1IWI=", + } + + for i, expected := range expectedSalts { + salt, err := newSalt(prng) + if err != nil { + t.Errorf("newSalt() returned an error (%d): %+v", i, err) + } + + saltString := base64.StdEncoding.EncodeToString(salt[:]) + + if expected != saltString { + t.Errorf("newSalt() did not return the expected salt (%d)."+ + "\nexpected: %s\nreceived: %s", i, expected, saltString) + } + + // fmt.Printf("\"%s\",\n", saltString) + } +} + +// Error path: reader returns an error. +func Test_newSalt_ReadError(t *testing.T) { + expectedErr := strings.SplitN(saltReadErr, "%", 2)[0] + + _, err := newSalt(strings.NewReader("")) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("newSalt() failed to return the expected error"+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Error path: reader fails to return enough bytes. +func Test_newSalt_ReadLengthError(t *testing.T) { + expectedErr := strings.SplitN(saltReadLengthErr, "%", 2)[0] + + _, err := newSalt(strings.NewReader("A")) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("newSalt() failed to return the expected error"+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Tests that the marshaled internalMsg can be unmarshaled and has all the +// original values. +func Test_setInternalPayload(t *testing.T) { + internalMsg, err := newInternalMsg(internalMinLen * 2) + if err != nil { + t.Errorf("Failed to create a new internalMsg: %+v", err) + } + + timestamp := netTime.Now() + sender := id.NewIdFromString("sender ID", id.User, t) + message := []byte("This is an internal message.") + + payload := setInternalPayload(internalMsg, timestamp, sender, message) + if err != nil { + t.Errorf("setInternalPayload() returned an error: %+v", err) + } + + // Attempt to unmarshal and check all values + unmarshalled, err := unmarshalInternalMsg(payload) + if err != nil { + t.Errorf("Failed to unmarshal internalMsg: %+v", err) + } + + if !timestamp.Equal(unmarshalled.GetTimestamp()) { + t.Errorf("Timestamp does not match original.\nexpected: %s\nreceived: %s", + timestamp, unmarshalled.GetTimestamp()) + } + + testSender, err := unmarshalled.GetSenderID() + if err != nil { + t.Errorf("Failed to get sender ID: %+v", err) + } + if !sender.Cmp(testSender) { + t.Errorf("Sender ID does not match original.\nexpected: %s\nreceived: %s", + sender, testSender) + } + + if !bytes.Equal(message, unmarshalled.GetPayload()) { + t.Errorf("Payload does not match original.\nexpected: %v\nreceived: %v", + message, unmarshalled.GetPayload()) + } +} + +// Tests that the marshaled publicMsg can be unmarshaled and has all the +// original values. +func Test_setPublicPayload(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + publicMsg, err := newPublicMsg(publicMinLen * 2) + if err != nil { + t.Errorf("Failed to create a new publicMsg: %+v", err) + } + + var salt [group.SaltLen]byte + prng.Read(salt[:]) + encryptedPayload := make([]byte, publicMsg.GetPayloadSize()) + copy(encryptedPayload, "This is an internal message.") + + payload := setPublicPayload(publicMsg, salt, encryptedPayload) + if err != nil { + t.Errorf("setPublicPayload() returned an error: %+v", err) + } + + // Attempt to unmarshal and check all values + unmarshalled, err := unmarshalPublicMsg(payload) + if err != nil { + t.Errorf("Failed to unmarshal publicMsg: %+v", err) + } + + if salt != unmarshalled.GetSalt() { + t.Errorf("Salt does not match original.\nexpected: %v\nreceived: %v", + salt, unmarshalled.GetSalt()) + } + + if !bytes.Equal(encryptedPayload, unmarshalled.GetPayload()) { + t.Errorf("Payload does not match original.\nexpected: %v\nreceived: %v", + encryptedPayload, unmarshalled.GetPayload()) + } +} diff --git a/groupChat/utils_test.go b/groupChat/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..df7fc4e35b68bad6d9a57fb7e2c34ef6f15613c0 --- /dev/null +++ b/groupChat/utils_test.go @@ -0,0 +1,343 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package groupChat + +import ( + "encoding/base64" + "github.com/pkg/errors" + gs "gitlab.com/elixxir/client/groupChat/groupStore" + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/client/network/gateway" + "gitlab.com/elixxir/client/stoppable" + "gitlab.com/elixxir/client/storage" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/client/switchboard" + "gitlab.com/elixxir/comms/network" + "gitlab.com/elixxir/crypto/contact" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/e2e" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/crypto/group" + "gitlab.com/elixxir/ekv" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/xx_network/comms/connect" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/crypto/large" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "gitlab.com/xx_network/primitives/ndf" + "math/rand" + "sync" + "testing" + "time" +) + +// newTestManager creates a new Manager for testing. +func newTestManager(rng *rand.Rand, t *testing.T) (*Manager, gs.Group) { + store := storage.InitTestingSession(t) + user := group.Member{ + ID: store.GetUser().ReceptionID, + DhKey: store.GetUser().E2eDhPublicKey, + } + + g := newTestGroupWithUser(store.E2e().GetGroup(), user.ID, user.DhKey, + store.GetUser().E2eDhPrivateKey, rng, t) + gStore, err := gs.NewStore(versioned.NewKV(make(ekv.Memstore)), user) + if err != nil { + t.Fatalf("Failed to create new group store: %+v", err) + } + m := &Manager{ + store: store, + rng: fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG), + gs: gStore, + } + return m, g +} + +// newTestManager creates a new Manager that has groups stored for testing. One +// of the groups in the list is also returned. +func newTestManagerWithStore(rng *rand.Rand, numGroups int, sendErr int, + requestFunc RequestCallback, receiveFunc ReceiveCallback, + t *testing.T) (*Manager, gs.Group) { + + store := storage.InitTestingSession(t) + + user := group.Member{ + ID: store.GetUser().ReceptionID, + DhKey: store.GetUser().E2eDhPublicKey, + } + + gStore, err := gs.NewStore(versioned.NewKV(make(ekv.Memstore)), user) + if err != nil { + t.Fatalf("Failed to create new group store: %+v", err) + } + + var g gs.Group + for i := 0; i < numGroups; i++ { + g = newTestGroupWithUser(store.E2e().GetGroup(), user.ID, user.DhKey, + store.GetUser().E2eDhPrivateKey, rng, t) + if err = gStore.Add(g); err != nil { + t.Fatalf("Failed to add group %d to group store: %+v", i, err) + } + } + + m := &Manager{ + store: store, + swb: switchboard.New(), + net: newTestNetworkManager(sendErr, t), + rng: fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG), + gs: gStore, + requestFunc: requestFunc, + receiveFunc: receiveFunc, + } + return m, g +} + +// getMembership returns a Membership with random members for testing. +func getMembership(size int, uid *id.ID, pubKey *cyclic.Int, grp *cyclic.Group, prng *rand.Rand, t *testing.T) group.Membership { + contacts := make([]contact.Contact, size) + for i := range contacts { + randId, _ := id.NewRandomID(prng, id.User) + contacts[i] = contact.Contact{ + ID: randId, + DhPubKey: grp.NewInt(int64(prng.Int31() + 1)), + } + } + + contacts[2].ID = uid + contacts[2].DhPubKey = pubKey + + membership, err := group.NewMembership(contacts[0], contacts[1:]...) + if err != nil { + t.Errorf("Failed to create new membership: %+v", err) + } + + return membership +} + +// newTestGroup generates a new group with random values for testing. +func newTestGroup(grp *cyclic.Group, privKey *cyclic.Int, rng *rand.Rand, t *testing.T) gs.Group { + // Generate name from base 64 encoded random data + nameBytes := make([]byte, 16) + rng.Read(nameBytes) + name := []byte(base64.StdEncoding.EncodeToString(nameBytes)) + + // Generate the message from base 64 encoded random data + msgBytes := make([]byte, 128) + rng.Read(msgBytes) + msg := []byte(base64.StdEncoding.EncodeToString(msgBytes)) + + membership := getMembership(10, id.NewIdFromString("userID", id.User, t), + randCycInt(rng), grp, rng, t) + + dkl := gs.GenerateDhKeyList(id.NewIdFromString("userID", id.User, t), privKey, membership, grp) + + idPreimage, err := group.NewIdPreimage(rng) + if err != nil { + t.Fatalf("Failed to generate new group ID preimage: %+v", err) + } + + keyPreimage, err := group.NewKeyPreimage(rng) + if err != nil { + t.Fatalf("Failed to generate new group key preimage: %+v", err) + } + + groupID := group.NewID(idPreimage, membership) + groupKey := group.NewKey(keyPreimage, membership) + + return gs.NewGroup(name, groupID, groupKey, idPreimage, keyPreimage, msg, + membership, dkl) +} + +// newTestGroup generates a new group with random values for testing. +func newTestGroupWithUser(grp *cyclic.Group, uid *id.ID, pubKey, + privKey *cyclic.Int, rng *rand.Rand, t *testing.T) gs.Group { + // Generate name from base 64 encoded random data + nameBytes := make([]byte, 16) + rng.Read(nameBytes) + name := []byte(base64.StdEncoding.EncodeToString(nameBytes)) + + // Generate the message from base 64 encoded random data + msgBytes := make([]byte, 128) + rng.Read(msgBytes) + msg := []byte(base64.StdEncoding.EncodeToString(msgBytes)) + + membership := getMembership(10, uid, pubKey, grp, rng, t) + + dkl := gs.GenerateDhKeyList(uid, privKey, membership, grp) + + idPreimage, err := group.NewIdPreimage(rng) + if err != nil { + t.Fatalf("Failed to generate new group ID preimage: %+v", err) + } + + keyPreimage, err := group.NewKeyPreimage(rng) + if err != nil { + t.Fatalf("Failed to generate new group key preimage: %+v", err) + } + + groupID := group.NewID(idPreimage, membership) + groupKey := group.NewKey(keyPreimage, membership) + + return gs.NewGroup(name, groupID, groupKey, idPreimage, keyPreimage, msg, + membership, dkl) +} + +// randCycInt returns a random cyclic int. +func randCycInt(rng *rand.Rand) *cyclic.Int { + return getGroup().NewInt(int64(rng.Int31() + 1)) +} + +func getGroup() *cyclic.Group { + return cyclic.NewGroup( + large.NewIntFromString(getNDF().E2E.Prime, 16), + large.NewIntFromString(getNDF().E2E.Generator, 16)) +} + +func newTestNetworkManager(sendErr int, t *testing.T) interfaces.NetworkManager { + instanceComms := &connect.ProtoComms{ + Manager: connect.NewManagerTesting(t), + } + + thisInstance, err := network.NewInstanceTesting(instanceComms, getNDF(), + getNDF(), nil, nil, t) + if err != nil { + t.Fatalf("Failed to create new test instance: %v", err) + } + + return &testNetworkManager{ + instance: thisInstance, + messages: []map[id.ID]format.Message{}, + sendErr: sendErr, + } +} + +// testNetworkManager is a test implementation of NetworkManager interface. +type testNetworkManager struct { + instance *network.Instance + messages []map[id.ID]format.Message + e2eMessages []message.Send + errSkip int + sendErr int + sync.RWMutex +} + +func (tnm *testNetworkManager) GetMsgMap(i int) map[id.ID]format.Message { + tnm.RLock() + defer tnm.RUnlock() + return tnm.messages[i] +} + +func (tnm *testNetworkManager) GetE2eMsg(i int) message.Send { + tnm.RLock() + defer tnm.RUnlock() + return tnm.e2eMessages[i] +} + +func (tnm *testNetworkManager) SendE2E(msg message.Send, _ params.E2E, _ *stoppable.Single) ([]id.Round, e2e.MessageID, time.Time, error) { + tnm.Lock() + defer tnm.Unlock() + + tnm.errSkip++ + if tnm.sendErr == 1 { + return nil, e2e.MessageID{}, time.Time{}, errors.New("SendE2E error") + } else if tnm.sendErr == 2 && tnm.errSkip%2 == 0 { + return nil, e2e.MessageID{}, time.Time{}, errors.New("SendE2E error") + } + + tnm.e2eMessages = append(tnm.e2eMessages, msg) + + return []id.Round{0, 1, 2, 3}, e2e.MessageID{}, time.Time{}, nil +} + +func (tnm *testNetworkManager) SendUnsafe(message.Send, params.Unsafe) ([]id.Round, error) { + return []id.Round{}, nil +} + +func (tnm *testNetworkManager) SendCMIX(format.Message, *id.ID, params.CMIX) (id.Round, ephemeral.Id, error) { + return 0, ephemeral.Id{}, nil +} + +func (tnm *testNetworkManager) SendManyCMIX(messages map[id.ID]format.Message, _ params.CMIX) (id.Round, []ephemeral.Id, error) { + if tnm.sendErr == 1 { + return 0, nil, errors.New("SendManyCMIX error") + } + + tnm.Lock() + defer tnm.Unlock() + + tnm.messages = append(tnm.messages, messages) + + return 0, nil, nil +} + +type dummyEventMgr struct{} + +func (d *dummyEventMgr) Report(p int, a, b, c string) {} +func (t *testNetworkManager) GetEventManager() interfaces.EventManager { + return &dummyEventMgr{} +} + +func (tnm *testNetworkManager) GetInstance() *network.Instance { return tnm.instance } +func (tnm *testNetworkManager) GetHealthTracker() interfaces.HealthTracker { return nil } +func (tnm *testNetworkManager) Follow(interfaces.ClientErrorReport) (stoppable.Stoppable, error) { + return nil, nil +} +func (tnm *testNetworkManager) CheckGarbledMessages() {} +func (tnm *testNetworkManager) InProgressRegistrations() int { return 0 } +func (tnm *testNetworkManager) GetSender() *gateway.Sender { return nil } +func (tnm *testNetworkManager) GetAddressSize() uint8 { return 0 } +func (tnm *testNetworkManager) RegisterAddressSizeNotification(string) (chan uint8, error) { + return nil, nil +} +func (tnm *testNetworkManager) UnregisterAddressSizeNotification(string) {} +func (tnm *testNetworkManager) SetPoolFilter(gateway.Filter) {} + +func getNDF() *ndf.NetworkDefinition { + return &ndf.NetworkDefinition{ + E2E: ndf.Group{ + Prime: "E2EE983D031DC1DB6F1A7A67DF0E9A8E5561DB8E8D49413394C049B7A" + + "8ACCEDC298708F121951D9CF920EC5D146727AA4AE535B0922C688B55B3D" + + "D2AEDF6C01C94764DAB937935AA83BE36E67760713AB44A6337C20E78615" + + "75E745D31F8B9E9AD8412118C62A3E2E29DF46B0864D0C951C394A5CBBDC" + + "6ADC718DD2A3E041023DBB5AB23EBB4742DE9C1687B5B34FA48C3521632C" + + "4A530E8FFB1BC51DADDF453B0B2717C2BC6669ED76B4BDD5C9FF558E88F2" + + "6E5785302BEDBCA23EAC5ACE92096EE8A60642FB61E8F3D24990B8CB12EE" + + "448EEF78E184C7242DD161C7738F32BF29A841698978825B4111B4BC3E1E" + + "198455095958333D776D8B2BEEED3A1A1A221A6E37E664A64B83981C46FF" + + "DDC1A45E3D5211AAF8BFBC072768C4F50D7D7803D2D4F278DE8014A47323" + + "631D7E064DE81C0C6BFA43EF0E6998860F1390B5D3FEACAF1696015CB79C" + + "3F9C2D93D961120CD0E5F12CBB687EAB045241F96789C38E89D796138E63" + + "19BE62E35D87B1048CA28BE389B575E994DCA755471584A09EC723742DC3" + + "5873847AEF49F66E43873", + Generator: "2", + }, + CMIX: ndf.Group{ + Prime: "9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642" + + "F0B5C48C8F7A41AADFA187324B87674FA1822B00F1ECF8136943D7C55757" + + "264E5A1A44FFE012E9936E00C1D3E9310B01C7D179805D3058B2A9F4BB6F" + + "9716BFE6117C6B5B3CC4D9BE341104AD4A80AD6C94E005F4B993E14F091E" + + "B51743BF33050C38DE235567E1B34C3D6A5C0CEAA1A0F368213C3D19843D" + + "0B4B09DCB9FC72D39C8DE41F1BF14D4BB4563CA28371621CAD3324B6A2D3" + + "92145BEBFAC748805236F5CA2FE92B871CD8F9C36D3292B5509CA8CAA77A" + + "2ADFC7BFD77DDA6F71125A7456FEA153E433256A2261C6A06ED3693797E7" + + "995FAD5AABBCFBE3EDA2741E375404AE25B", + Generator: "5C7FF6B06F8F143FE8288433493E4769C4D988ACE5BE25A0E2480" + + "9670716C613D7B0CEE6932F8FAA7C44D2CB24523DA53FBE4F6EC3595892D" + + "1AA58C4328A06C46A15662E7EAA703A1DECF8BBB2D05DBE2EB956C142A33" + + "8661D10461C0D135472085057F3494309FFA73C611F78B32ADBB5740C361" + + "C9F35BE90997DB2014E2EF5AA61782F52ABEB8BD6432C4DD097BC5423B28" + + "5DAFB60DC364E8161F4A2A35ACA3A10B1C4D203CC76A470A33AFDCBDD929" + + "59859ABD8B56E1725252D78EAC66E71BA9AE3F1DD2487199874393CD4D83" + + "2186800654760E1E34C09E4D155179F9EC0DC4473F996BDCE6EED1CABED8" + + "B6F116F7AD9CF505DF0F998E34AB27514B0FFE7", + }, + } +} diff --git a/interfaces/IsRunning.go b/interfaces/IsRunning.go deleted file mode 100644 index 950b842864712858b3d1dcf769f4765e5af86159..0000000000000000000000000000000000000000 --- a/interfaces/IsRunning.go +++ /dev/null @@ -1,8 +0,0 @@ -package interfaces - -// this interface is used to allow the follower to to be stopped later if it -// fails - -type Running interface{ - IsRunning()bool -} diff --git a/interfaces/event.go b/interfaces/event.go new file mode 100644 index 0000000000000000000000000000000000000000..f08ff547e637bf4b0fccde6d166d7e8cf21298d8 --- /dev/null +++ b/interfaces/event.go @@ -0,0 +1,16 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package interfaces + +// EventCallbackFunction defines the callback functions for client event reports +type EventCallbackFunction func(priority int, category, evtType, details string) + +// EventManager reporting api (used internally) +type EventManager interface { + Report(priority int, category, evtType, details string) +} diff --git a/interfaces/healthTracker.go b/interfaces/healthTracker.go index 39441984ee0088b9e82e33e7b7a11ab689288f00..0d746d50f73bc1215201c413e5d6d83e54bbbb55 100644 --- a/interfaces/healthTracker.go +++ b/interfaces/healthTracker.go @@ -8,8 +8,10 @@ package interfaces type HealthTracker interface { - AddChannel(chan bool) - AddFunc(f func(bool)) + AddChannel(chan bool) uint64 + RemoveChannel(uint64) + AddFunc(f func(bool)) uint64 + RemoveFunc(uint64) IsHealthy() bool WasHealthy() bool } diff --git a/interfaces/message/receiveMessage.go b/interfaces/message/receiveMessage.go index fad6fb750ccc21cc9f03391056a8559a53cd5159..d11f8880865e8c6db95086c054f554126835b5c2 100644 --- a/interfaces/message/receiveMessage.go +++ b/interfaces/message/receiveMessage.go @@ -15,12 +15,14 @@ import ( ) type Receive struct { - ID e2e.MessageID - Payload []byte - MessageType Type - Sender *id.ID - RecipientID *id.ID - EphemeralID ephemeral.Id - Timestamp time.Time - Encryption EncryptionType + ID e2e.MessageID + Payload []byte + MessageType Type + Sender *id.ID + RecipientID *id.ID + EphemeralID ephemeral.Id + RoundId id.Round + RoundTimestamp time.Time + Timestamp time.Time // Message timestamp of when the user sent + Encryption EncryptionType } diff --git a/interfaces/message/type.go b/interfaces/message/type.go index 71a1c72ff431ab1c6164856fe892e72af8c21a68..5c8012fea55a6f7c6afcc953ef37dbf0daa7b6e0 100644 --- a/interfaces/message/type.go +++ b/interfaces/message/type.go @@ -49,4 +49,8 @@ const ( KeyExchangeTrigger = 30 // Rekey confirmation message. Sent by partner to confirm completion of a rekey KeyExchangeConfirm = 31 + + /* Group chat message types */ + // A group chat request message sent to all members in a group. + GroupCreationRequest = 40 ) diff --git a/interfaces/networkManager.go b/interfaces/networkManager.go index 1daa7d38104731870459b1323c8d918401a398fd..072306fddc18f53c7f07299dcffb52e64cdce853 100644 --- a/interfaces/networkManager.go +++ b/interfaces/networkManager.go @@ -17,19 +17,39 @@ import ( "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" + "time" ) type NetworkManager interface { - SendE2E(m message.Send, p params.E2E) ([]id.Round, e2e.MessageID, error) + // The stoppable can be nil. + SendE2E(m message.Send, p params.E2E, stop *stoppable.Single) ([]id.Round, e2e.MessageID, time.Time, error) SendUnsafe(m message.Send, p params.Unsafe) ([]id.Round, error) SendCMIX(message format.Message, recipient *id.ID, p params.CMIX) (id.Round, ephemeral.Id, error) + SendManyCMIX(messages map[id.ID]format.Message, p params.CMIX) (id.Round, []ephemeral.Id, error) GetInstance() *network.Instance GetHealthTracker() HealthTracker + GetEventManager() EventManager GetSender() *gateway.Sender Follow(report ClientErrorReport) (stoppable.Stoppable, error) CheckGarbledMessages() InProgressRegistrations() int + + // GetAddressSize returns the current address size of IDs. Blocks until an + // address size is known. + GetAddressSize() uint8 + + // RegisterAddressSizeNotification returns a channel that will trigger for + // every address space size update. The provided tag is the unique ID for + // the channel. Returns an error if the tag is already used. + RegisterAddressSizeNotification(tag string) (chan uint8, error) + + // UnregisterAddressSizeNotification stops broadcasting address space size + // updates on the channel with the specified tag. + UnregisterAddressSizeNotification(tag string) + + // SetPoolFilter sets the filter used to filter gateway IDs. + SetPoolFilter(f gateway.Filter) } //for use in key exchange which needs to be callable inside of network -type SendE2E func(m message.Send, p params.E2E) ([]id.Round, e2e.MessageID, error) +type SendE2E func(m message.Send, p params.E2E, stop *stoppable.Single) ([]id.Round, e2e.MessageID, time.Time, error) diff --git a/interfaces/params/network.go b/interfaces/params/network.go index 24af3d540f2e7824328641c07ddf325c37e86dfc..d3ee2fd39f4dd528bcb62a972c135c9725eb9768 100644 --- a/interfaces/params/network.go +++ b/interfaces/params/network.go @@ -25,6 +25,12 @@ type Network struct { ParallelNodeRegistrations uint //How far back in rounds the network should actually check KnownRoundsThreshold uint + // Determines verbosity of network updates while polling + // If true, client receives a filtered set of updates + // If false, client receives the full list of network updates + FastPolling bool + // Messages will not be sent to Rounds containing these Nodes + BlacklistedNodes []string Rounds Messages @@ -42,6 +48,8 @@ func GetDefaultNetwork() Network { E2EParams: GetDefaultE2ESessionParams(), ParallelNodeRegistrations: 8, KnownRoundsThreshold: 1500, //5 rounds/sec * 60 sec/min * 5 min + FastPolling: true, + BlacklistedNodes: make([]string, 0), } n.Rounds = GetDefaultRounds() n.Messages = GetDefaultMessage() diff --git a/interfaces/params/rounds.go b/interfaces/params/rounds.go index 07c4c3c25d83f3c115263bef0269ab1c9226c7ee..3e39ad47827e5f5f6f9dc526fcfffa200f5cf5a8 100644 --- a/interfaces/params/rounds.go +++ b/interfaces/params/rounds.go @@ -31,6 +31,14 @@ type Rounds struct { // Maximum number of times a historical round lookup will be attempted MaxHistoricalRoundsRetries uint + + // Interval between checking for rounds in UncheckedRoundStore + // due for a message retrieval retry + UncheckRoundPeriod time.Duration + + // Toggles if message pickup retrying mechanism if forced + // by intentionally not looking up messages + ForceMessagePickupRetry bool } func GetDefaultRounds() Rounds { @@ -43,5 +51,7 @@ func GetDefaultRounds() Rounds { LookupRoundsBufferLen: 2000, ForceHistoricalRounds: false, MaxHistoricalRoundsRetries: 3, + UncheckRoundPeriod: 20 * time.Second, + ForceMessagePickupRetry: false, } } diff --git a/interfaces/user/user.go b/interfaces/user/user.go index 7d480700e0e1c048b4ac9fcd98edb6fb25112b51..8788856b494cf386dbd903510680935f2b320daa 100644 --- a/interfaces/user/user.go +++ b/interfaces/user/user.go @@ -13,6 +13,7 @@ import ( "gitlab.com/elixxir/primitives/fact" "gitlab.com/xx_network/crypto/signature/rsa" "gitlab.com/xx_network/primitives/id" + "time" ) type User struct { @@ -24,6 +25,8 @@ type User struct { ReceptionSalt []byte ReceptionRSA *rsa.PrivateKey Precanned bool + // Timestamp in which user has registered with the network + RegistrationTimestamp time.Time //cmix Identity CmixDhPrivateKey *cyclic.Int diff --git a/keyExchange/confirm.go b/keyExchange/confirm.go index 01c43c5d42dd602fb314815d993b20ab7d16e401..45224193f80a302ae24e4e4f01ec0f7afe647638 100644 --- a/keyExchange/confirm.go +++ b/keyExchange/confirm.go @@ -23,6 +23,7 @@ func startConfirm(sess *storage.Session, c chan message.Receive, select { case <-stop.Quit(): cleanup() + stop.ToStopped() return case confirmation := <-c: handleConfirm(sess, confirmation) @@ -82,11 +83,11 @@ func unmarshalConfirm(payload []byte) (e2e.SessionID, error) { "unmarshal payload: %s", err) } - confimedSessionID := e2e.SessionID{} - if err := confimedSessionID.Unmarshal(msg.SessionID); err != nil { + confirmedSessionID := e2e.SessionID{} + if err := confirmedSessionID.Unmarshal(msg.SessionID); err != nil { return e2e.SessionID{}, errors.Errorf("Failed to unmarshal"+ " sessionID: %s", err) } - return confimedSessionID, nil + return confirmedSessionID, nil } diff --git a/keyExchange/confirm_test.go b/keyExchange/confirm_test.go index 8cef6f6f60488568614208f751914332ca5267f9..10c5ab525f3bf266f8548a6503bf14ee4c011266 100644 --- a/keyExchange/confirm_test.go +++ b/keyExchange/confirm_test.go @@ -20,8 +20,14 @@ import ( // Smoke test for handleTrigger func TestHandleConfirm(t *testing.T) { // Generate alice and bob's session - aliceSession, _ := InitTestingContextGeneric(t) - bobSession, _ := InitTestingContextGeneric(t) + aliceSession, _, err := InitTestingContextGeneric(t) + if err != nil { + t.Fatalf("Failed to create alice session: %v", err) + } + bobSession, _, err := InitTestingContextGeneric(t) + if err != nil { + t.Fatalf("Failed to create bob session: %v", err) + } // Maintain an ID for bob bobID := id.NewIdFromBytes([]byte("test"), t) diff --git a/keyExchange/exchange.go b/keyExchange/exchange.go index 42b7a177e10595bf45dd1b11fe7715e3a0b0c4f2..121d3b2b4e0853d96a3509a188fe5da56e264179 100644 --- a/keyExchange/exchange.go +++ b/keyExchange/exchange.go @@ -22,7 +22,7 @@ const keyExchangeConfirmName = "KeyExchangeConfirm" const keyExchangeMulti = "KeyExchange" func Start(switchboard *switchboard.Switchboard, sess *storage.Session, net interfaces.NetworkManager, - params params.Rekey) stoppable.Stoppable { + params params.Rekey) (stoppable.Stoppable, error) { // register the rekey trigger thread triggerCh := make(chan message.Receive, 100) @@ -57,5 +57,5 @@ func Start(switchboard *switchboard.Switchboard, sess *storage.Session, net inte exchangeStop := stoppable.NewMulti(keyExchangeMulti) exchangeStop.Add(triggerStop) exchangeStop.Add(confirmStop) - return exchangeStop + return exchangeStop, nil } diff --git a/keyExchange/rekey.go b/keyExchange/rekey.go index f37e2e679715a3750ec40a8f9deed924476c32ad..5acc9ba71f383b5e8ae15966421ecd227ed7f6e7 100644 --- a/keyExchange/rekey.go +++ b/keyExchange/rekey.go @@ -15,6 +15,7 @@ import ( "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/interfaces/utility" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/client/storage/e2e" "gitlab.com/elixxir/comms/network" @@ -25,10 +26,11 @@ import ( ) func CheckKeyExchanges(instance *network.Instance, sendE2E interfaces.SendE2E, - sess *storage.Session, manager *e2e.Manager, sendTimeout time.Duration) { + sess *storage.Session, manager *e2e.Manager, sendTimeout time.Duration, + stop *stoppable.Single) { sessions := manager.TriggerNegotiations() for _, session := range sessions { - go trigger(instance, sendE2E, sess, manager, session, sendTimeout) + go trigger(instance, sendE2E, sess, manager, session, sendTimeout, stop) } } @@ -38,7 +40,7 @@ func CheckKeyExchanges(instance *network.Instance, sendE2E interfaces.SendE2E, // session while the latter on an extant session func trigger(instance *network.Instance, sendE2E interfaces.SendE2E, sess *storage.Session, manager *e2e.Manager, session *e2e.Session, - sendTimeout time.Duration) { + sendTimeout time.Duration, stop *stoppable.Single) { var negotiatingSession *e2e.Session jww.INFO.Printf("Negotation triggered for session %s with "+ "status: %s", session, session.NegotiationStatus()) @@ -61,7 +63,7 @@ func trigger(instance *network.Instance, sendE2E interfaces.SendE2E, } // send the rekey notification to the partner - err := negotiate(instance, sendE2E, sess, negotiatingSession, sendTimeout) + err := negotiate(instance, sendE2E, sess, negotiatingSession, sendTimeout, stop) // if sending the negotiation fails, revert the state of the session to // unconfirmed so it will be triggered in the future if err != nil { @@ -71,8 +73,8 @@ func trigger(instance *network.Instance, sendE2E interfaces.SendE2E, } func negotiate(instance *network.Instance, sendE2E interfaces.SendE2E, - sess *storage.Session, session *e2e.Session, - sendTimeout time.Duration) error { + sess *storage.Session, session *e2e.Session, sendTimeout time.Duration, + stop *stoppable.Single) error { e2eStore := sess.E2e() //generate public key @@ -102,7 +104,7 @@ func negotiate(instance *network.Instance, sendE2E interfaces.SendE2E, e2eParams := params.GetDefaultE2E() e2eParams.Type = params.KeyExchange - rounds, _, err := sendE2E(m, e2eParams) + rounds, _, _, err := sendE2E(m, e2eParams, stop) // If the send fails, returns the error so it can be handled. The caller // should ensure the calling session is in a state where the Rekey will // be triggered next time a key is used @@ -137,7 +139,7 @@ func negotiate(instance *network.Instance, sendE2E interfaces.SendE2E, // otherwise, the transmission is a success and this should be denoted // in the session and the log - jww.INFO.Printf("Key Negotiation transmission for %s sucesful", + jww.INFO.Printf("Key Negotiation transmission for %s successful", session) session.SetNegotiationStatus(e2e.Sent) diff --git a/keyExchange/trigger.go b/keyExchange/trigger.go index a72da7b110c92e0065a5f9acaf79698565f8815c..e25da821f9a3485969608bd320a7f22534622255 100644 --- a/keyExchange/trigger.go +++ b/keyExchange/trigger.go @@ -32,14 +32,15 @@ const ( func startTrigger(sess *storage.Session, net interfaces.NetworkManager, c chan message.Receive, stop *stoppable.Single, params params.Rekey, cleanup func()) { - for true { + for { select { case <-stop.Quit(): cleanup() + stop.ToStopped() return case request := <-c: go func() { - err := handleTrigger(sess, net, request, params) + err := handleTrigger(sess, net, request, params, stop) if err != nil { jww.ERROR.Printf(errFailed, err) } @@ -49,7 +50,7 @@ func startTrigger(sess *storage.Session, net interfaces.NetworkManager, } func handleTrigger(sess *storage.Session, net interfaces.NetworkManager, - request message.Receive, param params.Rekey) error { + request message.Receive, param params.Rekey, stop *stoppable.Single) error { //ensure the message was encrypted properly if request.Encryption != message.E2E { errMsg := fmt.Sprintf(errBadTrigger, request.Sender) @@ -126,7 +127,10 @@ func handleTrigger(sess *storage.Session, net interfaces.NetworkManager, // send fails sess.GetCriticalMessages().AddProcessing(m, e2eParams) - rounds, _, err := net.SendE2E(m, e2eParams) + rounds, _, _, err := net.SendE2E(m, e2eParams, stop) + if err != nil { + return err + } //Register the event for all rounds sendResults := make(chan ds.EventReturn, len(rounds)) @@ -153,7 +157,7 @@ func handleTrigger(sess *storage.Session, net interfaces.NetworkManager, // otherwise, the transmission is a success and this should be denoted // in the session and the log sess.GetCriticalMessages().Succeeded(m) - jww.INFO.Printf("Key Negotiation transmission for %s sucesfull", + jww.INFO.Printf("Key Negotiation transmission for %s successfully", session) return nil diff --git a/keyExchange/trigger_test.go b/keyExchange/trigger_test.go index 430bba91a0ba734541b03a61260493caa03237e0..d01f9101396d573c8c97892aa3256ede2ba91d49 100644 --- a/keyExchange/trigger_test.go +++ b/keyExchange/trigger_test.go @@ -10,6 +10,7 @@ package keyExchange import ( "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage/e2e" dh "gitlab.com/elixxir/crypto/diffieHellman" "gitlab.com/xx_network/crypto/csprng" @@ -23,9 +24,14 @@ import ( // Smoke test for handleTrigger func TestHandleTrigger(t *testing.T) { // Generate alice and bob's session - aliceSession, aliceManager := InitTestingContextGeneric(t) - bobSession, _ := InitTestingContextGeneric(t) - + aliceSession, aliceManager, err := InitTestingContextGeneric(t) + if err != nil { + t.Fatalf("Failed to create alice session: %v", err) + } + bobSession, _, err := InitTestingContextGeneric(t) + if err != nil { + t.Fatalf("Failed to create bob session: %v", err) + } // Pull the keys for Alice and Bob alicePrivKey := aliceSession.E2e().GetDHPrivateKey() bobPubKey := bobSession.E2e().GetDHPublicKey() @@ -61,8 +67,9 @@ func TestHandleTrigger(t *testing.T) { // Handle the trigger and check for an error rekeyParams := params.GetDefaultRekey() + stop := stoppable.NewSingle("stoppable") rekeyParams.RoundTimeout = 0 * time.Second - err := handleTrigger(aliceSession, aliceManager, receiveMsg, rekeyParams) + err = handleTrigger(aliceSession, aliceManager, receiveMsg, rekeyParams, stop) if err != nil { t.Errorf("Handle trigger error: %v", err) } diff --git a/keyExchange/utils_test.go b/keyExchange/utils_test.go index a23e673f3a258164e4e2c407defbb5dabd5394d9..0a1b0206bc8e72bf3ca25b4dd17e4ce02a5937d1 100644 --- a/keyExchange/utils_test.go +++ b/keyExchange/utils_test.go @@ -31,6 +31,7 @@ import ( "gitlab.com/xx_network/primitives/ndf" "gitlab.com/xx_network/primitives/netTime" "testing" + "time" ) // Generate partner ID for two people, used for smoke tests @@ -66,10 +67,10 @@ func (t *testNetworkManagerGeneric) CheckGarbledMessages() { return } -func (t *testNetworkManagerGeneric) SendE2E(m message.Send, p params.E2E) ( - []id.Round, cE2e.MessageID, error) { +func (t *testNetworkManagerGeneric) SendE2E(message.Send, params.E2E, *stoppable.Single) ( + []id.Round, cE2e.MessageID, time.Time, error) { rounds := []id.Round{id.Round(0), id.Round(1), id.Round(2)} - return rounds, cE2e.MessageID{}, nil + return rounds, cE2e.MessageID{}, time.Time{}, nil } @@ -84,11 +85,19 @@ func (t *testNetworkManagerGeneric) SendCMIX(message format.Message, rid *id.ID, } +func (t *testNetworkManagerGeneric) SendManyCMIX(messages map[id.ID]format.Message, p params.CMIX) (id.Round, []ephemeral.Id, error) { + return id.Round(0), []ephemeral.Id{}, nil +} + func (t *testNetworkManagerGeneric) GetInstance() *network.Instance { return t.instance } +func (t *testNetworkManagerGeneric) GetEventManager() interfaces.EventManager { + return &dummyEventMgr{} +} + func (t *testNetworkManagerGeneric) RegisterWithPermissioning(string) ([]byte, error) { return nil, nil } @@ -108,7 +117,16 @@ func (t *testNetworkManagerGeneric) GetSender() *gateway.Sender { return nil } -func InitTestingContextGeneric(i interface{}) (*storage.Session, interfaces.NetworkManager) { +func (t *testNetworkManagerGeneric) GetAddressSize() uint8 { return 0 } + +func (t *testNetworkManagerGeneric) RegisterAddressSizeNotification(string) (chan uint8, error) { + return nil, nil +} + +func (t *testNetworkManagerGeneric) UnregisterAddressSizeNotification(string) {} +func (t *testNetworkManagerGeneric) SetPoolFilter(gateway.Filter) {} + +func InitTestingContextGeneric(i interface{}) (*storage.Session, interfaces.NetworkManager, error) { switch i.(type) { case *testing.T, *testing.M, *testing.B, *testing.PB: break @@ -125,12 +143,12 @@ func InitTestingContextGeneric(i interface{}) (*storage.Session, interfaces.Netw thisInstance, err := network.NewInstanceTesting(instanceComms, def, def, nil, nil, i) if err != nil { - return nil, nil + return nil, nil, err } thisManager := &testNetworkManagerGeneric{instance: thisInstance} - return thisSession, thisManager + return thisSession, thisManager, nil } @@ -140,6 +158,12 @@ func InitTestingContextGeneric(i interface{}) (*storage.Session, interfaces.Netw type testNetworkManagerFullExchange struct { instance *network.Instance } +type dummyEventMgr struct{} + +func (d *dummyEventMgr) Report(p int, a, b, c string) {} +func (t *testNetworkManagerFullExchange) GetEventManager() interfaces.EventManager { + return &dummyEventMgr{} +} func (t *testNetworkManagerFullExchange) GetHealthTracker() interfaces.HealthTracker { return nil @@ -155,8 +179,8 @@ func (t *testNetworkManagerFullExchange) CheckGarbledMessages() { // Intended for alice to send to bob. Trigger's Bob's confirmation, chaining the operation // together -func (t *testNetworkManagerFullExchange) SendE2E(m message.Send, p params.E2E) ( - []id.Round, cE2e.MessageID, error) { +func (t *testNetworkManagerFullExchange) SendE2E(message.Send, params.E2E, *stoppable.Single) ( + []id.Round, cE2e.MessageID, time.Time, error) { rounds := []id.Round{id.Round(0), id.Round(1), id.Round(2)} alicePrivKey := aliceSession.E2e().GetDHPrivateKey() @@ -180,19 +204,19 @@ func (t *testNetworkManagerFullExchange) SendE2E(m message.Send, p params.E2E) ( bobSwitchboard.Speak(confirmMessage) - return rounds, cE2e.MessageID{}, nil - + return rounds, cE2e.MessageID{}, time.Time{}, nil } func (t *testNetworkManagerFullExchange) SendUnsafe(m message.Send, p params.Unsafe) ([]id.Round, error) { - return nil, nil } func (t *testNetworkManagerFullExchange) SendCMIX(message format.Message, eid *id.ID, p params.CMIX) (id.Round, ephemeral.Id, error) { - return id.Round(0), ephemeral.Id{}, nil +} +func (t *testNetworkManagerFullExchange) SendManyCMIX(messages map[id.ID]format.Message, p params.CMIX) (id.Round, []ephemeral.Id, error) { + return id.Round(0), []ephemeral.Id{}, nil } func (t *testNetworkManagerFullExchange) GetInstance() *network.Instance { @@ -219,6 +243,15 @@ func (t *testNetworkManagerFullExchange) GetSender() *gateway.Sender { return nil } +func (t *testNetworkManagerFullExchange) GetAddressSize() uint8 { return 0 } + +func (t *testNetworkManagerFullExchange) RegisterAddressSizeNotification(string) (chan uint8, error) { + return nil, nil +} + +func (t *testNetworkManagerFullExchange) UnregisterAddressSizeNotification(string) {} +func (t *testNetworkManagerFullExchange) SetPoolFilter(gateway.Filter) {} + func InitTestingContextFullExchange(i interface{}) (*storage.Session, *switchboard.Switchboard, interfaces.NetworkManager) { switch i.(type) { case *testing.T, *testing.M, *testing.B, *testing.PB: @@ -304,5 +337,8 @@ func getNDF() *ndf.NetworkDefinition { "BA9AE3F1DD2487199874393CD4D832186800654760E1E34C09E4D155179F9EC0" + "DC4473F996BDCE6EED1CABED8B6F116F7AD9CF505DF0F998E34AB27514B0FFE7", }, + Registration: ndf.Registration{ + EllipticPubKey: "/WRtT+mDZGC3FXQbvuQgfqOonAjJ47IKE0zhaGTQQ70=", + }, } } diff --git a/network/ephemeral/addressSpace.go b/network/ephemeral/addressSpace.go new file mode 100644 index 0000000000000000000000000000000000000000..f942df77df38bfa4455bdf096ad2d4e0add40b49 --- /dev/null +++ b/network/ephemeral/addressSpace.go @@ -0,0 +1,138 @@ +package ephemeral + +import ( + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "sync" + "testing" +) + +const ( + // The initial value for the address space size. This value signifies that + // the address space size has not yet been updated. + initSize = 1 +) + +// AddressSpace contains the current address space size used for creating +// ephemeral IDs and the infrastructure to alert other processes when an Update +// occurs. +type AddressSpace struct { + size uint8 + notifyMap map[string]chan uint8 + cond *sync.Cond +} + +// NewAddressSpace initialises a new AddressSpace and returns it. +func NewAddressSpace() *AddressSpace { + return &AddressSpace{ + size: initSize, + notifyMap: make(map[string]chan uint8), + cond: sync.NewCond(&sync.Mutex{}), + } +} + +// Get returns the current address space size. It blocks until an address space +// size is set. +func (as *AddressSpace) Get() uint8 { + as.cond.L.Lock() + defer as.cond.L.Unlock() + + // If the size has been set, then return the current size + if as.size != initSize { + return as.size + } + + // If the size is not set, then block until it is set + as.cond.Wait() + + return as.size +} + +// GetWithoutWait returns the current address space size regardless if it has +// been set yet. +func (as *AddressSpace) GetWithoutWait() uint8 { + as.cond.L.Lock() + defer as.cond.L.Unlock() + return as.size +} + +// Update updates the address space size to the new size, if it is larger. Then, +// each registered channel is notified of the Update. If this was the first time +// that the address space size was set, then the conditional broadcasts to stop +// blocking for all threads waiting on Get. +func (as *AddressSpace) Update(newSize uint8) { + as.cond.L.Lock() + defer as.cond.L.Unlock() + + // Skip Update if the address space size is unchanged + if as.size >= newSize { + return + } + + // Update address space size + oldSize := as.size + as.size = newSize + jww.INFO.Printf("Updated address space size from %d to %d", oldSize, as.size) + + // Broadcast that the address space size is set, if set for the first time + if oldSize == initSize { + as.cond.Broadcast() + } else { + // Broadcast the new address space size to all registered channels + for chanID, sizeChan := range as.notifyMap { + select { + case sizeChan <- as.size: + default: + jww.ERROR.Printf("Failed to send address space Update of %d on "+ + "channel with ID %s", as.size, chanID) + } + } + } +} + +// RegisterNotification returns a channel that will trigger for every address +// space size Update. The provided tag is the unique ID for the channel. +// Returns an error if the tag is already used. +func (as *AddressSpace) RegisterNotification(tag string) (chan uint8, error) { + as.cond.L.Lock() + defer as.cond.L.Unlock() + + if _, exists := as.notifyMap[tag]; exists { + return nil, errors.Errorf("tag \"%s\" already exists in notify map", tag) + } + + as.notifyMap[tag] = make(chan uint8, 1) + + return as.notifyMap[tag], nil +} + +// UnregisterNotification stops broadcasting address space size updates on the +// channel with the specified tag. +func (as *AddressSpace) UnregisterNotification(tag string) { + as.cond.L.Lock() + defer as.cond.L.Unlock() + + delete(as.notifyMap, tag) +} + +// NewTestAddressSpace initialises a new AddressSpace for testing with the given +// size. +func NewTestAddressSpace(newSize uint8, x interface{}) *AddressSpace { + switch x.(type) { + case *testing.T, *testing.M, *testing.B, *testing.PB: + break + default: + jww.FATAL.Panicf("NewTestAddressSpace is restricted to testing only. "+ + "Got %T", x) + } + + as := &AddressSpace{ + size: initSize, + notifyMap: make(map[string]chan uint8), + cond: sync.NewCond(&sync.Mutex{}), + } + + as.Update(newSize) + + return as +} diff --git a/network/ephemeral/addressSpace_test.go b/network/ephemeral/addressSpace_test.go new file mode 100644 index 0000000000000000000000000000000000000000..03cca463bc829e9db4ff6bb916fb6738e9b12e19 --- /dev/null +++ b/network/ephemeral/addressSpace_test.go @@ -0,0 +1,291 @@ +package ephemeral + +import ( + "reflect" + "strconv" + "sync" + "testing" + "time" +) + +// Unit test of NewAddressSpace. +func Test_newAddressSpace(t *testing.T) { + expected := &AddressSpace{ + size: initSize, + notifyMap: make(map[string]chan uint8), + cond: sync.NewCond(&sync.Mutex{}), + } + + as := NewAddressSpace() + + if !reflect.DeepEqual(expected, as) { + t.Errorf("NewAddressSpace failed to return the expected AddressSpace."+ + "\nexpected: %+v\nreceived: %+v", expected, as) + } +} + +// Test that AddressSpace.Get blocks when the address space size has not been +// set and that it does not block when it has been set. +func Test_addressSpace_Get(t *testing.T) { + as := NewAddressSpace() + expectedSize := uint8(42) + + // Call Get and error if it does not block + wait := make(chan uint8) + go func() { wait <- as.Get() }() + select { + case size := <-wait: + t.Errorf("Get failed to block and returned size %d.", size) + case <-time.NewTimer(10 * time.Millisecond).C: + } + + // Update address size + as.cond.L.Lock() + as.size = expectedSize + as.cond.L.Unlock() + + // Call Get and error if it does block + wait = make(chan uint8) + go func() { wait <- as.Get() }() + select { + case size := <-wait: + if size != expectedSize { + t.Errorf("Get returned the wrong size.\nexpected: %d\nreceived: %d", + expectedSize, size) + } + case <-time.NewTimer(15 * time.Millisecond).C: + t.Error("Get blocking when the size has been updated.") + } +} + +// Test that AddressSpace.Get blocks until the condition broadcasts. +func Test_addressSpace_Get_WaitBroadcast(t *testing.T) { + as := NewAddressSpace() + + wait := make(chan uint8) + go func() { wait <- as.Get() }() + + go func() { + select { + case size := <-wait: + if size != initSize { + t.Errorf("Get returned the wrong size.\nexpected: %d\nreceived: %d", + initSize, size) + } + case <-time.NewTimer(25 * time.Millisecond).C: + t.Error("Get blocking when the Cond has broadcast.") + } + }() + + time.Sleep(5 * time.Millisecond) + + as.cond.Broadcast() +} + +// Unit test of AddressSpace.GetWithoutWait. +func Test_addressSpace_GetWithoutWait(t *testing.T) { + as := NewAddressSpace() + + size := as.GetWithoutWait() + if size != initSize { + t.Errorf("GetWithoutWait returned the wrong size."+ + "\nexpected: %d\nreceived: %d", initSize, size) + } +} + +// Tests that AddressSpace.Update only updates the size when it is larger. +func Test_addressSpace_update(t *testing.T) { + as := NewAddressSpace() + expectedSize := uint8(42) + + // Attempt to Update to larger size + as.Update(expectedSize) + if as.size != expectedSize { + t.Errorf("Update failed to set the new size."+ + "\nexpected: %d\nreceived: %d", expectedSize, as.size) + } + + // Attempt to Update to smaller size + as.Update(expectedSize - 1) + if as.size != expectedSize { + t.Errorf("Update failed to set the new size."+ + "\nexpected: %d\nreceived: %d", expectedSize, as.size) + } +} + +// Tests that AddressSpace.Update sends the new size to all registered channels. +func Test_addressSpace_update_GetAndChannels(t *testing.T) { + as := NewAddressSpace() + var wg sync.WaitGroup + expectedSize := uint8(42) + + // Start threads that are waiting for an Update + wait := []chan uint8{make(chan uint8), make(chan uint8), make(chan uint8)} + for _, waitChan := range wait { + go func(waitChan chan uint8) { waitChan <- as.Get() }(waitChan) + } + + // Wait on threads + for i, waitChan := range wait { + go func(i int, waitChan chan uint8) { + wg.Add(1) + defer wg.Done() + + select { + case size := <-waitChan: + if size != expectedSize { + t.Errorf("Thread %d received unexpected size."+ + "\nexpected: %d\nreceived: %d", i, expectedSize, size) + } + case <-time.NewTimer(20 * time.Millisecond).C: + t.Errorf("Timed out waiting for Get to return on thread %d.", i) + } + }(i, waitChan) + } + + // Register channels + notifyChannels := make(map[string]chan uint8) + var notifyChan chan uint8 + var err error + var chanID string + for i := 0; i < 3; i++ { + chanID = strconv.Itoa(i) + notifyChannels[chanID], err = as.RegisterNotification(chanID) + if err != nil { + t.Errorf("Failed to regisdter channel: %+v", err) + } + } + + // Wait for new size on channels + for chanID, notifyChan := range notifyChannels { + go func(chanID string, notifyChan chan uint8) { + wg.Add(1) + defer wg.Done() + + select { + case size := <-notifyChan: + t.Errorf("Received size %d on channel %s when it should not have.", + size, chanID) + case <-time.NewTimer(20 * time.Millisecond).C: + } + }(chanID, notifyChan) + } + + time.Sleep(5 * time.Millisecond) + + // Attempt to Update to larger size + as.Update(expectedSize) + + wg.Wait() + + // Unregistered one channel and make sure it will not receive + delete(notifyChannels, chanID) + as.UnregisterNotification(chanID) + + expectedSize++ + + // Wait for new size on channels + for chanID, notifyChan := range notifyChannels { + go func(chanID string, notifyChan chan uint8) { + wg.Add(1) + defer wg.Done() + + select { + case size := <-notifyChan: + if size != expectedSize { + t.Errorf("Failed to receive expected size on channel %s."+ + "\nexpected: %d\nreceived: %d", chanID, expectedSize, size) + } + case <-time.NewTimer(20 * time.Millisecond).C: + t.Errorf("Timed out waiting on channel %s", chanID) + } + }(chanID, notifyChan) + } + + // Wait for timeout on unregistered channel + go func() { + wg.Add(1) + defer wg.Done() + + select { + case size := <-notifyChan: + t.Errorf("Received size %d on channel %s when it should not have.", + size, chanID) + case <-time.NewTimer(20 * time.Millisecond).C: + } + }() + + time.Sleep(5 * time.Millisecond) + + // Attempt to Update to larger size + as.Update(expectedSize) + + wg.Wait() +} + +// Tests that a channel created by AddressSpace.RegisterNotification receives +// the expected size when triggered. +func Test_addressSpace_RegisterNotification(t *testing.T) { + as := NewAddressSpace() + expectedSize := uint8(42) + + // Register channel + chanID := "chanID" + sizeChan, err := as.RegisterNotification(chanID) + if err != nil { + t.Errorf("RegisterNotification returned an error: %+v", err) + } + + // Wait on channel or error after timing out + go func() { + select { + case size := <-sizeChan: + if size != expectedSize { + t.Errorf("received wrong size on channel."+ + "\nexpected: %d\nreceived: %d", expectedSize, size) + } + case <-time.NewTimer(10 * time.Millisecond).C: + t.Error("Timed out waiting on channel.") + } + }() + + // Send on channel + select { + case as.notifyMap[chanID] <- expectedSize: + default: + t.Errorf("Sent on channel %s that should not be in map.", chanID) + } +} + +// Tests that when AddressSpace.UnregisterNotification unregisters a channel, +// it no longer can be triggered from the map. +func Test_addressSpace_UnregisterNotification(t *testing.T) { + as := NewAddressSpace() + expectedSize := uint8(42) + + // Register channel and then unregister it + chanID := "chanID" + sizeChan, err := as.RegisterNotification(chanID) + if err != nil { + t.Errorf("RegisterNotification returned an error: %+v", err) + } + as.UnregisterNotification(chanID) + + // Wait for timeout or error if the channel receives + go func() { + select { + case size := <-sizeChan: + t.Errorf("Received %d on channel %s that should not be in map.", + size, chanID) + case <-time.NewTimer(10 * time.Millisecond).C: + } + }() + + // Send on channel + select { + case as.notifyMap[chanID] <- expectedSize: + t.Errorf("Sent size %d on channel %s that should not be in map.", + expectedSize, chanID) + default: + } +} diff --git a/network/ephemeral/testutil.go b/network/ephemeral/testutil.go index d2708b0c71c7d112151e4494d3ef631a78adaf62..1051e9c297d84a22436a08429f8f023deb953698 100644 --- a/network/ephemeral/testutil.go +++ b/network/ephemeral/testutil.go @@ -10,6 +10,7 @@ package ephemeral import ( "gitlab.com/elixxir/client/network/gateway" "testing" + "time" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces" @@ -33,8 +34,8 @@ type testNetworkManager struct { msg message.Send } -func (t *testNetworkManager) SendE2E(m message.Send, _ params.E2E) ([]id.Round, - e2e.MessageID, error) { +func (t *testNetworkManager) SendE2E(m message.Send, _ params.E2E, _ *stoppable.Single) ([]id.Round, + e2e.MessageID, time.Time, error) { rounds := []id.Round{ id.Round(0), id.Round(1), @@ -43,7 +44,7 @@ func (t *testNetworkManager) SendE2E(m message.Send, _ params.E2E) ([]id.Round, t.msg = m - return rounds, e2e.MessageID{}, nil + return rounds, e2e.MessageID{}, time.Time{}, nil } func (t *testNetworkManager) SendUnsafe(m message.Send, _ params.Unsafe) ([]id.Round, error) { @@ -62,15 +63,26 @@ func (t *testNetworkManager) SendCMIX(format.Message, *id.ID, params.CMIX) (id.R return 0, ephemeral.Id{}, nil } +func (t *testNetworkManager) SendManyCMIX(messages map[id.ID]format.Message, p params.CMIX) (id.Round, []ephemeral.Id, error) { + return 0, []ephemeral.Id{}, nil +} + func (t *testNetworkManager) GetInstance() *network.Instance { return t.instance } +type dummyEventMgr struct{} + +func (d *dummyEventMgr) Report(p int, a, b, c string) {} +func (t *testNetworkManager) GetEventManager() interfaces.EventManager { + return &dummyEventMgr{} +} + func (t *testNetworkManager) GetHealthTracker() interfaces.HealthTracker { return nil } -func (t *testNetworkManager) Follow(report interfaces.ClientErrorReport) (stoppable.Stoppable, error) { +func (t *testNetworkManager) Follow(_ interfaces.ClientErrorReport) (stoppable.Stoppable, error) { return nil, nil } @@ -84,12 +96,20 @@ func (t *testNetworkManager) GetSender() *gateway.Sender { return nil } +func (t *testNetworkManager) GetAddressSize() uint8 { return 15 } +func (t *testNetworkManager) RegisterAddressSizeNotification(string) (chan uint8, error) { + return nil, nil +} + +func (t *testNetworkManager) UnregisterAddressSizeNotification(string) {} +func (t *testNetworkManager) SetPoolFilter(gateway.Filter) {} + func NewTestNetworkManager(i interface{}) interfaces.NetworkManager { switch i.(type) { case *testing.T, *testing.M, *testing.B: break default: - jww.FATAL.Panicf("initTesting is restricted to testing only."+ + jww.FATAL.Panicf("NewTestNetworkManager is restricted to testing only."+ "Got %T", i) } @@ -97,17 +117,22 @@ func NewTestNetworkManager(i interface{}) interfaces.NetworkManager { cert, err := utils.ReadFile(testkeys.GetNodeCertPath()) if err != nil { - jww.FATAL.Panicf("Failed to create new test Instance: %v", err) + jww.FATAL.Panicf("Failed to create new test Instance: %+v", err) } - commsManager.AddHost(&id.Permissioning, "", cert, connect.GetDefaultHostParams()) + _, err = commsManager.AddHost( + &id.Permissioning, "", cert, connect.GetDefaultHostParams()) + if err != nil { + jww.FATAL.Panicf("Failed to add host: %+v", err) + } instanceComms := &connect.ProtoComms{ Manager: commsManager, } - thisInstance, err := network.NewInstanceTesting(instanceComms, getNDF(), getNDF(), nil, nil, i) + thisInstance, err := network.NewInstanceTesting( + instanceComms, getNDF(), getNDF(), nil, nil, i) if err != nil { - jww.FATAL.Panicf("Failed to create new test Instance: %v", err) + jww.FATAL.Panicf("Failed to create new test Instance: %+v", err) } thisManager := &testNetworkManager{instance: thisInstance} diff --git a/network/ephemeral/tracker.go b/network/ephemeral/tracker.go index 7292f73d8cfa8741c4bac631dd9c43ab3859b53d..b3265afe1a14738d9abd1247dd7932024a0debc3 100644 --- a/network/ephemeral/tracker.go +++ b/network/ephemeral/tracker.go @@ -22,150 +22,159 @@ import ( const validityGracePeriod = 5 * time.Minute const TimestampKey = "IDTrackingTimestamp" +const TimestampStoreVersion = 0 const ephemeralStoppable = "EphemeralCheck" +const addressSpaceSizeChanTag = "ephemeralTracker" -// Track runs a thread which checks for past and present ephemeral ids -func Track(session *storage.Session, ourId *id.ID) stoppable.Stoppable { +// Track runs a thread which checks for past and present ephemeral ID. +func Track(session *storage.Session, addrSpace *AddressSpace, ourId *id.ID) stoppable.Stoppable { stop := stoppable.NewSingle(ephemeralStoppable) - go track(session, ourId, stop) + go track(session, addrSpace, ourId, stop) return stop } -// track is a thread which continuously processes ephemeral ids. -// If any error occurs, the thread crashes -func track(session *storage.Session, ourId *id.ID, stop *stoppable.Single) { +// track is a thread which continuously processes ephemeral IDs. Panics if any +// error occurs. +func track(session *storage.Session, addrSpace *AddressSpace, ourId *id.ID, stop *stoppable.Single) { // Check that there is a timestamp in store at all err := checkTimestampStore(session) if err != nil { - jww.FATAL.Panicf("Could not store timestamp "+ - "for ephemeral ID tracking: %v", err) + jww.FATAL.Panicf("Could not store timestamp for ephemeral ID "+ + "tracking: %+v", err) } // Get the latest timestamp from store lastTimestampObj, err := session.Get(TimestampKey) if err != nil { - jww.FATAL.Panicf("Could not get timestamp: %v", err) + jww.FATAL.Panicf("Could not get timestamp: %+v", err) } lastCheck, err := unmarshalTimestamp(lastTimestampObj) if err != nil { - jww.FATAL.Panicf("Could not parse stored timestamp: %v", err) + jww.FATAL.Panicf("Could not parse stored timestamp: %+v", err) } - // Wait until we get the id size from the network + // Wait until we get the ID size from the network receptionStore := session.Reception() - receptionStore.WaitForIdSizeUpdate() + addrSpace.UnregisterNotification(addressSpaceSizeChanTag) + addressSizeUpdate, err := addrSpace.RegisterNotification(addressSpaceSizeChanTag) + if err != nil { + jww.FATAL.Panicf("failed to register address size notification "+ + "channel: %+v", err) + } + addressSize := addrSpace.Get() - for true { + for { now := netTime.Now() - //hack for inconsistent time on android - if now.Sub(lastCheck) <=0{ + // Hack for inconsistent time on android + if now.Before(lastCheck) || now.Equal(lastCheck) { now = lastCheck.Add(time.Nanosecond) } // Generates the IDs since the last track - protoIds, err := ephemeral.GetIdsByRange(ourId, receptionStore.GetIDSize(), - now, now.Sub(lastCheck)) + protoIds, err := ephemeral.GetIdsByRange( + ourId, uint(addressSize), lastCheck, now.Sub(lastCheck)) jww.DEBUG.Printf("Now: %s, LastCheck: %s, Different: %s", now, lastCheck, now.Sub(lastCheck)) - jww.DEBUG.Printf("protoIds Count: %d", len(protoIds)) if err != nil { - jww.FATAL.Panicf("Could not generate "+ - "upcoming IDs: %v", err) + jww.FATAL.Panicf("Could not generate upcoming IDs: %+v", err) } // Generate identities off of that list - identities := generateIdentities(protoIds, ourId) - - jww.INFO.Printf("Number of Identities Generated: %d", - len(identities)) + identities := generateIdentities(protoIds, ourId, addressSize) + jww.INFO.Printf("Number of Identities Generated: %d", len(identities)) jww.INFO.Printf("Current Identity: %d (source: %s), Start: %s, End: %s", - identities[len(identities)-1].EphId.Int64(), identities[len(identities)-1].Source, - identities[len(identities)-1].StartValid, identities[len(identities)-1].EndValid) + identities[len(identities)-1].EphId.Int64(), + identities[len(identities)-1].Source, + identities[len(identities)-1].StartValid, + identities[len(identities)-1].EndValid) - // Add identities to storage if unique + // Add identities to storage, if unique for _, identity := range identities { if err = receptionStore.AddIdentity(identity); err != nil { - jww.FATAL.Panicf("Could not insert "+ - "identity: %v", err) + jww.FATAL.Panicf("Could not insert identity: %+v", err) } } - // Generate the time stamp for storage + // Generate the timestamp for storage vo, err := marshalTimestamp(now) if err != nil { - jww.FATAL.Panicf("Could not marshal "+ - "timestamp for storage: %v", err) + jww.FATAL.Panicf("Could not marshal timestamp for storage: %+v", err) } + lastCheck = now // Store the timestamp if err = session.Set(TimestampKey, vo); err != nil { - jww.FATAL.Panicf("Could not store timestamp: %v", err) + jww.FATAL.Panicf("Could not store timestamp: %+v", err) } - // Sleep until the last Id has expired - timeToSleep := calculateTickerTime(protoIds) - t := time.NewTimer(timeToSleep) + // Sleep until the last ID has expired + timeToSleep := calculateTickerTime(protoIds, now) select { - case <-t.C: + case <-time.NewTimer(timeToSleep).C: + case addressSize = <-addressSizeUpdate: + receptionStore.SetToExpire(addressSize) case <-stop.Quit(): + addrSpace.UnregisterNotification(addressSpaceSizeChanTag) + stop.ToStopped() return } } } -// generateIdentities is a constructor which generates a list of -// identities off of the list of protoIdentities passed in -func generateIdentities(protoIds []ephemeral.ProtoIdentity, - ourId *id.ID) []reception.Identity { +// generateIdentities generates a list of identities off of the list of passed +// in ProtoIdentity. +func generateIdentities(protoIds []ephemeral.ProtoIdentity, ourId *id.ID, + addressSize uint8) []reception.Identity { - identities := make([]reception.Identity, 0) + identities := make([]reception.Identity, len(protoIds)) - // Add identities for every ephemeral id - for _, eid := range protoIds { + // Add identities for every ephemeral ID + for i, eid := range protoIds { // Expand the grace period for both start and end - eid.End.Add(validityGracePeriod) - eid.Start.Add(-validityGracePeriod) - identities = append(identities, reception.Identity{ - EphId: eid.Id, - Source: ourId, - End: eid.End, - StartValid: eid.Start, - EndValid: eid.End, - Ephemeral: false, - }) + identities[i] = reception.Identity{ + EphId: eid.Id, + Source: ourId, + AddressSize: addressSize, + End: eid.End, + StartValid: eid.Start.Add(-validityGracePeriod), + EndValid: eid.End.Add(validityGracePeriod), + Ephemeral: false, + } } return identities } -// Sanitation check of timestamp store. If a value has not been stored yet -// then the current time is stored +// checkTimestampStore performs a sanitation check of timestamp store. If a +// value has not been stored yet, then the current time is stored. func checkTimestampStore(session *storage.Session) error { if _, err := session.Get(TimestampKey); err != nil { - // only generate from the last hour because this is a new id, it - // couldn't receive messages yet + // Only generate from the last hour because this is a new ID; it could + // not yet receive messages now, err := marshalTimestamp(netTime.Now().Add(-1 * time.Hour)) if err != nil { - return errors.Errorf("Could not marshal new timestamp for storage: %v", err) + return errors.Errorf("Could not marshal new timestamp for "+ + "storage: %+v", err) } + return session.Set(TimestampKey, now) } return nil } -// Takes the stored timestamp and unmarshal into a time object +// unmarshalTimestamp unmarshal the stored timestamp into a time.Time. func unmarshalTimestamp(lastTimestampObj *versioned.Object) (time.Time, error) { if lastTimestampObj == nil || lastTimestampObj.Data == nil { return netTime.Now(), nil @@ -176,28 +185,30 @@ func unmarshalTimestamp(lastTimestampObj *versioned.Object) (time.Time, error) { return lastTimestamp, err } -// Marshals the timestamp for ekv storage. Generates a storable object +// marshalTimestamp marshals the timestamp and generates a storable object for +// ekv storage. func marshalTimestamp(timeToStore time.Time) (*versioned.Object, error) { data, err := timeToStore.MarshalBinary() return &versioned.Object{ - Version: 0, + Version: TimestampStoreVersion, Timestamp: netTime.Now(), Data: data, }, err } -// Helper function which calculates the time for the ticker based -// off of the last ephemeral ID to expire -func calculateTickerTime(baseIDs []ephemeral.ProtoIdentity) time.Duration { +// calculateTickerTime calculates the time for the ticker based off of the last +// ephemeral ID to expire. +func calculateTickerTime(baseIDs []ephemeral.ProtoIdentity, now time.Time) time.Duration { if len(baseIDs) == 0 { return time.Duration(0) } + // Get the last identity in the list lastIdentity := baseIDs[len(baseIDs)-1] - // Factor out the grace period previously expanded upon. + // Factor out the grace period previously expanded upon // Calculate and return that duration - gracePeriod := lastIdentity.End.Add(-validityGracePeriod) - return lastIdentity.End.Sub(gracePeriod) + gracePeriod := lastIdentity.End.Add(-1 * validityGracePeriod) + return gracePeriod.Sub(now) } diff --git a/network/ephemeral/tracker_test.go b/network/ephemeral/tracker_test.go index aa4589ebe7650ab55d4d04ab97fb98cfb9d1c008..47b8f034766c45c003f9f4997a599cf1ad22cfbd 100644 --- a/network/ephemeral/tracker_test.go +++ b/network/ephemeral/tracker_test.go @@ -17,6 +17,7 @@ import ( "gitlab.com/xx_network/comms/signature" "gitlab.com/xx_network/crypto/signature/rsa" "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" "gitlab.com/xx_network/primitives/netTime" "gitlab.com/xx_network/primitives/utils" "testing" @@ -28,35 +29,34 @@ func TestCheck(t *testing.T) { session := storage.InitTestingSession(t) instance := NewTestNetworkManager(t) if err := setupInstance(instance); err != nil { - t.Errorf("Could not set up instance: %v", err) + t.Errorf("Could not set up instance: %+v", err) } - /// Store a mock initial timestamp the store + // Store a mock initial timestamp the store now := netTime.Now() twoDaysAgo := now.Add(-2 * 24 * time.Hour) twoDaysTimestamp, err := marshalTimestamp(twoDaysAgo) if err != nil { - t.Errorf("Could not marshal timestamp for test setup: %v", err) + t.Errorf("Could not marshal timestamp for test setup: %+v", err) } + err = session.Set(TimestampKey, twoDaysTimestamp) if err != nil { - t.Errorf("Could not set mock timestamp for test setup: %v", err) + t.Errorf("Could not set mock timestamp for test setup: %+v", err) } ourId := id.NewIdFromBytes([]byte("Sauron"), t) - stop := Track(session, ourId) - session.Reception().MarkIdSizeAsSet() + stop := Track(session, NewTestAddressSpace(15, t), ourId) - err = stop.Close(3 * time.Second) + err = stop.Close() if err != nil { - t.Errorf("Could not close thread: %v", err) + t.Errorf("Could not close thread: %+v", err) } } -// Unit test for track +// Unit test for track. func TestCheck_Thread(t *testing.T) { - session := storage.InitTestingSession(t) instance := NewTestNetworkManager(t) if err := setupInstance(instance); err != nil { @@ -65,27 +65,26 @@ func TestCheck_Thread(t *testing.T) { ourId := id.NewIdFromBytes([]byte("Sauron"), t) stop := stoppable.NewSingle(ephemeralStoppable) - /// Store a mock initial timestamp the store + // Store a mock initial timestamp the store now := netTime.Now() yesterday := now.Add(-24 * time.Hour) yesterdayTimestamp, err := marshalTimestamp(yesterday) if err != nil { - t.Errorf("Could not marshal timestamp for test setup: %v", err) + t.Errorf("Could not marshal timestamp for test setup: %+v", err) } + err = session.Set(TimestampKey, yesterdayTimestamp) if err != nil { - t.Errorf("Could not set mock timestamp for test setup: %v", err) + t.Errorf("Could not set mock timestamp for test setup: %+v", err) } // Run the tracker go func() { - track(session, ourId, stop) + track(session, NewTestAddressSpace(15, t), ourId, stop) }() time.Sleep(3 * time.Second) - session.Reception().MarkIdSizeAsSet() - - err = stop.Close(3 * time.Second) + err = stop.Close() if err != nil { t.Errorf("Could not close thread: %v", err) } @@ -95,7 +94,7 @@ func TestCheck_Thread(t *testing.T) { func setupInstance(instance interfaces.NetworkManager) error { cert, err := utils.ReadFile(testkeys.GetNodeKeyPath()) if err != nil { - return errors.Errorf("Failed to read cert from from file: %v", err) + return errors.Errorf("Failed to read cert from from file: %+v", err) } ri := &mixmessages.RoundInfo{ ID: 1, @@ -103,20 +102,20 @@ func setupInstance(instance interfaces.NetworkManager) error { testCert, err := rsa.LoadPrivateKeyFromPem(cert) if err != nil { - return errors.Errorf("Failed to load cert from from file: %v", err) + return errors.Errorf("Failed to load cert from from file: %+v", err) } - if err = signature.Sign(ri, testCert); err != nil { - return errors.Errorf("Failed to sign round info: %v", err) + if err = signature.SignRsa(ri, testCert); err != nil { + return errors.Errorf("Failed to sign round info: %+v", err) } if err = instance.GetInstance().RoundUpdate(ri); err != nil { - return errors.Errorf("Failed to RoundUpdate from from file: %v", err) + return errors.Errorf("Failed to RoundUpdate from from file: %+v", err) } ri = &mixmessages.RoundInfo{ ID: 2, } - if err = signature.Sign(ri, testCert); err != nil { - return errors.Errorf("Failed to sign round info: %v", err) + if err = signature.SignRsa(ri, testCert); err != nil { + return errors.Errorf("Failed to sign round info: %+v", err) } if err = instance.GetInstance().RoundUpdate(ri); err != nil { return errors.Errorf("Failed to RoundUpdate from from file: %v", err) @@ -124,3 +123,18 @@ func setupInstance(instance interfaces.NetworkManager) error { return nil } + +func TestGenerateIdentities(t *testing.T) { + eid, s, e, err := ephemeral.GetId(id.NewIdFromString("zezima", id.Node, t), 16, time.Now().UnixNano()) + if err != nil { + t.Errorf("Failed to get eid: %+v", err) + } + protoIds := []ephemeral.ProtoIdentity{{eid, s, e}} + generated := generateIdentities(protoIds, id.NewIdFromString("escaline", id.Node, t), 16) + if generated[0].EndValid != protoIds[0].End.Add(5*time.Minute) { + t.Errorf("End was not modified. Orig %+v, Generated %+v", protoIds[0].End, generated[0].End) + } + if generated[0].StartValid != protoIds[0].Start.Add(-5*time.Minute) { + t.Errorf("End was not modified. Orig %+v, Generated %+v", protoIds[0].End, generated[0].End) + } +} diff --git a/network/follow.go b/network/follow.go index 71eee53edaf362fdad6cfdeb700b74cdf96fbf94..2082a6ee4b5417a7de66b4ebc76209deff9072b7 100644 --- a/network/follow.go +++ b/network/follow.go @@ -23,15 +23,19 @@ package network // instance import ( + "bytes" "fmt" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces" "gitlab.com/elixxir/client/network/rounds" + "gitlab.com/elixxir/client/stoppable" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/primitives/knownRounds" + "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" "sync/atomic" "time" ) @@ -46,38 +50,54 @@ type followNetworkComms interface { // followNetwork polls the network to get updated on the state of nodes, the // round status, and informs the client when messages can be retrieved. -func (m *manager) followNetwork(report interfaces.ClientErrorReport, quitCh <-chan struct{}, isRunning interfaces.Running) { +func (m *manager) followNetwork(report interfaces.ClientErrorReport, + stop *stoppable.Single) { ticker := time.NewTicker(m.param.TrackNetworkPeriod) TrackTicker := time.NewTicker(debugTrackPeriod) rng := m.Rng.GetStream() - done := false - for !done { + for { select { - case <-quitCh: + case <-stop.Quit(): rng.Close() - done = true + stop.ToStopped() + return case <-ticker.C: - m.follow(report, rng, m.Comms, isRunning) + m.follow(report, rng, m.Comms, stop) case <-TrackTicker.C: numPolls := atomic.SwapUint64(m.tracker, 0) - jww.INFO.Printf("Polled the network %d times in the "+ - "last %s", numPolls, debugTrackPeriod) - } - if !isRunning.IsRunning(){ - jww.ERROR.Printf("Killing network follower " + - "due to failed exit") - return + if m.numLatencies != 0 { + latencyAvg := time.Nanosecond * time.Duration( + m.latencySum/m.numLatencies) + m.latencySum, m.numLatencies = 0, 0 + + infoMsg := fmt.Sprintf("Polled the network "+ + "%d times in the last %s, with an "+ + "average newest packet latency of %s", + numPolls, debugTrackPeriod, latencyAvg) + + jww.INFO.Printf(infoMsg) + m.Internal.Events.Report(1, "Polling", + "MetricsWithLatency", infoMsg) + } else { + infoMsg := fmt.Sprintf("Polled the network "+ + "%d times in the last %s", numPolls, + debugTrackPeriod) + + jww.INFO.Printf(infoMsg) + m.Internal.Events.Report(1, "Polling", + "Metrics", infoMsg) + } } } } // executes each iteration of the follower func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source, - comms followNetworkComms, isRunning interfaces.Running) { + comms followNetworkComms, stop *stoppable.Single) { - //get the identity we will poll for - identity, err := m.Session.Reception().GetIdentity(rng) + //Get the identity we will poll for + identity, err := m.Session.Reception().GetIdentity(rng, m.addrSpace.GetWithoutWait()) if err != nil { jww.FATAL.Panicf("Failed to get an identity, this should be "+ "impossible: %+v", err) @@ -95,22 +115,27 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source, }, LastUpdate: uint64(m.Instance.GetLastUpdateID()), ReceptionID: identity.EphId[:], - StartTimestamp: identity.StartRequest.UnixNano(), - EndTimestamp: identity.EndRequest.UnixNano(), + StartTimestamp: identity.StartValid.UnixNano(), + EndTimestamp: identity.EndValid.UnixNano(), ClientVersion: []byte(version.String()), + FastPolling: m.param.FastPolling, } result, err := m.GetSender().SendToAny(func(host *connect.Host) (interface{}, error) { jww.DEBUG.Printf("Executing poll for %v(%s) range: %s-%s(%s) from %s", - identity.EphId.Int64(), identity.Source, identity.StartRequest, - identity.EndRequest, identity.EndRequest.Sub(identity.StartRequest), host.GetId()) + identity.EphId.Int64(), identity.Source, identity.StartValid, + identity.EndValid, identity.StartValid.Sub(identity.EndValid), host.GetId()) return comms.SendPoll(host, &pollReq) - }) - if !isRunning.IsRunning(){ - jww.ERROR.Printf("Killing network follower " + - "due to failed exit") + }, stop) + + // Exit if the thread has been stopped + if stoppable.CheckErr(err) { + jww.INFO.Print(err) return } + + now := netTime.Now() + if err != nil { if report != nil { report( @@ -119,7 +144,9 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source, fmt.Sprintf("%+v", err), ) } - jww.ERROR.Printf("Unable to poll gateways: %+v", err) + errMsg := fmt.Sprintf("Unable to poll gateway: %+v", err) + m.Internal.Events.Report(10, "Polling", "Error", errMsg) + jww.ERROR.Printf(errMsg) return } @@ -145,12 +172,18 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source, // update gateway connections m.GetSender().UpdateNdf(m.GetInstance().GetPartialNdf().Get()) + m.Session.SetNDF(m.GetInstance().GetPartialNdf().Get()) + } + + // Update the address space size + // todo: this is a fix for incompatibility with the live network + // remove once the live network has been pushed to + if len(m.Instance.GetPartialNdf().Get().AddressSpace) != 0 { + m.addrSpace.Update(m.Instance.GetPartialNdf().Get().AddressSpace[0].Size) + } else { + m.addrSpace.Update(18) } - //check that the stored address space is correct - m.Session.Reception().UpdateIdSize(uint(m.Instance.GetPartialNdf().Get().AddressSpaceSize)) - // Updates any id size readers of a network compliant id size - m.Session.Reception().MarkIdSizeAsSet() // NOTE: this updates rounds and updates the tracking of the health of the // network if pollResp.Updates != nil { @@ -162,50 +195,64 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source, // TODO: ClientErr needs to know the source of the error and it doesn't yet // Iterate over ClientErrors for each RoundUpdate - //for _, update := range pollResp.Updates { - // - // // Ignore irrelevant updates - // if update.State != uint32(states.COMPLETED) && update.State != uint32(states.FAILED) { - // continue - // } - // - // for _, clientErr := range update.ClientErrors { - // // If this Client appears in the ClientError - // if bytes.Equal(clientErr.ClientId, m.Session.GetUser().TransmissionID.Marshal()) { - // - // // Obtain relevant NodeGateway information - // // TODO ??? - // nGw, err := m.Instance.GetNodeAndGateway(gwHost.GetId()) - // if err != nil { - // jww.ERROR.Printf("Unable to get NodeGateway: %+v", err) - // return - // } - // nid, err := nGw.Node.GetNodeId() - // if err != nil { - // jww.ERROR.Printf("Unable to get NodeID: %+v", err) - // return - // } - // - // // FIXME: Should be able to trigger proper type of round event - // // FIXME: without mutating the RoundInfo. Signature also needs verified - // // FIXME: before keys are deleted - // update.State = uint32(states.FAILED) - // rnd, err := m.Instance.GetWrappedRound(id.Round(update.ID)) - // if err != nil { - // jww.ERROR.Printf("Failed to report client error: "+ - // "Could not get round for event triggering: "+ - // "Unable to get round %d from instance: %+v", - // id.Round(update.ID), err) - // break - // } - // m.Instance.GetRoundEvents().TriggerRoundEvent(rnd) - // - // // delete all existing keys and trigger a re-registration with the relevant Node - // m.Session.Cmix().Remove(nid) - // m.Instance.GetAddGatewayChan() <- nGw - // } - // } - //} + for _, update := range pollResp.Updates { + + // Ignore irrelevant updates + if update.State != uint32(states.COMPLETED) && update.State != uint32(states.FAILED) { + continue + } + + for _, clientErr := range update.ClientErrors { + // If this Client appears in the ClientError + if bytes.Equal(clientErr.ClientId, m.Session.GetUser().TransmissionID.Marshal()) { + + // Obtain relevant NodeGateway information + nid, err := id.Unmarshal(clientErr.Source) + if err != nil { + jww.ERROR.Printf("Unable to get NodeID: %+v", err) + return + } + nGw, err := m.Instance.GetNodeAndGateway(nid) + if err != nil { + jww.ERROR.Printf("Unable to get gateway: %+v", err) + return + } + + // FIXME: Should be able to trigger proper type of round event + // FIXME: without mutating the RoundInfo. Signature also needs verified + // FIXME: before keys are deleted + update.State = uint32(states.FAILED) + rnd, err := m.Instance.GetWrappedRound(id.Round(update.ID)) + if err != nil { + jww.ERROR.Printf("Failed to report client error: "+ + "Could not get round for event triggering: "+ + "Unable to get round %d from instance: %+v", + id.Round(update.ID), err) + break + } + m.Instance.GetRoundEvents().TriggerRoundEvent(rnd) + + // delete all existing keys and trigger a re-registration with the relevant Node + m.Session.Cmix().Remove(nid) + m.Instance.GetAddGatewayChan() <- nGw + } + } + } + + newestTS := uint64(0) + for i := 0; i < len(pollResp.Updates[len(pollResp.Updates)-1].Timestamps); i++ { + if pollResp.Updates[len(pollResp.Updates)-1].Timestamps[i] != 0 { + newestTS = pollResp.Updates[len(pollResp.Updates)-1].Timestamps[i] + } + } + + newest := time.Unix(0, int64(newestTS)) + + if newest.After(now) { + deltaDur := newest.Sub(now) + m.latencySum = uint64(deltaDur) + m.numLatencies++ + } } // ---- Identity Specific Round Processing ----- diff --git a/network/gateway/hostPool.go b/network/gateway/hostPool.go index 34b381c67a928f0bce6c9e6d14e3fa9c87458644..52139d1a185b760c9d23cf408e65a86c7d2c67bd 100644 --- a/network/gateway/hostPool.go +++ b/network/gateway/hostPool.go @@ -13,7 +13,6 @@ package gateway import ( "encoding/binary" - "fmt" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/storage" @@ -42,6 +41,13 @@ type HostManager interface { RemoveHost(hid *id.ID) } +// Filter filters out IDs from the provided map based on criteria in the NDF. +// The passed in map is a map of the NDF for easier acesss. The map is ID -> index in the NDF +// There is no multithreading, the filter function can either edit the passed map or make a new one +// and return it. The general pattern is to loop through the map, then look up data about the node +// in the ndf to make a filtering decision, then add them to a new map if they are accepted. +type Filter func(map[id.ID]int, *ndf.NetworkDefinition) map[id.ID]int + // HostPool Handles providing hosts to the Client type HostPool struct { hostMap map[id.ID]uint32 // map key to its index in the slice @@ -57,6 +63,9 @@ type HostPool struct { storage *storage.Session manager HostManager addGatewayChan chan network.NodeGateway + + filterMux sync.Mutex + filter Filter } // PoolParams Allows configuration of HostPool parameters @@ -78,21 +87,23 @@ func DefaultPoolParams() PoolParams { } p.HostParams.MaxRetries = 1 p.HostParams.AuthEnabled = false - p.HostParams.EnableCoolOff = true + p.HostParams.EnableCoolOff = false p.HostParams.NumSendsBeforeCoolOff = 1 p.HostParams.CoolOffTimeout = 5 * time.Minute - p.HostParams.SendTimeout = 3500 * time.Millisecond + p.HostParams.SendTimeout = 2000 * time.Millisecond return p } // Build and return new HostPool object -func newHostPool(poolParams PoolParams, rng *fastRNG.StreamGenerator, ndf *ndf.NetworkDefinition, getter HostManager, - storage *storage.Session, addGateway chan network.NodeGateway) (*HostPool, error) { +func newHostPool(poolParams PoolParams, rng *fastRNG.StreamGenerator, + netDef *ndf.NetworkDefinition, getter HostManager, storage *storage.Session, + addGateway chan network.NodeGateway) (*HostPool, error) { var err error // Determine size of HostPool if poolParams.PoolSize == 0 { - poolParams.PoolSize, err = getPoolSize(uint32(len(ndf.Gateways)), poolParams.MaxPoolSize) + poolParams.PoolSize, err = getPoolSize(uint32(len(netDef.Gateways)), + poolParams.MaxPoolSize) if err != nil { return nil, err } @@ -103,10 +114,15 @@ func newHostPool(poolParams PoolParams, rng *fastRNG.StreamGenerator, ndf *ndf.N hostMap: make(map[id.ID]uint32), hostList: make([]*connect.Host, poolParams.PoolSize), poolParams: poolParams, - ndf: ndf, + ndf: netDef, rng: rng, storage: storage, addGatewayChan: addGateway, + + // Initialise the filter so it does not filter any IDs + filter: func(m map[id.ID]int, _ *ndf.NetworkDefinition) map[id.ID]int { + return m + }, } // Propagate the NDF @@ -115,15 +131,31 @@ func newHostPool(poolParams PoolParams, rng *fastRNG.StreamGenerator, ndf *ndf.N return nil, err } + // Get the last used list of hosts and use it to seed the host pool list + hostList, err := storage.HostList().Get() + numHostsAdded := 0 + if err == nil { + for _, hid := range hostList { + err := result.replaceHostNoStore(hid, uint32(numHostsAdded)) + if err != nil { + jww.WARN.Printf("Unable to add stored host %s: %s", hid, err.Error()) + } else { + numHostsAdded++ + } + } + } else { + jww.WARN.Printf("Building new HostPool because no HostList stored: %+v", err) + } + // Build the initial HostPool and return - for i := 0; i < len(result.hostList); i++ { + for i := numHostsAdded; i < len(result.hostList); i++ { err := result.forceReplace(uint32(i)) if err != nil { return nil, err } } - jww.INFO.Printf("Initialized HostPool with size: %d/%d", poolParams.PoolSize, len(ndf.Gateways)) + jww.INFO.Printf("Initialized HostPool with size: %d/%d", poolParams.PoolSize, len(netDef.Gateways)) return result, nil } @@ -146,6 +178,22 @@ func (h *HostPool) UpdateNdf(ndf *ndf.NetworkDefinition) { h.ndfMux.Unlock() } +// SetFilter sets the filter used to filter gateways from the ID map. +func (h *HostPool) SetFilter(f Filter) { + h.filterMux.Lock() + defer h.filterMux.Unlock() + + h.filter = f +} + +// getFilter returns the filter used to filter gateways from the ID map. +func (h *HostPool) getFilter() Filter { + h.filterMux.Lock() + defer h.filterMux.Unlock() + + return h.filter +} + // Obtain a random, unique list of Hosts of the given length from the HostPool func (h *HostPool) getAny(length uint32, excluded []*id.ID) []*connect.Host { if length > h.poolParams.PoolSize { @@ -225,7 +273,9 @@ func (h *HostPool) getPreferred(targets []*id.ID) []*connect.Host { } // Replaces the given hostId in the HostPool if the given hostErr is in errorList -func (h *HostPool) checkReplace(hostId *id.ID, hostErr error) error { +// Returns whether the host was replaced +func (h *HostPool) checkReplace(hostId *id.ID, hostErr error) (bool, error) { + var err error // Check if Host should be replaced doReplace := false if hostErr != nil { @@ -239,19 +289,17 @@ func (h *HostPool) checkReplace(hostId *id.ID, hostErr error) error { } if doReplace { - h.hostMux.Lock() - defer h.hostMux.Unlock() - // If the Host is still in the pool + h.hostMux.Lock() if oldPoolIndex, ok := h.hostMap[*hostId]; ok { // Replace it h.ndfMux.RLock() - err := h.forceReplace(oldPoolIndex) + err = h.forceReplace(oldPoolIndex) h.ndfMux.RUnlock() - return err } + h.hostMux.Unlock() } - return nil + return doReplace, err } // Replace given Host index with a new, randomly-selected Host from the NDF @@ -279,8 +327,29 @@ func (h *HostPool) forceReplace(oldPoolIndex uint32) error { } } -// Replace the given slot in the HostPool with a new Gateway with the specified ID +// replaceHost replaces the given slot in the HostPool with a new Gateway with +// the specified ID. The resulting host list is saved to storage. func (h *HostPool) replaceHost(newId *id.ID, oldPoolIndex uint32) error { + err := h.replaceHostNoStore(newId, oldPoolIndex) + if err != nil { + return err + } + + // Convert list of of non-nil and non-zero hosts to ID list + idList := make([]*id.ID, 0, len(h.hostList)) + for _, host := range h.hostList { + if host.GetId() != nil && !host.GetId().Cmp(&id.ID{}) { + idList = append(idList, host.GetId()) + } + } + + // Save the list to storage + return h.storage.HostList().Store(idList) +} + +// replaceHostNoStore replaces the given slot in the HostPool with a new Gateway +// with the specified ID. +func (h *HostPool) replaceHostNoStore(newId *id.ID, oldPoolIndex uint32) error { // Obtain that GwId's Host object newHost, ok := h.manager.GetHost(newId) if !ok { @@ -291,7 +360,8 @@ func (h *HostPool) replaceHost(newId *id.ID, oldPoolIndex uint32) error { // Keep track of oldHost for cleanup oldHost := h.hostList[oldPoolIndex] - // Use the poolIdx to overwrite the random Host in the corresponding index in the hostList + // Use the poolIdx to overwrite the random Host in the corresponding index + // in the hostList h.hostList[oldPoolIndex] = newHost // Use the GwId to keep track of the new random Host's index in the hostList h.hostMap[*newId] = oldPoolIndex @@ -301,7 +371,9 @@ func (h *HostPool) replaceHost(newId *id.ID, oldPoolIndex uint32) error { delete(h.hostMap, *oldHost.GetId()) go oldHost.Disconnect() } - jww.DEBUG.Printf("Replaced Host at %d with new Host %s", oldPoolIndex, newId.String()) + jww.DEBUG.Printf("Replaced Host at %d with new Host %s", oldPoolIndex, + newId.String()) + return nil } @@ -331,6 +403,9 @@ func (h *HostPool) updateConns() error { return errors.Errorf("Unable to convert new NDF to set: %+v", err) } + // Filter out gateway IDs + newMap = h.getFilter()(newMap, h.ndf) + // Handle adding Gateways for gwId, ndfIdx := range newMap { if _, ok := h.ndfMap[gwId]; !ok { @@ -388,7 +463,7 @@ func (h *HostPool) removeGateway(gwId *id.ID) { func (h *HostPool) addGateway(gwId *id.ID, ndfIndex int) { gw := h.ndf.Gateways[ndfIndex] - //check if the host exists + // Check if the host exists host, ok := h.manager.GetHost(gwId) if !ok { @@ -443,7 +518,7 @@ func readUint32(rng io.Reader) uint32 { var rndBytes [4]byte i, err := rng.Read(rndBytes[:]) if i != 4 || err != nil { - panic(fmt.Sprintf("cannot read from rng: %+v", err)) + jww.FATAL.Panicf("cannot read from rng: %+v", err) } return binary.BigEndian.Uint32(rndBytes[:]) } diff --git a/network/gateway/hostpool_test.go b/network/gateway/hostpool_test.go index 8181e6d49f0ea5522e8ae33eb6ac143516c45a26..e06d49dab9f88673e8a1fea3c4a364e7697c4190 100644 --- a/network/gateway/hostpool_test.go +++ b/network/gateway/hostpool_test.go @@ -54,6 +54,49 @@ func TestNewHostPool(t *testing.T) { } } +// Tests that the hosts are loaded from storage, if they exist. +func TestNewHostPool_HostListStore(t *testing.T) { + manager := newMockManager() + rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG) + testNdf := getTestNdf(t) + testStorage := storage.InitTestingSession(t) + addGwChan := make(chan network.NodeGateway) + params := DefaultPoolParams() + params.MaxPoolSize = uint32(len(testNdf.Gateways)) + + addedIDs := []*id.ID{ + id.NewIdFromString("testID0", id.Gateway, t), + id.NewIdFromString("testID1", id.Gateway, t), + id.NewIdFromString("testID2", id.Gateway, t), + id.NewIdFromString("testID3", id.Gateway, t), + } + err := testStorage.HostList().Store(addedIDs) + if err != nil { + t.Fatalf("Failed to store host list: %+v", err) + } + + for i, hid := range addedIDs { + testNdf.Gateways[i].ID = hid.Marshal() + } + + // Call the constructor + hp, err := newHostPool(params, rng, testNdf, manager, testStorage, addGwChan) + if err != nil { + t.Fatalf("Failed to create mock host pool: %v", err) + } + + // Check that the host list was saved to storage + hostList, err := hp.storage.HostList().Get() + if err != nil { + t.Errorf("Failed to get host list: %+v", err) + } + + if !reflect.DeepEqual(addedIDs, hostList) { + t.Errorf("Failed to save expected host list to storage."+ + "\nexpected: %+v\nreceived: %+v", addedIDs, hostList) + } +} + // Unit test func TestHostPool_ManageHostPool(t *testing.T) { manager := newMockManager() @@ -115,7 +158,7 @@ func TestHostPool_ManageHostPool(t *testing.T) { for _, ndfGw := range testNdf.Gateways { gwId, err := id.Unmarshal(ndfGw.ID) if err != nil { - t.Errorf("Failed to marshal gateway id for %v", ndfGw) + t.Fatalf("Failed to marshal gateway id for %v", ndfGw) } if _, ok := testPool.hostMap[*gwId]; ok { t.Errorf("Expected gateway %v to be removed from pool", gwId) @@ -135,6 +178,7 @@ func TestHostPool_ReplaceHost(t *testing.T) { hostList: make([]*connect.Host, newIndex+1), hostMap: make(map[id.ID]uint32), ndf: testNdf, + storage: storage.InitTestingSession(t), } /* "Replace" a host with no entry */ @@ -228,6 +272,18 @@ func TestHostPool_ReplaceHost(t *testing.T) { "\n\tReceived: %d", newIndex, retrievedIndex) } + // Check that the host list was saved to storage + hostList, err := hostPool.storage.HostList().Get() + if err != nil { + t.Errorf("Failed to get host list: %+v", err) + } + + expectedList := []*id.ID{gwIdTwo} + + if !reflect.DeepEqual(expectedList, hostList) { + t.Errorf("Failed to save expected host list to storage."+ + "\nexpected: %+v\nreceived: %+v", expectedList, hostList) + } } // Error path, could not get host @@ -359,10 +415,13 @@ func TestHostPool_CheckReplace(t *testing.T) { oldGatewayIndex := 0 oldHost := testPool.hostList[oldGatewayIndex] expectedError := fmt.Errorf(errorsList[0]) - err = testPool.checkReplace(oldHost.GetId(), expectedError) + wasReplaced, err := testPool.checkReplace(oldHost.GetId(), expectedError) if err != nil { t.Errorf("Failed to check replace: %v", err) } + if !wasReplaced { + t.Errorf("Expected to replace") + } // Ensure that old gateway has been removed from the map if _, ok := testPool.hostMap[*oldHost.GetId()]; ok { @@ -378,10 +437,13 @@ func TestHostPool_CheckReplace(t *testing.T) { goodGatewayIndex := 0 goodGateway := testPool.hostList[goodGatewayIndex] unexpectedErr := fmt.Errorf("not in global error list") - err = testPool.checkReplace(oldHost.GetId(), unexpectedErr) + wasReplaced, err = testPool.checkReplace(oldHost.GetId(), unexpectedErr) if err != nil { t.Errorf("Failed to check replace: %v", err) } + if wasReplaced { + t.Errorf("Expected not to replace") + } // Ensure that gateway with an unexpected error was not modified if _, ok := testPool.hostMap[*goodGateway.GetId()]; !ok { @@ -408,6 +470,9 @@ func TestHostPool_UpdateNdf(t *testing.T) { hostMap: make(map[id.ID]uint32), ndf: testNdf, storage: storage.InitTestingSession(t), + filter: func(m map[id.ID]int, _ *ndf.NetworkDefinition) map[id.ID]int { + return m + }, } // Construct a new Ndf different from original one above @@ -748,7 +813,7 @@ func TestHostPool_UpdateConns_RemoveGateways(t *testing.T) { for _, ndfGw := range testNdf.Gateways { gwId, err := id.Unmarshal(ndfGw.ID) if err != nil { - t.Errorf("Failed to marshal gateway id for %v", ndfGw) + t.Fatalf("Failed to marshal gateway id for %v", ndfGw) } if _, ok := testPool.hostMap[*gwId]; ok { t.Errorf("Expected gateway %v to be removed from pool", gwId) diff --git a/network/gateway/sender.go b/network/gateway/sender.go index dcfcdbb5b88a0be02b67f4f61dfcaa01c1169489..ddd41478cc7845347bd5adde640794766cf9cc8f 100644 --- a/network/gateway/sender.go +++ b/network/gateway/sender.go @@ -11,6 +11,7 @@ package gateway import ( "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/comms/network" "gitlab.com/elixxir/crypto/fastRNG" @@ -35,55 +36,19 @@ func NewSender(poolParams PoolParams, rng *fastRNG.StreamGenerator, ndf *ndf.Net return &Sender{hostPool}, nil } -// SendToSpecific Call given sendFunc to a specific Host in the HostPool, -// attempting with up to numProxies destinations in case of failure -func (s *Sender) SendToSpecific(target *id.ID, - sendFunc func(host *connect.Host, target *id.ID) (interface{}, bool, error)) (interface{}, error) { - host, ok := s.getSpecific(target) - if ok { - result, didAbort, err := sendFunc(host, target) - if err == nil { - return result, s.forceAdd(target) - } else { - if didAbort { - return nil, errors.WithMessagef(err, "Aborted SendToSpecific gateway %s", host.GetId().String()) - } - jww.WARN.Printf("Unable to SendToSpecific %s: %s", host.GetId().String(), err) - } - } - - proxies := s.getAny(s.poolParams.ProxyAttempts, []*id.ID{target}) - for i := range proxies { - result, didAbort, err := sendFunc(proxies[i], target) - if err == nil { - return result, nil - } else { - if didAbort { - return nil, errors.WithMessagef(err, "Aborted SendToSpecific gateway proxy %s", - host.GetId().String()) - } - jww.WARN.Printf("Unable to SendToSpecific proxy %s: %s", proxies[i].GetId().String(), err) - err = s.checkReplace(proxies[i].GetId(), err) - if err != nil { - jww.ERROR.Printf("Unable to checkReplace: %+v", err) - } - } - } - - return nil, errors.Errorf("Unable to send to specific with proxies") -} - // SendToAny Call given sendFunc to any Host in the HostPool, attempting with up to numProxies destinations -func (s *Sender) SendToAny(sendFunc func(host *connect.Host) (interface{}, error)) (interface{}, error) { +func (s *Sender) SendToAny(sendFunc func(host *connect.Host) (interface{}, error), stop *stoppable.Single) (interface{}, error) { proxies := s.getAny(s.poolParams.ProxyAttempts, nil) for i := range proxies { result, err := sendFunc(proxies[i]) - if err == nil { + if stop != nil && !stop.IsRunning() { + return nil, errors.Errorf(stoppable.ErrMsg, stop.Name(), "SendToAny") + } else if err == nil { return result, nil } else { jww.WARN.Printf("Unable to SendToAny %s: %s", proxies[i].GetId().String(), err) - err = s.checkReplace(proxies[i].GetId(), err) + _, err = s.checkReplace(proxies[i].GetId(), err) if err != nil { jww.ERROR.Printf("Unable to checkReplace: %+v", err) } @@ -95,35 +60,81 @@ func (s *Sender) SendToAny(sendFunc func(host *connect.Host) (interface{}, error // SendToPreferred Call given sendFunc to any Host in the HostPool, attempting with up to numProxies destinations func (s *Sender) SendToPreferred(targets []*id.ID, - sendFunc func(host *connect.Host, target *id.ID) (interface{}, error)) (interface{}, error) { + sendFunc func(host *connect.Host, target *id.ID) (interface{}, bool, error), + stop *stoppable.Single) (interface{}, error) { + // Get the hosts and shuffle randomly targetHosts := s.getPreferred(targets) + + // Attempt to send directly to targets if they are in the HostPool for i := range targetHosts { - result, err := sendFunc(targetHosts[i], targets[i]) - if err == nil { + result, didAbort, err := sendFunc(targetHosts[i], targets[i]) + if stop != nil && !stop.IsRunning() { + return nil, errors.Errorf(stoppable.ErrMsg, stop.Name(), "SendToPreferred") + } else if err == nil { return result, nil } else { + if didAbort { + return nil, errors.WithMessagef(err, "Aborted SendToPreferred gateway %s", + targetHosts[i].GetId().String()) + } jww.WARN.Printf("Unable to SendToPreferred %s via %s: %s", targets[i], targetHosts[i].GetId(), err) - err = s.checkReplace(targetHosts[i].GetId(), err) + _, err = s.checkReplace(targetHosts[i].GetId(), err) if err != nil { jww.ERROR.Printf("Unable to checkReplace: %+v", err) } } } - proxies := s.getAny(s.poolParams.ProxyAttempts, targets) - for i := range proxies { - target := targets[i%len(targets)].DeepCopy() - result, err := sendFunc(proxies[i], target) - if err == nil { - return result, nil - } else { - jww.WARN.Printf("Unable to SendToPreferred %s via proxy "+ - "%s: %s", target, proxies[i].GetId(), err) - err = s.checkReplace(proxies[i].GetId(), err) - if err != nil { - jww.ERROR.Printf("Unable to checkReplace: %+v", err) + // Build a list of proxies for every target + proxies := make([][]*connect.Host, len(targets)) + for i := 0; i < len(targets); i++ { + proxies[i] = s.getAny(s.poolParams.ProxyAttempts, targets) + } + + // Build a map of bad proxies + badProxies := make(map[string]interface{}) + + // Iterate between each target's list of proxies, using the next target for each proxy + + for proxyIdx := uint32(0); proxyIdx < s.poolParams.ProxyAttempts; proxyIdx++ { + for targetIdx := range proxies { + target := targets[targetIdx] + targetProxies := proxies[targetIdx] + if !(int(proxyIdx) < len(targetProxies)) { + jww.WARN.Printf("Failed to send to proxy %d on target %d (%s) "+ + "due to not enough proxies (only %d), skipping attempt", proxyIdx, + targetIdx, target, len(targetProxies)) + continue + } + proxy := targetProxies[proxyIdx] + + // Skip bad proxies + if _, ok := badProxies[proxy.String()]; ok { + continue + } + + result, didAbort, err := sendFunc(targetProxies[proxyIdx], target) + if stop != nil && !stop.IsRunning() { + return nil, errors.Errorf(stoppable.ErrMsg, stop.Name(), "SendToPreferred") + } else if err == nil { + return result, nil + } else { + if didAbort { + return nil, errors.WithMessagef(err, "Aborted SendToPreferred gateway proxy %s", + proxy.GetId().String()) + } + jww.WARN.Printf("Unable to SendToPreferred %s via proxy "+ + "%s: %s", target, proxy.GetId(), err) + wasReplaced, err := s.checkReplace(proxy.GetId(), err) + if err != nil { + jww.ERROR.Printf("Unable to checkReplace: %+v", err) + } + // If the proxy was replaced, add as a bad proxy + if wasReplaced { + badProxies[proxy.String()] = nil + } } } } diff --git a/network/gateway/sender_test.go b/network/gateway/sender_test.go index 4dd5d49c02ea525e547164f8c2c6392b526b30de..d8dbf16227e8724ef509589bb7133efea888cc26 100644 --- a/network/gateway/sender_test.go +++ b/network/gateway/sender_test.go @@ -78,7 +78,7 @@ func TestSender_SendToAny(t *testing.T) { } // Test sendToAny with test interfaces - result, err := sender.SendToAny(SendToAny_HappyPath) + result, err := sender.SendToAny(SendToAny_HappyPath, nil) if err != nil { t.Errorf("Should not error in SendToAny happy path: %v", err) } @@ -89,12 +89,12 @@ func TestSender_SendToAny(t *testing.T) { "\n\tReceived: %v", happyPathReturn, result) } - _, err = sender.SendToAny(SendToAny_KnownError) + _, err = sender.SendToAny(SendToAny_KnownError, nil) if err == nil { t.Fatalf("Expected error path did not receive error") } - _, err = sender.SendToAny(SendToAny_UnknownError) + _, err = sender.SendToAny(SendToAny_UnknownError, nil) if err == nil { t.Fatalf("Expected error path did not receive error") } @@ -139,7 +139,7 @@ func TestSender_SendToPreferred(t *testing.T) { preferredHost := sender.hostList[preferredIndex] // Happy path - result, err := sender.SendToPreferred([]*id.ID{preferredHost.GetId()}, SendToPreferred_HappyPath) + result, err := sender.SendToPreferred([]*id.ID{preferredHost.GetId()}, SendToPreferred_HappyPath, nil) if err != nil { t.Errorf("Should not error in SendToPreferred happy path: %v", err) } @@ -151,7 +151,7 @@ func TestSender_SendToPreferred(t *testing.T) { } // Call a send which returns an error which triggers replacement - _, err = sender.SendToPreferred([]*id.ID{preferredHost.GetId()}, SendToPreferred_KnownError) + _, err = sender.SendToPreferred([]*id.ID{preferredHost.GetId()}, SendToPreferred_KnownError, nil) if err == nil { t.Fatalf("Expected error path did not receive error") } @@ -171,7 +171,7 @@ func TestSender_SendToPreferred(t *testing.T) { preferredHost = sender.hostList[preferredIndex] // Unknown error return will not trigger replacement - _, err = sender.SendToPreferred([]*id.ID{preferredHost.GetId()}, SendToPreferred_UnknownError) + _, err = sender.SendToPreferred([]*id.ID{preferredHost.GetId()}, SendToPreferred_UnknownError, nil) if err == nil { t.Fatalf("Expected error path did not receive error") } @@ -187,63 +187,3 @@ func TestSender_SendToPreferred(t *testing.T) { } } - -func TestSender_SendToSpecific(t *testing.T) { - manager := newMockManager() - rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG) - testNdf := getTestNdf(t) - testStorage := storage.InitTestingSession(t) - addGwChan := make(chan network.NodeGateway) - params := DefaultPoolParams() - params.MaxPoolSize = uint32(len(testNdf.Gateways)) - 5 - - // Do not test proxy attempts code in this test - // (self contain to code specific in sendPreferred) - params.ProxyAttempts = 0 - - // Pull all gateways from ndf into host manager - for _, gw := range testNdf.Gateways { - - gwId, err := id.Unmarshal(gw.ID) - if err != nil { - t.Fatalf("Failed to unmarshal ID in mock ndf: %v", err) - } - // Add mock gateway to manager - _, err = manager.AddHost(gwId, gw.Address, nil, connect.GetDefaultHostParams()) - if err != nil { - t.Fatalf("Could not add mock host to manager: %v", err) - } - - } - - sender, err := NewSender(params, rng, testNdf, manager, testStorage, addGwChan) - if err != nil { - t.Fatalf("Failed to create mock sender: %v", err) - } - - preferredIndex := 0 - preferredHost := sender.hostList[preferredIndex] - - // Happy path - result, err := sender.SendToSpecific(preferredHost.GetId(), SendToSpecific_HappyPath) - if err != nil { - t.Errorf("Should not error in SendToSpecific happy path: %v", err) - } - - if !reflect.DeepEqual(result, happyPathReturn) { - t.Errorf("Expected result not returnev via SendToSpecific interface."+ - "\n\tExpected: %v"+ - "\n\tReceived: %v", happyPathReturn, result) - } - - // Ensure host is now in map - if _, ok := sender.hostMap[*preferredHost.GetId()]; !ok { - t.Errorf("Failed to forcefully add new gateway ID: %v", preferredHost.GetId()) - } - - _, err = sender.SendToSpecific(preferredHost.GetId(), SendToSpecific_Abort) - if err == nil { - t.Errorf("Expected sendSpecific to return an abort") - } - -} diff --git a/network/gateway/utils_test.go b/network/gateway/utils_test.go index 0ec7dc11f8edde72a82affd3b718bcec121bf60c..9f75ace1429e947ef7c8a399f3f088e2583777de 100644 --- a/network/gateway/utils_test.go +++ b/network/gateway/utils_test.go @@ -129,16 +129,16 @@ func getTestNdf(face interface{}) *ndf.NetworkDefinition { const happyPathReturn = "happyPathReturn" -func SendToPreferred_HappyPath(host *connect.Host, target *id.ID) (interface{}, error) { - return happyPathReturn, nil +func SendToPreferred_HappyPath(host *connect.Host, target *id.ID) (interface{}, bool, error) { + return happyPathReturn, false, nil } -func SendToPreferred_KnownError(host *connect.Host, target *id.ID) (interface{}, error) { - return nil, fmt.Errorf(errorsList[0]) +func SendToPreferred_KnownError(host *connect.Host, target *id.ID) (interface{}, bool, error) { + return nil, false, fmt.Errorf(errorsList[0]) } -func SendToPreferred_UnknownError(host *connect.Host, target *id.ID) (interface{}, error) { - return nil, fmt.Errorf("Unexpected error: Oopsie") +func SendToPreferred_UnknownError(host *connect.Host, target *id.ID) (interface{}, bool, error) { + return nil, false, fmt.Errorf("Unexpected error: Oopsie") } func SendToAny_HappyPath(host *connect.Host) (interface{}, error) { diff --git a/network/health/tracker.go b/network/health/tracker.go index ff53904f2015fe81af4938efd9ca8222fd68eba1..7d5b6a7d9c36edd2723685276c385c114b20bc13 100644 --- a/network/health/tracker.go +++ b/network/health/tracker.go @@ -5,7 +5,8 @@ // LICENSE file // /////////////////////////////////////////////////////////////////////////////// -// Contains functionality related to the event model driven network health tracker +// Contains functionality related to the event model driven network health +// tracker. package health @@ -23,70 +24,108 @@ type Tracker struct { heartbeat chan network.Heartbeat - channels []chan bool - funcs []func(isHealthy bool) + channels map[uint64]chan bool + funcs map[uint64]func(isHealthy bool) + channelsID uint64 + funcsID uint64 running bool // Determines the current health status isHealthy bool - // Denotes the past health status - // wasHealthy is true if isHealthy has ever been true + + // Denotes that the past health status wasHealthy is true if isHealthy has + // ever been true wasHealthy bool mux sync.RWMutex } -// Creates a single HealthTracker thread, starts it, and returns a tracker and a stoppable +// Init creates a single HealthTracker thread, starts it, and returns a tracker +// and a stoppable. func Init(instance *network.Instance, timeout time.Duration) *Tracker { - tracker := newTracker(timeout) instance.SetNetworkHealthChan(tracker.heartbeat) return tracker } -// Builds and returns a new Tracker object given a Context +// newTracker builds and returns a new Tracker object given a Context. func newTracker(timeout time.Duration) *Tracker { return &Tracker{ timeout: timeout, - channels: make([]chan bool, 0), + channels: map[uint64]chan bool{}, + funcs: map[uint64]func(isHealthy bool){}, heartbeat: make(chan network.Heartbeat, 100), isHealthy: false, running: false, } } -// Add a channel to the list of Tracker channels -// such that each channel can be notified of network changes -func (t *Tracker) AddChannel(c chan bool) { +// AddChannel adds a channel to the list of Tracker channels such that each +// channel can be notified of network changes. Returns a unique ID for the +// channel. +func (t *Tracker) AddChannel(c chan bool) uint64 { + var currentID uint64 + t.mux.Lock() - t.channels = append(t.channels, c) + t.channels[t.channelsID] = c + currentID = t.channelsID + t.channelsID++ t.mux.Unlock() + select { case c <- t.IsHealthy(): default: } + + return currentID } -// Add a function to the list of Tracker function -// such that each function can be run after network changes -func (t *Tracker) AddFunc(f func(isHealthy bool)) { +// RemoveChannel removes the channel with the given ID from the list of Tracker +// channels so that it will not longer be notified of network changes. +func (t *Tracker) RemoveChannel(chanID uint64) { t.mux.Lock() - t.funcs = append(t.funcs, f) + delete(t.channels, chanID) t.mux.Unlock() +} + +// AddFunc adds a function to the list of Tracker functions such that each +// function can be run after network changes. Returns a unique ID for the +// function. +func (t *Tracker) AddFunc(f func(isHealthy bool)) uint64 { + var currentID uint64 + + t.mux.Lock() + t.funcs[t.funcsID] = f + currentID = t.funcsID + t.funcsID++ + t.mux.Unlock() + go f(t.IsHealthy()) + + return currentID +} + +// RemoveFunc removes the function with the given ID from the list of Tracker +// functions so that it will not longer be run. +func (t *Tracker) RemoveFunc(chanID uint64) { + t.mux.Lock() + delete(t.channels, chanID) + t.mux.Unlock() } func (t *Tracker) IsHealthy() bool { t.mux.RLock() defer t.mux.RUnlock() + return t.isHealthy } -// Returns true if isHealthy has ever been true +// WasHealthy returns true if isHealthy has ever been true. func (t *Tracker) WasHealthy() bool { t.mux.RLock() defer t.mux.RUnlock() + return t.wasHealthy } @@ -94,10 +133,11 @@ func (t *Tracker) setHealth(h bool) { t.mux.Lock() // Only set wasHealthy to true if either // wasHealthy is true or - // wasHealthy false but h value is true + // wasHealthy is false but h value is true t.wasHealthy = t.wasHealthy || h t.isHealthy = h t.mux.Unlock() + t.transmit(h) } @@ -114,25 +154,29 @@ func (t *Tracker) Start() (stoppable.Stoppable, error) { stop := stoppable.NewSingle("Health Tracker") - go t.start(stop.Quit()) + go t.start(stop) return stop, nil } -// Long-running thread used to monitor and report on network health -func (t *Tracker) start(quitCh <-chan struct{}) { +// start starts a long-running thread used to monitor and report on network +// health. +func (t *Tracker) start(stop *stoppable.Single) { timer := time.NewTimer(t.timeout) for { var heartbeat network.Heartbeat select { - case <-quitCh: + case <-stop.Quit(): t.mux.Lock() t.isHealthy = false t.running = false t.mux.Unlock() + t.transmit(false) - break + stop.ToStopped() + + return case heartbeat = <-t.heartbeat: if healthy(heartbeat) { // Stop and reset timer @@ -146,10 +190,11 @@ func (t *Tracker) start(quitCh <-chan struct{}) { timer.Reset(t.timeout) t.setHealth(true) } - break case <-timer.C: + if !t.isHealthy { + jww.WARN.Printf("Network health tracker timed out, network is no longer healthy...") + } t.setHealth(false) - break } } } diff --git a/network/health/tracker_test.go b/network/health/tracker_test.go index 4a10843c36ef23bcd3dc8cfbb5699e71a9643e78..a2e20651adaa06781f4d685cb5502cd5b56faae0 100644 --- a/network/health/tracker_test.go +++ b/network/health/tracker_test.go @@ -9,12 +9,11 @@ package health import ( "gitlab.com/elixxir/comms/network" - // "gitlab.com/elixxir/comms/network" "testing" "time" ) -// Happy path smoke test +// Happy path smoke test. func TestNewTracker(t *testing.T) { // Initialize required variables timeout := 250 * time.Millisecond @@ -49,8 +48,7 @@ func TestNewTracker(t *testing.T) { // Begin the health tracker _, err := tracker.Start() if err != nil { - t.Errorf("Unable to start tracker: %+v", err) - return + t.Fatalf("Unable to start tracker: %+v", err) } // Send a positive health heartbeat @@ -68,14 +66,12 @@ func TestNewTracker(t *testing.T) { // Verify the network was marked as healthy if !tracker.IsHealthy() { - t.Errorf("Tracker did not become healthy") - return + t.Fatal("Tracker did not become healthy.") } // Check if the tracker was ever healthy if !tracker.WasHealthy() { - t.Errorf("Tracker did not become healthy") - return + t.Fatal("Tracker did not become healthy.") } // Verify the heartbeat triggered the listening chan/func @@ -89,15 +85,12 @@ func TestNewTracker(t *testing.T) { // Verify the network was marked as NOT healthy if tracker.IsHealthy() { - t.Errorf("Tracker should not report healthy") - return + t.Fatal("Tracker should not report healthy.") } - // Check if the tracker was ever healthy, - // after setting healthy to false + // Check if the tracker was ever healthy, after setting healthy to false if !tracker.WasHealthy() { - t.Errorf("Tracker was healthy previously but not reported healthy") - return + t.Fatal("Tracker was healthy previously but not reported healthy.") } // Verify the timeout triggered the listening chan/func diff --git a/network/internal/internal.go b/network/internal/internal.go index fc0d6aa429348b43469494b3136abc30c347e6da..8fe96d073d123e971f14f3a3985bb05c30c0ce10 100644 --- a/network/internal/internal.go +++ b/network/internal/internal.go @@ -8,6 +8,7 @@ package internal import ( + "gitlab.com/elixxir/client/interfaces" "gitlab.com/elixxir/client/network/health" "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/client/switchboard" @@ -37,4 +38,7 @@ type Internal struct { //channels NodeRegistration chan network.NodeGateway + + // Event Reporting + Events interfaces.EventManager } diff --git a/network/manager.go b/network/manager.go index 1cf065d691b44ca061404b426af7c26df965ca40..e26c455b39c25b594780ef7695f84bf207414b9b 100644 --- a/network/manager.go +++ b/network/manager.go @@ -11,6 +11,7 @@ package network // and intraclient state are accessible through the context object. import ( + "fmt" "github.com/pkg/errors" "gitlab.com/elixxir/client/interfaces" "gitlab.com/elixxir/client/interfaces/params" @@ -28,6 +29,8 @@ import ( "gitlab.com/elixxir/comms/network" "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/xx_network/primitives/ndf" + "math" + "time" ) // Manager implements the NetworkManager interface inside context. It @@ -47,16 +50,25 @@ type manager struct { message *message.Manager //number of polls done in a period of time - tracker *uint64 + tracker *uint64 + latencySum uint64 + numLatencies uint64 + + // Address space size + addrSpace *ephemeral.AddressSpace + + // Event reporting api + events interfaces.EventManager } // NewManager builds a new reception manager object using inputted key fields func NewManager(session *storage.Session, switchboard *switchboard.Switchboard, - rng *fastRNG.StreamGenerator, comms *client.Comms, - params params.Network, ndf *ndf.NetworkDefinition) (interfaces.NetworkManager, error) { + rng *fastRNG.StreamGenerator, events interfaces.EventManager, + comms *client.Comms, params params.Network, + ndf *ndf.NetworkDefinition) (interfaces.NetworkManager, error) { //start network instance - instance, err := network.NewInstance(comms.ProtoComms, ndf, nil, nil, network.None) + instance, err := network.NewInstance(comms.ProtoComms, ndf, nil, nil, network.None, params.FastPolling) if err != nil { return nil, errors.WithMessage(err, "failed to create"+ " client network manager") @@ -69,10 +81,12 @@ func NewManager(session *storage.Session, switchboard *switchboard.Switchboard, tracker := uint64(0) - //create manager object + // create manager object m := manager{ - param: params, - tracker: &tracker, + param: params, + tracker: &tracker, + addrSpace: ephemeral.NewAddressSpace(), + events: events, } m.Internal = internal.Internal{ @@ -85,18 +99,27 @@ func NewManager(session *storage.Session, switchboard *switchboard.Switchboard, Instance: instance, TransmissionID: session.User().GetCryptographicIdentity().GetTransmissionID(), ReceptionID: session.User().GetCryptographicIdentity().GetReceptionID(), + Events: events, } // Set up gateway.Sender poolParams := gateway.DefaultPoolParams() + // Client will not send KeepAlive packets + poolParams.HostParams.KaClientOpts.Time = time.Duration(math.MaxInt64) m.sender, err = gateway.NewSender(poolParams, rng, ndf, comms, session, m.NodeRegistration) if err != nil { return nil, err } + // Report health events + m.Internal.Health.AddFunc(func(isHealthy bool) { + m.Internal.Events.Report(5, "Health", "IsHealthy", + fmt.Sprintf("%v", isHealthy)) + }) + //create sub managers - m.message = message.NewManager(m.Internal, m.param.Messages, m.NodeRegistration, m.sender) + m.message = message.NewManager(m.Internal, m.param, m.NodeRegistration, m.sender) m.round = rounds.NewManager(m.Internal, m.param.Rounds, m.message.GetMessageReceptionChannel(), m.sender) return &m, nil @@ -130,7 +153,7 @@ func (m *manager) Follow(report interfaces.ClientErrorReport) (stoppable.Stoppab // Start the Network Tracker trackNetworkStopper := stoppable.NewSingle("TrackNetwork") - go m.followNetwork(report, trackNetworkStopper.Quit(), trackNetworkStopper) + go m.followNetwork(report, trackNetworkStopper) multi.Add(trackNetworkStopper) // Message reception @@ -139,11 +162,16 @@ func (m *manager) Follow(report interfaces.ClientErrorReport) (stoppable.Stoppab // Round processing multi.Add(m.round.StartProcessors()) - multi.Add(ephemeral.Track(m.Session, m.ReceptionID)) + multi.Add(ephemeral.Track(m.Session, m.addrSpace, m.ReceptionID)) return multi, nil } +// GetEventManager returns the health tracker +func (m *manager) GetEventManager() interfaces.EventManager { + return m.events +} + // GetHealthTracker returns the health tracker func (m *manager) GetHealthTracker() interfaces.HealthTracker { return m.Health @@ -171,3 +199,27 @@ func (m *manager) CheckGarbledMessages() { func (m *manager) InProgressRegistrations() int { return len(m.Internal.NodeRegistration) } + +// GetAddressSize returns the current address space size. It blocks until an +// address space size is set. +func (m *manager) GetAddressSize() uint8 { + return m.addrSpace.Get() +} + +// RegisterAddressSizeNotification returns a channel that will trigger for every +// address space size update. The provided tag is the unique ID for the channel. +// Returns an error if the tag is already used. +func (m *manager) RegisterAddressSizeNotification(tag string) (chan uint8, error) { + return m.addrSpace.RegisterNotification(tag) +} + +// UnregisterAddressSizeNotification stops broadcasting address space size +// updates on the channel with the specified tag. +func (m *manager) UnregisterAddressSizeNotification(tag string) { + m.addrSpace.UnregisterNotification(tag) +} + +// SetPoolFilter sets the filter used to filter gateway IDs. +func (m *manager) SetPoolFilter(f gateway.Filter) { + m.sender.SetFilter(f) +} diff --git a/network/message/bundle.go b/network/message/bundle.go index 56f1618d643641da6e9c2550e998a37a1269344c..81c649bd3798d693cd451dd7322d9d83e95510d9 100644 --- a/network/message/bundle.go +++ b/network/message/bundle.go @@ -9,13 +9,15 @@ package message import ( "gitlab.com/elixxir/client/storage/reception" + pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" ) type Bundle struct { - Round id.Round - Messages []format.Message - Finish func() - Identity reception.IdentityUse + Round id.Round + RoundInfo *pb.RoundInfo + Messages []format.Message + Finish func() + Identity reception.IdentityUse } diff --git a/network/message/critical.go b/network/message/critical.go index 1dad92c821e756e3c89c6d013c5d78c9e09a1015..9c5b6bff7c824a925c969459ddb683e38a1faa1d 100644 --- a/network/message/critical.go +++ b/network/message/critical.go @@ -12,6 +12,7 @@ import ( "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/interfaces/utility" + "gitlab.com/elixxir/client/stoppable" ds "gitlab.com/elixxir/comms/network/dataStructures" "gitlab.com/elixxir/primitives/format" "gitlab.com/elixxir/primitives/states" @@ -27,22 +28,22 @@ import ( // Tracker (/network/Health/Tracker.g0) //Thread loop for processing critical messages -func (m *Manager) processCriticalMessages(quitCh <-chan struct{}) { - done := false - for !done { +func (m *Manager) processCriticalMessages(stop *stoppable.Single) { + for { select { - case <-quitCh: - done = true + case <-stop.Quit(): + stop.ToStopped() + return case isHealthy := <-m.networkIsHealthy: if isHealthy { - m.criticalMessages() + m.criticalMessages(stop) } } } } // processes all critical messages -func (m *Manager) criticalMessages() { +func (m *Manager) criticalMessages(stop *stoppable.Single) { critMsgs := m.Session.GetCriticalMessages() // try to send every message in the critical messages and the raw critical // messages buffer in parallel @@ -53,7 +54,7 @@ func (m *Manager) criticalMessages() { jww.INFO.Printf("Resending critical message to %s ", msg.Recipient) //send the message - rounds, _, err := m.SendE2E(msg, param) + rounds, _, _, err := m.SendE2E(msg, param, stop) //if the message fail to send, notify the buffer so it can be handled //in the future and exit if err != nil { @@ -80,7 +81,7 @@ func (m *Manager) criticalMessages() { return } - jww.INFO.Printf("Sucesfull resend of critical message "+ + jww.INFO.Printf("Successful resend of critical message "+ "to %s on rounds %d", msg.Recipient, rounds) critMsgs.Succeeded(msg) }(msg, param) @@ -95,7 +96,7 @@ func (m *Manager) criticalMessages() { jww.INFO.Printf("Resending critical raw message to %s "+ "(msgDigest: %s)", rid, msg.Digest()) //send the message - round, _, err := m.SendCMIX(m.sender, msg, rid, param) + round, _, err := m.SendCMIX(m.sender, msg, rid, param, stop) //if the message fail to send, notify the buffer so it can be handled //in the future and exit if err != nil { @@ -128,7 +129,7 @@ func (m *Manager) criticalMessages() { return } - jww.INFO.Printf("Sucesfull resend of critical raw message "+ + jww.INFO.Printf("Successful resend of critical raw message "+ "to %s (msgDigest: %s) on round %d", rid, msg.Digest(), round) critRawMsgs.Succeeded(msg, rid) diff --git a/network/message/garbled.go b/network/message/garbled.go index d9e1ac6be427f2ca0e9c8e89572fb6eed2285483..e8fb10cbe49845f7370639877c4dcdba45342a39 100644 --- a/network/message/garbled.go +++ b/network/message/garbled.go @@ -10,8 +10,9 @@ package message import ( jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/primitives/format" - "time" + "gitlab.com/xx_network/primitives/netTime" ) // Messages can arrive in the network out of order. When message handling fails @@ -33,12 +34,12 @@ func (m *Manager) CheckGarbledMessages() { } //long running thread which processes garbled messages -func (m *Manager) processGarbledMessages(quitCh <-chan struct{}) { - done := false - for !done { +func (m *Manager) processGarbledMessages(stop *stoppable.Single) { + for { select { - case <-quitCh: - done = true + case <-stop.Quit(): + stop.ToStopped() + return case <-m.triggerGarbled: m.handleGarbledMessages() } @@ -80,7 +81,7 @@ func (m *Manager) handleGarbledMessages() { // unless it is the last attempts and has been in the buffer long // enough, in which case remove it if count == m.param.MaxChecksGarbledMessage && - time.Since(timestamp) > m.param.GarbledMessageWait { + netTime.Since(timestamp) > m.param.GarbledMessageWait { garbledMsgs.Remove(grbldMsg) } else { failedMsgs = append(failedMsgs, grbldMsg) diff --git a/network/message/garbled_test.go b/network/message/garbled_test.go index 9d021ae488ed26717aade2d76d87ba688c7f9001..b651c02e041f68467799d02d7892e4916f032042 100644 --- a/network/message/garbled_test.go +++ b/network/message/garbled_test.go @@ -7,6 +7,7 @@ import ( "gitlab.com/elixxir/client/network/gateway" "gitlab.com/elixxir/client/network/internal" "gitlab.com/elixxir/client/network/message/parse" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/client/switchboard" "gitlab.com/elixxir/comms/client" @@ -64,12 +65,12 @@ func TestManager_CheckGarbledMessages(t *testing.T) { if err != nil { t.Errorf(err.Error()) } - m := NewManager(i, params.Messages{ + m := NewManager(i, params.Network{Messages: params.Messages{ MessageReceptionBuffLen: 20, MessageReceptionWorkerPoolSize: 20, MaxChecksGarbledMessage: 20, GarbledMessageWait: time.Hour, - }, nil, sender) + }}, nil, sender) e2ekv := i.Session.E2e() err = e2ekv.AddPartner(sess2.GetUser().TransmissionID, sess2.E2e().GetDHPublicKey(), e2ekv.GetDHPrivateKey(), @@ -105,6 +106,7 @@ func TestManager_CheckGarbledMessages(t *testing.T) { contents := make([]byte, msg.ContentsSize()) prng := rand.New(rand.NewSource(42)) prng.Read(contents) + contents[len(contents)-1] = 0 fmp := parse.FirstMessagePartFromBytes(contents) binary.BigEndian.PutUint32(fmp.Type, uint32(message.Raw)) fmp.NumParts[0] = uint8(1) @@ -119,8 +121,8 @@ func TestManager_CheckGarbledMessages(t *testing.T) { encryptedMsg := key.Encrypt(msg) i.Session.GetGarbledMessages().Add(encryptedMsg) - quitch := make(chan struct{}) - go m.processGarbledMessages(quitch) + stop := stoppable.NewSingle("stop") + go m.processGarbledMessages(stop) m.CheckGarbledMessages() diff --git a/network/message/handler.go b/network/message/handler.go index b8892cff56f51009558998fcd74b1fa027c4a712..08ced621adaddc0b74772e82dfd02862ece95b5b 100644 --- a/network/message/handler.go +++ b/network/message/handler.go @@ -8,25 +8,27 @@ package message import ( + "fmt" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces/message" - "gitlab.com/elixxir/client/storage/reception" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/crypto/e2e" fingerprint2 "gitlab.com/elixxir/crypto/fingerprint" "gitlab.com/elixxir/primitives/format" + "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/primitives/id" "time" ) -func (m *Manager) handleMessages(quitCh <-chan struct{}) { - done := false - for !done { +func (m *Manager) handleMessages(stop *stoppable.Single) { + for { select { - case <-quitCh: - done = true + case <-stop.Quit(): + stop.ToStopped() + return case bundle := <-m.messageReception: for _, msg := range bundle.Messages { - m.handleMessage(msg, bundle.Identity) + m.handleMessage(msg, bundle) } bundle.Finish() } @@ -34,10 +36,11 @@ func (m *Manager) handleMessages(quitCh <-chan struct{}) { } -func (m *Manager) handleMessage(ecrMsg format.Message, identity reception.IdentityUse) { +func (m *Manager) handleMessage(ecrMsg format.Message, bundle Bundle) { // We've done all the networking, now process the message fingerprint := ecrMsg.GetKeyFP() msgDigest := ecrMsg.Digest() + identity := bundle.Identity e2eKv := m.Session.E2e() @@ -74,8 +77,12 @@ func (m *Manager) handleMessage(ecrMsg format.Message, identity reception.Identi //drop the message is decryption failed if err != nil { //if decryption failed, print an error - jww.WARN.Printf("Failed to decrypt message with fp %s "+ - "from partner %s: %s", key.Fingerprint(), sender, err) + msg := fmt.Sprintf("Failed to decrypt message with "+ + "fp %s from partner %s: %s", key.Fingerprint(), + sender, err) + jww.WARN.Printf(msg) + m.Internal.Events.Report(9, "MessageReception", + "DecryptionError", msg) return } //set the type as E2E encrypted @@ -90,28 +97,30 @@ func (m *Manager) handleMessage(ecrMsg format.Message, identity reception.Identi // if it doesnt match any form of encrypted, hear it as a raw message // and add it to garbled messages to be handled later msg = ecrMsg - if err != nil { - jww.DEBUG.Printf("Failed to unmarshal ephemeral ID "+ - "on unknown message: %+v", err) - } raw := message.Receive{ - Payload: msg.Marshal(), - MessageType: message.Raw, - Sender: &id.ID{}, - EphemeralID: identity.EphId, - Timestamp: time.Time{}, - Encryption: message.None, - RecipientID: identity.Source, + Payload: msg.Marshal(), + MessageType: message.Raw, + Sender: &id.ID{}, + EphemeralID: identity.EphId, + Timestamp: time.Time{}, + Encryption: message.None, + RecipientID: identity.Source, + RoundId: id.Round(bundle.RoundInfo.ID), + RoundTimestamp: time.Unix(0, int64(bundle.RoundInfo.Timestamps[states.QUEUED])), } - jww.INFO.Printf("Garbled/RAW Message: keyFP: %v, msgDigest: %s", - msg.GetKeyFP(), msg.Digest()) + im := fmt.Sprintf("Garbled/RAW Message: keyFP: %v, "+ + "msgDigest: %s", msg.GetKeyFP(), msg.Digest()) + jww.INFO.Print(im) + m.Internal.Events.Report(1, "MessageReception", "Garbled", im) m.Session.GetGarbledMessages().Add(msg) m.Switchboard.Speak(raw) return } - jww.INFO.Printf("Received message of type %s from %s,"+ + im := fmt.Sprintf("Received message of type %s from %s,"+ " msgDigest: %s", encTy, sender, msgDigest) + jww.INFO.Print(im) + m.Internal.Events.Report(2, "MessageReception", "MessagePart", im) // Process the decrypted/unencrypted message partition, to see if // we get a full message @@ -124,10 +133,15 @@ func (m *Manager) handleMessage(ecrMsg format.Message, identity reception.Identi xxMsg.RecipientID = identity.Source xxMsg.EphemeralID = identity.EphId xxMsg.Encryption = encTy + xxMsg.RoundId = id.Round(bundle.RoundInfo.ID) + xxMsg.RoundTimestamp = time.Unix(0, int64(bundle.RoundInfo.Timestamps[states.QUEUED])) if xxMsg.MessageType == message.Raw { - jww.WARN.Panicf("Recieved a message of type 'Raw' from %s."+ + rm := fmt.Sprintf("Recieved a message of type 'Raw' from %s."+ "Message Ignored, 'Raw' is a reserved type. Message supressed.", xxMsg.ID) + jww.WARN.Print(rm) + m.Internal.Events.Report(10, "MessageReception", + "Error", rm) } else { m.Switchboard.Speak(xxMsg) } diff --git a/network/message/manager.go b/network/message/manager.go index 7728910aa7e62186c682dcb97b297cb470dcac58..0ac5eeceb0f892ef6ef7d4b0e0f578904e1901a3 100644 --- a/network/message/manager.go +++ b/network/message/manager.go @@ -8,7 +8,9 @@ package message import ( + "encoding/base64" "fmt" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/network/gateway" "gitlab.com/elixxir/client/network/internal" @@ -19,10 +21,11 @@ import ( ) type Manager struct { - param params.Messages + param params.Network partitioner parse.Partitioner internal.Internal - sender *gateway.Sender + sender *gateway.Sender + blacklistedNodes map[string]interface{} messageReception chan Bundle nodeRegistration chan network.NodeGateway @@ -30,7 +33,7 @@ type Manager struct { triggerGarbled chan struct{} } -func NewManager(internal internal.Internal, param params.Messages, +func NewManager(internal internal.Internal, param params.Network, nodeRegistration chan network.NodeGateway, sender *gateway.Sender) *Manager { dummyMessage := format.NewMessage(internal.Session.Cmix().GetGroup().GetP().ByteLen()) m := Manager{ @@ -41,8 +44,16 @@ func NewManager(internal internal.Internal, param params.Messages, triggerGarbled: make(chan struct{}, 100), nodeRegistration: nodeRegistration, sender: sender, + Internal: internal, + } + for _, nodeId := range param.BlacklistedNodes { + decodedId, err := base64.StdEncoding.DecodeString(nodeId) + if err != nil { + jww.ERROR.Printf("Unable to decode blacklisted Node ID %s: %+v", decodedId, err) + continue + } + m.blacklistedNodes[string(decodedId)] = nil } - m.Internal = internal return &m } @@ -58,19 +69,19 @@ func (m *Manager) StartProcessies() stoppable.Stoppable { //create the message handler workers for i := uint(0); i < m.param.MessageReceptionWorkerPoolSize; i++ { stop := stoppable.NewSingle(fmt.Sprintf("MessageReception Worker %v", i)) - go m.handleMessages(stop.Quit()) + go m.handleMessages(stop) multi.Add(stop) } //create the critical messages thread critStop := stoppable.NewSingle("CriticalMessages") - go m.processCriticalMessages(critStop.Quit()) + go m.processCriticalMessages(critStop) m.Health.AddChannel(m.networkIsHealthy) multi.Add(critStop) //create the garbled messages thread garbledStop := stoppable.NewSingle("GarbledMessages") - go m.processGarbledMessages(garbledStop.Quit()) + go m.processGarbledMessages(garbledStop) multi.Add(garbledStop) return multi diff --git a/network/message/parse/firstMessagePart.go b/network/message/parse/firstMessagePart.go index 958a386992f161f007921476f52eb9749ea628bf..fa67b64411e0f2272178452eeb0a8deb91612271 100644 --- a/network/message/parse/firstMessagePart.go +++ b/network/message/parse/firstMessagePart.go @@ -9,92 +9,123 @@ package parse import ( "encoding/binary" - jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces/message" "time" ) -const numPartsLen = 1 -const typeLen = message.TypeLen -const timestampLen = 15 -const firstHeaderLen = headerLen + numPartsLen + typeLen + timestampLen +// Sizes of message parts, in bytes. +const ( + numPartsLen = 1 + typeLen = message.TypeLen + timestampLen = 8 + firstPartVerLen = 1 + firstHeaderLen = headerLen + numPartsLen + typeLen + timestampLen + firstPartVerLen +) + +// The current version of the firstMessagePart message format. +const firstMessagePartCurrentVersion = 0 type firstMessagePart struct { messagePart NumParts []byte Type []byte Timestamp []byte + Version []byte // Version of the message format; always the last bit } -//creates a new first message part for the passed in contents. Does no length checks +// newFirstMessagePart creates a new firstMessagePart for the passed in +// contents. Does no length checks. func newFirstMessagePart(mt message.Type, id uint32, numParts uint8, timestamp time.Time, contents []byte) firstMessagePart { - //create the message structure - data := make([]byte, len(contents)+firstHeaderLen) - m := FirstMessagePartFromBytes(data) - //Put the message type in the message + // Create the message structure + m := FirstMessagePartFromBytes(make([]byte, len(contents)+firstHeaderLen)) + + // Set the message type binary.BigEndian.PutUint32(m.Type, uint32(mt)) - //Add the message ID + // Set the message ID binary.BigEndian.PutUint32(m.Id, id) - // Add the part number to the message, its always zero because this is the - // first part. Because the default is zero this step could be skipped, but\ - // keep it in the code for clarity + // Set the part number. It is always zero because this is the first part. + // Because the default is zero this step could be skipped, but keep it in + // the code for clarity. m.Part[0] = 0 - // Add the number of parts to the message + // Set the number of parts to the message m.NumParts[0] = numParts - //Serialize and add the timestamp to the payload - timestampBytes, err := timestamp.MarshalBinary() - if err != nil { - jww.FATAL.Panicf("Failed to create firstMessagePart: %s", err.Error()) - } - copy(m.Timestamp, timestampBytes) + // Set the timestamp as unix nano + binary.BigEndian.PutUint64(m.Timestamp, uint64(timestamp.UnixNano())) - //set the contents length + // Set the length of the contents binary.BigEndian.PutUint16(m.Len, uint16(len(contents))) - //add the contents to the payload + // Set the contents copy(m.Contents[:len(contents)], contents) + // Set the version number + m.Version[0] = firstMessagePartCurrentVersion + return m } -// Builds a first message part mapped to the passed in data slice. Mapped by -// reference, a copy is not made. +// Map of firstMessagePart encoding version numbers to their map functions. +var firstMessagePartFromBytesVersions = map[uint8]func([]byte) firstMessagePart{ + firstMessagePartCurrentVersion: firstMessagePartFromBytesVer0, +} + +// FirstMessagePartFromBytes builds a firstMessagePart mapped to the passed in +// data slice. Mapped by reference; a copy is not made. func FirstMessagePartFromBytes(data []byte) firstMessagePart { - m := firstMessagePart{ + + // Map the data according to its version + version := data[len(data)-1] + mapFunc, exists := firstMessagePartFromBytesVersions[version] + if exists { + return mapFunc(data) + } + + return firstMessagePart{} +} + +func firstMessagePartFromBytesVer0(data []byte) firstMessagePart { + return firstMessagePart{ messagePart: messagePart{ Data: data, Id: data[:idLen], Part: data[idLen : idLen+partLen], Len: data[idLen+partLen : idLen+partLen+lenLen], - Contents: data[idLen+partLen+numPartsLen+typeLen+timestampLen+lenLen:], + Contents: data[idLen+partLen+lenLen+numPartsLen+typeLen+timestampLen : len(data)-firstPartVerLen-1], }, - NumParts: data[idLen+partLen+lenLen : idLen+partLen+numPartsLen+lenLen], - Type: data[idLen+partLen+numPartsLen+lenLen : idLen+partLen+numPartsLen+typeLen+lenLen], - Timestamp: data[idLen+partLen+numPartsLen+typeLen+lenLen : idLen+partLen+numPartsLen+typeLen+timestampLen+lenLen], + NumParts: data[idLen+partLen+lenLen : idLen+partLen+lenLen+numPartsLen], + Type: data[idLen+partLen+lenLen+numPartsLen : idLen+partLen+lenLen+numPartsLen+typeLen], + Timestamp: data[idLen+partLen+lenLen+numPartsLen+typeLen : idLen+partLen+lenLen+numPartsLen+typeLen+timestampLen], + Version: data[len(data)-firstPartVerLen:], } - return m } +// GetType returns the message type. func (m firstMessagePart) GetType() message.Type { return message.Type(binary.BigEndian.Uint32(m.Type)) } +// GetNumParts returns the number of message parts. func (m firstMessagePart) GetNumParts() uint8 { return m.NumParts[0] } -func (m firstMessagePart) GetTimestamp() (time.Time, error) { - var t time.Time - err := t.UnmarshalBinary(m.Timestamp) - return t, err +// GetTimestamp returns the timestamp as a time.Time. +func (m firstMessagePart) GetTimestamp() time.Time { + return time.Unix(0, int64(binary.BigEndian.Uint64(m.Timestamp))) +} + +// GetVersion returns the version number of the data encoding. +func (m firstMessagePart) GetVersion() uint8 { + return m.Version[0] } +// Bytes returns the serialised message data. func (m firstMessagePart) Bytes() []byte { return m.Data } diff --git a/network/message/parse/firstMessagePart_test.go b/network/message/parse/firstMessagePart_test.go index 676723247fec562607d43f9a29dec827890c76a8..2411dd4a6896453f08d2fd02ae2dd15946f0f76c 100644 --- a/network/message/parse/firstMessagePart_test.go +++ b/network/message/parse/firstMessagePart_test.go @@ -18,16 +18,19 @@ import ( // Expected firstMessagePart for checking against, generated by fmp in TestNewFirstMessagePart var efmp = firstMessagePart{ messagePart: messagePart{ - Data: []byte{0, 0, 4, 53, 0, 0, 13, 2, 0, 0, 0, 2, 1, 0, 0, 0, 14, 215, 133, 90, 117, 0, 0, 0, 0, 255, 255, - 116, 101, 115, 116, 105, 110, 103, 115, 116, 114, 105, 110, 103}, - Id: []byte{0, 0, 4, 53}, - Part: []byte{0}, - Len: []byte{0, 13}, - Contents: []byte{116, 101, 115, 116, 105, 110, 103, 115, 116, 114, 105, 110, 103}, + Data: []byte{0, 0, 4, 53, 0, 0, 13, 2, 0, 0, 0, 2, 22, 87, 28, 11, 215, + 220, 82, 0, 116, 101, 115, 116, 105, 110, 103, 115, 116, 114, 105, + 110, 103, 0, firstMessagePartCurrentVersion}, + Id: []byte{0, 0, 4, 53}, + Part: []byte{0}, + Len: []byte{0, 13}, + Contents: []byte{116, 101, 115, 116, 105, 110, 103, 115, 116, 114, 105, + 110, 103}, }, NumParts: []byte{2}, Type: []byte{0, 0, 0, 2}, - Timestamp: []byte{1, 0, 0, 0, 14, 215, 133, 90, 117, 0, 0, 0, 0, 255, 255}, + Timestamp: []byte{22, 87, 28, 11, 215, 220, 82, 0}, + Version: []byte{firstMessagePartCurrentVersion}, } // Test that newFirstMessagePart returns a correctly made firstMessagePart @@ -37,24 +40,19 @@ func TestNewFirstMessagePart(t *testing.T) { 1077, 2, time.Unix(1609786229, 0).UTC(), - []byte{'t', 'e', 's', 't', 'i', 'n', 'g', - 's', 't', 'r', 'i', 'n', 'g'}, + []byte{'t', 'e', 's', 't', 'i', 'n', 'g', 's', 't', 'r', 'i', 'n', 'g'}, ) - gotTime, err := fmp.GetTimestamp() - if err != nil { - t.Error(err) - } - expectedTime, err := fmp.GetTimestamp() - if err != nil { - t.Error(err) - } + gotTime := fmp.GetTimestamp() + expectedTime := time.Unix(1609786229, 0).UTC() if !gotTime.Equal(expectedTime) { - t.Errorf("Got time: %v, expected time: %v", gotTime, expectedTime) + t.Errorf("Failed to get expected timestamp."+ + "\nexpected: %s\nreceived: %s", expectedTime, gotTime) } if !reflect.DeepEqual(fmp, efmp) { - t.Errorf("Expected and got firstMessagePart did not match.\n\tGot: %#v\n\tExpected: %#v", fmp, efmp) + t.Errorf("Expected and got firstMessagePart did not match."+ + "\nexpected: %+v\nrecieved: %+v", efmp, fmp) } } @@ -83,10 +81,7 @@ func TestFirstMessagePart_GetNumParts(t *testing.T) { // Test that GetTimestamp returns the correct timestamp for a firstMessagePart func TestFirstMessagePart_GetTimestamp(t *testing.T) { - et, err := efmp.GetTimestamp() - if err != nil { - t.Error(err) - } + et := efmp.GetTimestamp() if !time.Unix(1609786229, 0).Equal(et) { t.Errorf("Got %v, expected %v", et, time.Unix(1609786229, 0)) } diff --git a/network/message/parse/messagePart.go b/network/message/parse/messagePart.go index 815544be80370e2c4f66482c903ae0cbe9e9ad92..01c514d7c33fe1654534a67aeaf8671744d93982 100644 --- a/network/message/parse/messagePart.go +++ b/network/message/parse/messagePart.go @@ -11,10 +11,17 @@ import ( "encoding/binary" ) -const idLen = 4 -const partLen = 1 -const lenLen = 2 -const headerLen = idLen + partLen + lenLen +// Sizes of message parts, in bytes. +const ( + idLen = 4 + partLen = 1 + lenLen = 2 + partVerLen = 1 + headerLen = idLen + partLen + lenLen + partVerLen +) + +// The current version of the messagePart message format. +const messagePartCurrentVersion = 0 type messagePart struct { Data []byte @@ -22,62 +29,90 @@ type messagePart struct { Part []byte Len []byte Contents []byte + Version []byte // Version of the message format; always the last bit } -//creates a new message part for the passed in contents. Does no length checks +// newMessagePart creates a new messagePart for the passed in contents. Does no +// length checks. func newMessagePart(id uint32, part uint8, contents []byte) messagePart { - //create the message structure + // Create the message structure data := make([]byte, len(contents)+headerLen) - m := MessagePartFromBytes(data) + m := messagePartFromBytes(data) - //add the message ID to the message + // Set the message ID binary.BigEndian.PutUint32(m.Id, id) - //set the message part number + // Set the message part number m.Part[0] = part - //set the contents length + // Set the contents length binary.BigEndian.PutUint16(m.Len, uint16(len(contents))) - //copy the contents into the message + // Copy the contents into the message copy(m.Contents[:len(contents)], contents) + + // Set the version number + m.Version[0] = messagePartCurrentVersion + return m } -// Builds a Message part mapped to the passed in data slice. Mapped by -// reference, a copy is not made. -func MessagePartFromBytes(data []byte) messagePart { - m := messagePart{ +// Map of messagePart encoding version numbers to their map functions. +var messagePartFromBytesVersions = map[uint8]func([]byte) messagePart{ + messagePartCurrentVersion: messagePartFromBytesVer0, +} + +// messagePartFromBytes builds a messagePart mapped to the passed in data slice. +// Mapped by reference; a copy is not made. +func messagePartFromBytes(data []byte) messagePart { + + // Map the data according to its version + version := data[len(data)-1] + mapFunc, exists := messagePartFromBytesVersions[version] + if exists { + return mapFunc(data) + } + + return messagePart{} +} + +func messagePartFromBytesVer0(data []byte) messagePart { + return messagePart{ Data: data, Id: data[:idLen], Part: data[idLen : idLen+partLen], Len: data[idLen+partLen : idLen+partLen+lenLen], - Contents: data[idLen+partLen+lenLen:], + Contents: data[idLen+partLen+lenLen : len(data)-partVerLen], + Version: data[len(data)-partVerLen:], } - return m } +// GetID returns the message ID. func (m messagePart) GetID() uint32 { return binary.BigEndian.Uint32(m.Id) } +// GetPart returns the message part number. func (m messagePart) GetPart() uint8 { return m.Part[0] } +// GetContents returns the entire contents slice. func (m messagePart) GetContents() []byte { return m.Contents } +// GetSizedContents returns the contents truncated to include only stored data. func (m messagePart) GetSizedContents() []byte { - size := m.GetContentsLength() - return m.Contents[:size] + return m.Contents[:m.GetContentsLength()] } +// GetContentsLength returns the length of the data in the contents. func (m messagePart) GetContentsLength() int { return int(binary.BigEndian.Uint16(m.Len)) } +// Bytes returns the serialised message data. func (m messagePart) Bytes() []byte { return m.Data } diff --git a/network/message/parse/messagePart_test.go b/network/message/parse/messagePart_test.go index 5ab1db0213c05a18f7615a836e627350a5879616..61ecc7467ff5c95dd2cf89830cacbb319cf9660f 100644 --- a/network/message/parse/messagePart_test.go +++ b/network/message/parse/messagePart_test.go @@ -15,17 +15,19 @@ import ( // Expected messagePart for checking against, generated by gotmp in Test_newMessagePart var emp = messagePart{ - Data: []uint8{0x0, 0x0, 0x0, 0x20, 0x6, 0x0, 0x7, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67}, + Data: []uint8{0x0, 0x0, 0x0, 0x20, 0x6, 0x0, 0x7, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, messagePartCurrentVersion}, Id: []uint8{0x0, 0x0, 0x0, 0x20}, Part: []uint8{0x6}, Len: []uint8{0x0, 0x7}, Contents: []uint8{0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67}, + Version: []uint8{messagePartCurrentVersion}, } // This tests that a new function part is successfully created func Test_newMessagePart(t *testing.T) { gotmp := newMessagePart(32, 6, []byte{'t', 'e', 's', 't', 'i', 'n', 'g'}) if !reflect.DeepEqual(gotmp, emp) { - t.Errorf("MessagePart received and MessagePart expected do not match.\n\tGot: %#v\n\tExpected: %#v", gotmp, emp) + t.Errorf("MessagePart received and MessagePart expected do not match."+ + "\nexpected: %#v\nreceived: %#v", emp, gotmp) } } diff --git a/network/message/parse/partition.go b/network/message/parse/partition.go index 0b690a5bef804dd269cd62cbcc0ecac6d2667439..ad66ba2f30e09434dc090f07e0917137973d9b00 100644 --- a/network/message/parse/partition.go +++ b/network/message/parse/partition.go @@ -9,10 +9,10 @@ package parse import ( "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/storage" "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" "time" ) @@ -44,23 +44,23 @@ func (p Partitioner) Partition(recipient *id.ID, mt message.Type, if len(payload) > p.maxSize { return nil, 0, errors.Errorf("Payload is too long, max payload "+ - "length is %v, received %v", p.maxSize, len(payload)) + "length is %d, received %d", p.maxSize, len(payload)) } - //Get the ID of the sent message + // Get the ID of the sent message fullMessageID, messageID := p.session.Conversations().Get(recipient).GetNextSendID() - // get the number of parts of the message. This equates to just a linear + // Get the number of parts of the message; this equates to just a linear // equation numParts := uint8((len(payload) + p.deltaFirstPart + p.partContentsSize - 1) / p.partContentsSize) parts := make([][]byte, numParts) - //Create the first message part + // Create the first message part var sub []byte sub, payload = splitPayload(payload, p.firstContentsSize) parts[0] = newFirstMessagePart(mt, messageID, numParts, timestamp, sub).Bytes() - //create all subsiquent message parts + // Create all subsequent message parts for i := uint8(1); i < numParts; i++ { sub, payload = splitPayload(payload, p.partContentsSize) parts[i] = newMessagePart(messageID, i, sub).Bytes() @@ -69,31 +69,25 @@ func (p Partitioner) Partition(recipient *id.ID, mt message.Type, return parts, fullMessageID, nil } -func (p Partitioner) HandlePartition(sender *id.ID, e message.EncryptionType, +func (p Partitioner) HandlePartition(sender *id.ID, _ message.EncryptionType, contents []byte, relationshipFingerprint []byte) (message.Receive, bool) { - //If it is the first message in a set, handle it as so if isFirst(contents) { - //decode the message structure + // If it is the first message in a set, then handle it as so + + // Decode the message structure fm := FirstMessagePartFromBytes(contents) - timestamp, err := fm.GetTimestamp() - if err != nil { - jww.FATAL.Panicf("Failed Handle Partition, failed to get "+ - "timestamp message from %s messageID %v: %s", sender, - fm.Timestamp, err) - } - - //Handle the message ID + + // Handle the message ID messageID := p.session.Conversations().Get(sender). ProcessReceivedMessageID(fm.GetID()) - - //Return the + storeageTimestamp := netTime.Now() return p.session.Partition().AddFirst(sender, fm.GetType(), - messageID, fm.GetPart(), fm.GetNumParts(), timestamp, + messageID, fm.GetPart(), fm.GetNumParts(), fm.GetTimestamp(), storeageTimestamp, fm.GetSizedContents(), relationshipFingerprint) - //If it is a subsiquent message part, handle it as so } else { - mp := MessagePartFromBytes(contents) + // If it is a subsequent message part, handle it as so + mp := messagePartFromBytes(contents) messageID := p.session.Conversations().Get(sender). ProcessReceivedMessageID(mp.GetID()) diff --git a/network/message/parse/partition_test.go b/network/message/parse/partition_test.go index 933db1a01d4e95605d44d7cf5f5dce4429f2a9b5..9351a1e77b7e80dfc74938fefe01203dd4f0b705 100644 --- a/network/message/parse/partition_test.go +++ b/network/message/parse/partition_test.go @@ -32,28 +32,28 @@ func TestNewPartitioner(t *testing.T) { 4096, p.baseMessageSize) } - if p.deltaFirstPart != 20 { + if p.deltaFirstPart != firstHeaderLen-headerLen { t.Errorf("deltaFirstPart content mismatch"+ "\n\texpected: %v\n\treceived: %v", - 20, p.deltaFirstPart) + firstHeaderLen-headerLen, p.deltaFirstPart) } - if p.firstContentsSize != 4069 { + if p.firstContentsSize != 4096-firstHeaderLen { t.Errorf("firstContentsSize content mismatch"+ "\n\texpected: %v\n\treceived: %v", - 4069, p.firstContentsSize) + 4096-firstHeaderLen, p.firstContentsSize) } - if p.maxSize != 1042675 { + if p.maxSize != (4096-firstHeaderLen)+(MaxMessageParts-1)*(4096-headerLen) { t.Errorf("maxSize content mismatch"+ "\n\texpected: %v\n\treceived: %v", - 1042675, p.maxSize) + (4096-firstHeaderLen)+(MaxMessageParts-1)*(4096-headerLen), p.maxSize) } - if p.partContentsSize != 4089 { + if p.partContentsSize != 4088 { t.Errorf("partContentsSize content mismatch"+ "\n\texpected: %v\n\treceived: %v", - 4089, p.partContentsSize) + 4088, p.partContentsSize) } if p.session != storeSession { diff --git a/network/message/sendCmix.go b/network/message/sendCmix.go index 2a7095843eb57c6f9af2af16ea9725fab0aac627..2028e435692425f6df01d2bdcbc9d03087c37250 100644 --- a/network/message/sendCmix.go +++ b/network/message/sendCmix.go @@ -8,42 +8,40 @@ package message import ( + "fmt" "github.com/golang-collections/collections/set" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/interfaces" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/network/gateway" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/comms/network" "gitlab.com/elixxir/crypto/fastRNG" - "gitlab.com/elixxir/crypto/fingerprint" "gitlab.com/elixxir/primitives/format" - "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" "gitlab.com/xx_network/primitives/netTime" "strings" - "time" ) -// interface for SendCMIX comms; allows mocking this in testing -type sendCmixCommsInterface interface { - SendPutMessage(host *connect.Host, message *pb.GatewaySlot) (*pb.GatewaySlotResponse, error) -} - -// 1.5 seconds -const sendTimeBuffer = 2500 * time.Millisecond - // WARNING: Potentially Unsafe // Public manager function to send a message over CMIX -func (m *Manager) SendCMIX(sender *gateway.Sender, msg format.Message, recipient *id.ID, param params.CMIX) (id.Round, ephemeral.Id, error) { +func (m *Manager) SendCMIX(sender *gateway.Sender, msg format.Message, + recipient *id.ID, cmixParams params.CMIX, + stop *stoppable.Single) (id.Round, ephemeral.Id, error) { + msgCopy := msg.Copy() - return sendCmixHelper(sender, msgCopy, recipient, param, m.Instance, m.Session, m.nodeRegistration, m.Rng, m.TransmissionID, m.Comms) + return sendCmixHelper(sender, msgCopy, recipient, cmixParams, m.blacklistedNodes, m.Instance, + m.Session, m.nodeRegistration, m.Rng, m.Internal.Events, + m.TransmissionID, m.Comms, stop) } -// Payloads send are not End to End encrypted, MetaData is NOT protected with +// Helper function for sendCmix +// NOTE: Payloads send are not End to End encrypted, MetaData is NOT protected with // this call, see SendE2E for End to End encryption and full privacy protection // Internal SendCmix which bypasses the network check, will attempt to send to // the network without checking state. It has a built in retry system which can @@ -51,9 +49,12 @@ func (m *Manager) SendCMIX(sender *gateway.Sender, msg format.Message, recipient // If the message is successfully sent, the id of the round sent it is returned, // which can be registered with the network instance to get a callback on // its status -func sendCmixHelper(sender *gateway.Sender, msg format.Message, recipient *id.ID, param params.CMIX, instance *network.Instance, - session *storage.Session, nodeRegistration chan network.NodeGateway, rng *fastRNG.StreamGenerator, senderId *id.ID, - comms sendCmixCommsInterface) (id.Round, ephemeral.Id, error) { +func sendCmixHelper(sender *gateway.Sender, msg format.Message, + recipient *id.ID, cmixParams params.CMIX, blacklistedNodes map[string]interface{}, instance *network.Instance, + session *storage.Session, nodeRegistration chan network.NodeGateway, + rng *fastRNG.StreamGenerator, events interfaces.EventManager, + senderId *id.ID, comms sendCmixCommsInterface, + stop *stoppable.Single) (id.Round, ephemeral.Id, error) { timeStart := netTime.Now() attempted := set.New() @@ -61,13 +62,13 @@ func sendCmixHelper(sender *gateway.Sender, msg format.Message, recipient *id.ID jww.INFO.Printf("Looking for round to send cMix message to %s "+ "(msgDigest: %s)", recipient, msg.Digest()) - for numRoundTries := uint(0); numRoundTries < param.RoundTries; numRoundTries++ { - elapsed := netTime.Now().Sub(timeStart) + for numRoundTries := uint(0); numRoundTries < cmixParams.RoundTries; numRoundTries++ { + elapsed := netTime.Since(timeStart) - if elapsed > param.Timeout { + if elapsed > cmixParams.Timeout { jww.INFO.Printf("No rounds to send to %s (msgDigest: %s) "+ "were found before timeout %s", recipient, msg.Digest(), - param.Timeout) + cmixParams.Timeout) return 0, ephemeral.Id{}, errors.New("Sending cmix message timed out") } if numRoundTries > 0 { @@ -76,98 +77,50 @@ func sendCmixHelper(sender *gateway.Sender, msg format.Message, recipient *id.ID msg.Digest()) } - remainingTime := param.Timeout - elapsed + remainingTime := cmixParams.Timeout - elapsed //find the best round to send to, excluding attempted rounds - bestRound, _ := instance.GetWaitingRounds().GetUpcomingRealtime(remainingTime, attempted, sendTimeBuffer) + bestRound, err := instance.GetWaitingRounds().GetUpcomingRealtime(remainingTime, attempted, sendTimeBuffer) + if err != nil { + jww.WARN.Printf("Failed to GetUpcomingRealtime (msgDigest: %s): %+v", msg.Digest(), err) + } if bestRound == nil { continue } - //add the round on to the list of attempted so it is not tried again + //add the round on to the list of attempted, so it is not tried again attempted.Insert(bestRound) - //set the ephemeral ID - ephID, _, _, err := ephemeral.GetId(recipient, - uint(bestRound.AddressSpaceSize), - int64(bestRound.Timestamps[states.QUEUED])) - if err != nil { - jww.FATAL.Panicf("Failed to generate ephemeral ID when "+ - "sending to %s (msgDigest: %s): %+v", err, recipient, - msg.Digest()) + // Determine whether the selected round contains any Nodes + // that are blacklisted by the params.Network object + containsBlacklisted := false + for _, nodeId := range bestRound.Topology { + if _, isBlacklisted := blacklistedNodes[string(nodeId)]; isBlacklisted { + containsBlacklisted = true + break + } } - - stream := rng.GetStream() - ephIdFilled, err := ephID.Fill(uint(bestRound.AddressSpaceSize), stream) - if err != nil { - jww.FATAL.Panicf("Failed to obfuscate the ephemeralID when "+ - "sending to %s (msgDigest: %s): %+v", recipient, msg.Digest(), - err) + if containsBlacklisted { + jww.WARN.Printf("Round %d contains blacklisted node, skipping...", bestRound.ID) + continue } - stream.Close() - - msg.SetEphemeralRID(ephIdFilled[:]) - - //set the identity fingerprint - ifp := fingerprint.IdentityFP(msg.GetContents(), recipient) - msg.SetIdentityFP(ifp) - //build the topology - idList, err := id.NewIDListFromBytes(bestRound.Topology) + // Retrieve host and key information from round + firstGateway, roundKeys, err := processRound(instance, session, nodeRegistration, bestRound, recipient.String(), msg.Digest()) if err != nil { - jww.ERROR.Printf("Failed to use topology for round %d when "+ - "sending to %s (msgDigest: %s): %+v", bestRound.ID, - recipient, msg.Digest(), err) + jww.WARN.Printf("SendCmix failed to process round (will retry): %v", err) continue } - topology := connect.NewCircuit(idList) - //get they keys for the round, reject if any nodes do not have - //keying relationships - roundKeys, missingKeys := session.Cmix().GetRoundKeys(topology) - if len(missingKeys) > 0 { - jww.WARN.Printf("Failed to send on round %d to %s "+ - "(msgDigest: %s) due to missing relationships with nodes: %s", - bestRound.ID, recipient, msg.Digest(), missingKeys) - go handleMissingNodeKeys(instance, nodeRegistration, missingKeys) - time.Sleep(param.RetryDelay) - continue - } - - //get the gateway to transmit to - firstGateway := topology.GetNodeAtIndex(0).DeepCopy() - firstGateway.SetType(id.Gateway) - //encrypt the message - stream = rng.GetStream() - salt := make([]byte, 32) - _, err = stream.Read(salt) - stream.Close() + // Build the messages to send + stream := rng.GetStream() + wrappedMsg, encMsg, ephID, err := buildSlotMessage(msg, recipient, + firstGateway, stream, senderId, bestRound, roundKeys) if err != nil { - jww.ERROR.Printf("Failed to generate salt when sending to "+ - "%s (msgDigest: %s): %+v", recipient, msg.Digest(), err) - return 0, ephemeral.Id{}, errors.WithMessage(err, - "Failed to generate salt, this should never happen") + stream.Close() + return 0, ephemeral.Id{}, err } - - encMsg, kmacs := roundKeys.Encrypt(msg, salt, id.Round(bestRound.ID)) - - //build the message payload - msgPacket := &pb.Slot{ - SenderID: senderId.Bytes(), - PayloadA: encMsg.GetPayloadA(), - PayloadB: encMsg.GetPayloadB(), - Salt: salt, - KMACs: kmacs, - } - - //create the wrapper to the gateway - wrappedMsg := &pb.GatewaySlot{ - Message: msgPacket, - RoundID: bestRound.ID, - } - //Add the mac proving ownership - wrappedMsg.MAC = roundKeys.MakeClientGatewayKey(salt, - network.GenerateSlotDigest(wrappedMsg)) + stream.Close() jww.INFO.Printf("Sending to EphID %d (%s) on round %d, "+ "(msgDigest: %s, ecrMsgDigest: %s) via gateway %s", @@ -175,76 +128,57 @@ func sendCmixHelper(sender *gateway.Sender, msg format.Message, recipient *id.ID encMsg.Digest(), firstGateway.String()) // Send the payload - result, err := sender.SendToSpecific(firstGateway, func(host *connect.Host, target *id.ID) (interface{}, bool, error) { + sendFunc := func(host *connect.Host, target *id.ID) (interface{}, bool, error) { wrappedMsg.Target = target.Marshal() result, err := comms.SendPutMessage(host, wrappedMsg) if err != nil { - if strings.Contains(err.Error(), - "try a different round.") { - jww.WARN.Printf("Failed to send to %s (msgDigest: %s) "+ - "due to round error with round %d, retrying: %+v", - recipient, msg.Digest(), bestRound.ID, err) - return nil, true, err - } else if strings.Contains(err.Error(), - "Could not authenticate client. Is the client registered "+ - "with this node?") { - jww.WARN.Printf("Failed to send to %s (msgDigest: %s) "+ - "via %s due to failed authentication: %s", - recipient, msg.Digest(), firstGateway.String(), err) - //if we failed to send due to the gateway not recognizing our - // authorization, renegotiate with the node to refresh it - nodeID := firstGateway.DeepCopy() - nodeID.SetType(id.Node) - //delete the keys - session.Cmix().Remove(nodeID) - //trigger - go handleMissingNodeKeys(instance, nodeRegistration, []*id.ID{nodeID}) - return nil, true, err + // fixme: should we provide as a slice the whole topology? + warn, err := handlePutMessageError(firstGateway, instance, session, nodeRegistration, recipient.String(), bestRound, err) + if warn { + jww.WARN.Printf("SendCmix Failed: %+v", err) + } else { + return result, true, errors.WithMessagef(err, "SendCmix %s", unrecoverableError) } } return result, false, err - }) + } + result, err := sender.SendToPreferred([]*id.ID{firstGateway}, sendFunc, stop) + + // Exit if the thread has been stopped + if stoppable.CheckErr(err) { + return 0, ephemeral.Id{}, err + } //if the comm errors or the message fails to send, continue retrying. - //return if it sends properly if err != nil { - jww.ERROR.Printf("Failed to send to EphID %d (%s) on "+ - "round %d, trying a new round: %+v", ephID.Int64(), recipient, - bestRound.ID, err) - continue + if !strings.Contains(err.Error(), unrecoverableError) { + jww.ERROR.Printf("SendCmix failed to send to EphID %d (%s) on "+ + "round %d, trying a new round: %+v", ephID.Int64(), recipient, + bestRound.ID, err) + continue + } + + return 0, ephemeral.Id{}, err } + // Return if it sends properly gwSlotResp := result.(*pb.GatewaySlotResponse) if gwSlotResp.Accepted { - jww.INFO.Printf("Successfully sent to EphID %v (source: %s) "+ - "in round %d", ephID.Int64(), recipient, bestRound.ID) + m := fmt.Sprintf("Successfully sent to EphID %v "+ + "(source: %s) in round %d (msgDigest: %s), "+ + "elapsed: %s numRoundTries: %d", ephID.Int64(), + recipient, bestRound.ID, msg.Digest(), + elapsed, numRoundTries) + jww.INFO.Print(m) + events.Report(1, "MessageSend", "Metric", m) return id.Round(bestRound.ID), ephID, nil } else { jww.FATAL.Panicf("Gateway %s returned no error, but failed "+ "to accept message when sending to EphID %d (%s) on round %d", - firstGateway.String(), ephID.Int64(), recipient, bestRound.ID) + firstGateway, ephID.Int64(), recipient, bestRound.ID) } + } return 0, ephemeral.Id{}, errors.New("failed to send the message, " + "unknown error") } - -// Signals to the node registration thread to register a node if keys are -// missing. Identity is triggered automatically when the node is first seen, -// so this should on trigger on rare events. -func handleMissingNodeKeys(instance *network.Instance, - newNodeChan chan network.NodeGateway, nodes []*id.ID) { - for _, n := range nodes { - ng, err := instance.GetNodeAndGateway(n) - if err != nil { - jww.ERROR.Printf("Node contained in round cannot be found: %s", err) - continue - } - select { - case newNodeChan <- ng: - default: - jww.ERROR.Printf("Failed to send node registration for %s", n) - } - - } -} diff --git a/network/message/sendCmixUtils.go b/network/message/sendCmixUtils.go new file mode 100644 index 0000000000000000000000000000000000000000..28917e297a1d0f86e27c9dc2837567196bf1cfbd --- /dev/null +++ b/network/message/sendCmixUtils.go @@ -0,0 +1,231 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package message + +import ( + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/storage" + "gitlab.com/elixxir/client/storage/cmix" + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/comms/network" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/crypto/fingerprint" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/comms/connect" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "strconv" + "strings" + "time" +) + +// Interface for SendCMIX comms; allows mocking this in testing. +type sendCmixCommsInterface interface { + SendPutMessage(host *connect.Host, message *pb.GatewaySlot) (*pb.GatewaySlotResponse, error) + SendPutManyMessages(host *connect.Host, messages *pb.GatewaySlots) (*pb.GatewaySlotResponse, error) +} + +// how much in the future a round needs to be to send to it +const sendTimeBuffer = 1000 * time.Millisecond +const unrecoverableError = "failed with an unrecoverable error" + +// handlePutMessageError handles errors received from a PutMessage or a +// PutManyMessage network call. A printable error will be returned giving more +// context. If the error is not among recoverable errors, then the recoverable +// boolean will be returned false. If the error is among recoverable errors, +// then the boolean will return true. +func handlePutMessageError(firstGateway *id.ID, instance *network.Instance, + session *storage.Session, nodeRegistration chan network.NodeGateway, + recipientString string, bestRound *pb.RoundInfo, + err error) (recoverable bool, returnErr error) { + + // If the comm errors or the message fails to send, then continue retrying; + // otherwise, return if it sends properly + if strings.Contains(err.Error(), "try a different round.") { + return false, errors.WithMessagef(err, "Failed to send to [%s] due to "+ + "round error with round %d, bailing...", + recipientString, bestRound.ID) + } else if strings.Contains(err.Error(), "Could not authenticate client. "+ + "Is the client registered with this node?") { + // If send failed due to the gateway not recognizing the authorization, + // then renegotiate with the node to refresh it + nodeID := firstGateway.DeepCopy() + nodeID.SetType(id.Node) + + // Delete the keys + session.Cmix().Remove(nodeID) + + // Trigger + go handleMissingNodeKeys(instance, nodeRegistration, []*id.ID{nodeID}) + + return true, errors.WithMessagef(err, "Failed to send to [%s] via %s "+ + "due to failed authentication, retrying...", + recipientString, firstGateway) + } + + return false, errors.WithMessage(err, "Failed to put cmix message") + +} + +// processRound is a helper function that determines the gateway to send to for +// a round and retrieves the round keys. +func processRound(instance *network.Instance, session *storage.Session, + nodeRegistration chan network.NodeGateway, bestRound *pb.RoundInfo, + recipientString, messageDigest string) (*id.ID, *cmix.RoundKeys, error) { + + // Build the topology + idList, err := id.NewIDListFromBytes(bestRound.Topology) + if err != nil { + return nil, nil, errors.WithMessagef(err, "Failed to use topology for "+ + "round %d when sending to [%s] (msgDigest(s): %s)", + bestRound.ID, recipientString, messageDigest) + } + topology := connect.NewCircuit(idList) + + // Get the keys for the round, reject if any nodes do not have keying + // relationships + roundKeys, missingKeys := session.Cmix().GetRoundKeys(topology) + if len(missingKeys) > 0 { + go handleMissingNodeKeys(instance, nodeRegistration, missingKeys) + + return nil, nil, errors.Errorf("Failed to send on round %d to [%s] "+ + "(msgDigest(s): %s) due to missing relationships with nodes: %s", + bestRound.ID, recipientString, messageDigest, missingKeys) + } + + // Get the gateway to transmit to + firstGateway := topology.GetNodeAtIndex(0).DeepCopy() + firstGateway.SetType(id.Gateway) + + return firstGateway, roundKeys, nil +} + +// buildSlotMessage is a helper function which forms a slotted message to send +// to a gateway. It encrypts passed in message and generates an ephemeral ID for +// the recipient. +func buildSlotMessage(msg format.Message, recipient *id.ID, target *id.ID, + stream *fastRNG.Stream, senderId *id.ID, bestRound *pb.RoundInfo, + roundKeys *cmix.RoundKeys) (*pb.GatewaySlot, format.Message, ephemeral.Id, + error) { + + // Set the ephemeral ID + ephID, _, _, err := ephemeral.GetId(recipient, + uint(bestRound.AddressSpaceSize), + int64(bestRound.Timestamps[states.QUEUED])) + if err != nil { + jww.FATAL.Panicf("Failed to generate ephemeral ID when sending to %s "+ + "(msgDigest: %s): %+v", err, recipient, msg.Digest()) + } + + ephIdFilled, err := ephID.Fill(uint(bestRound.AddressSpaceSize), stream) + if err != nil { + jww.FATAL.Panicf("Failed to obfuscate the ephemeralID when sending "+ + "to %s (msgDigest: %s): %+v", recipient, msg.Digest(), err) + } + + msg.SetEphemeralRID(ephIdFilled[:]) + + // Set the identity fingerprint + ifp := fingerprint.IdentityFP(msg.GetContents(), recipient) + + msg.SetIdentityFP(ifp) + + // Encrypt the message + salt := make([]byte, 32) + _, err = stream.Read(salt) + if err != nil { + jww.ERROR.Printf("Failed to generate salt when sending to %s "+ + "(msgDigest: %s): %+v", recipient, msg.Digest(), err) + return nil, format.Message{}, ephemeral.Id{}, errors.WithMessage(err, + "Failed to generate salt, this should never happen") + } + + encMsg, kmacs := roundKeys.Encrypt(msg, salt, id.Round(bestRound.ID)) + + // Build the message payload + msgPacket := &pb.Slot{ + SenderID: senderId.Bytes(), + PayloadA: encMsg.GetPayloadA(), + PayloadB: encMsg.GetPayloadB(), + Salt: salt, + KMACs: kmacs, + } + + // Create the wrapper to the gateway + slot := &pb.GatewaySlot{ + Message: msgPacket, + RoundID: bestRound.ID, + Target: target.Bytes(), + } + + // Add the mac proving ownership + slot.MAC = roundKeys.MakeClientGatewayKey(salt, + network.GenerateSlotDigest(slot)) + + return slot, encMsg, ephID, nil +} + +// handleMissingNodeKeys signals to the node registration thread to register a +// node if keys are missing. Identity is triggered automatically when the node +// is first seen, so this should on trigger on rare events. +func handleMissingNodeKeys(instance *network.Instance, + newNodeChan chan network.NodeGateway, nodes []*id.ID) { + for _, n := range nodes { + ng, err := instance.GetNodeAndGateway(n) + if err != nil { + jww.ERROR.Printf("Node contained in round cannot be found: %s", err) + continue + } + + select { + case newNodeChan <- ng: + default: + jww.ERROR.Printf("Failed to send node registration for %s", n) + } + + } +} + +// messageMapToStrings serializes a map of IDs and messages into a string of IDs +// and a string of message digests. Intended for use in printing to logs. +func messageMapToStrings(msgList map[id.ID]format.Message) (string, string) { + idStrings := make([]string, 0, len(msgList)) + msgDigests := make([]string, 0, len(msgList)) + for uid, msg := range msgList { + idStrings = append(idStrings, uid.String()) + msgDigests = append(msgDigests, msg.Digest()) + } + + return strings.Join(idStrings, ","), strings.Join(msgDigests, ",") +} + +// messagesToDigestString serializes a list of messages into a string of message +// digests. Intended for use in printing to the logs. +func messagesToDigestString(msgs []format.Message) string { + msgDigests := make([]string, 0, len(msgs)) + for _, msg := range msgs { + msgDigests = append(msgDigests, msg.Digest()) + } + + return strings.Join(msgDigests, ",") +} + +// ephemeralIdListToString serializes a list of ephemeral IDs into a human- +// readable format. Intended for use in printing to logs. +func ephemeralIdListToString(idList []ephemeral.Id) string { + idStrings := make([]string, 0, len(idList)) + + for i := 0; i < len(idList); i++ { + ephIdStr := strconv.FormatInt(idList[i].Int64(), 10) + idStrings = append(idStrings, ephIdStr) + } + + return strings.Join(idStrings, ",") +} diff --git a/network/message/sendCmix_test.go b/network/message/sendCmix_test.go index 04da108426dda151bec4777338fe4a0036366832..d3d7e116bd4eb5337628adccc87d422b0ae4b327 100644 --- a/network/message/sendCmix_test.go +++ b/network/message/sendCmix_test.go @@ -18,52 +18,26 @@ import ( "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/elixxir/primitives/format" "gitlab.com/elixxir/primitives/states" - "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/crypto/large" "gitlab.com/xx_network/primitives/id" - "gitlab.com/xx_network/primitives/ndf" "gitlab.com/xx_network/primitives/netTime" "testing" "time" ) -type MockSendCMIXComms struct { - t *testing.T -} - -func (mc *MockSendCMIXComms) GetHost(hostId *id.ID) (*connect.Host, bool) { - nid1 := id.NewIdFromString("zezima", id.Node, mc.t) - gwid := nid1.DeepCopy() - gwid.SetType(id.Gateway) - h, _ := connect.NewHost(gwid, "0.0.0.0", []byte(""), connect.HostParams{ - MaxRetries: 0, - AuthEnabled: false, - }) - return h, true -} +type dummyEvent struct{} -func (mc *MockSendCMIXComms) AddHost(hid *id.ID, address string, cert []byte, params connect.HostParams) (host *connect.Host, err error) { - host, _ = mc.GetHost(nil) - return host, nil -} - -func (mc *MockSendCMIXComms) RemoveHost(hid *id.ID) { - -} - -func (mc *MockSendCMIXComms) SendPutMessage(host *connect.Host, message *mixmessages.GatewaySlot) (*mixmessages.GatewaySlotResponse, error) { - return &mixmessages.GatewaySlotResponse{ - Accepted: true, - RoundID: 3, - }, nil -} +func (e *dummyEvent) Report(priority int, category, evtType, details string) {} +// Unit test func Test_attemptSendCmix(t *testing.T) { sess1 := storage.InitTestingSession(t) sess2 := storage.InitTestingSession(t) + events := &dummyEvent{} + sw := switchboard.New() l := TestListener{ ch: make(chan bool), @@ -107,7 +81,7 @@ func Test_attemptSendCmix(t *testing.T) { AddressSpaceSize: 4, } - if err = testutils.SignRoundInfo(ri, t); err != nil { + if err = testutils.SignRoundInfoRsa(ri, t); err != nil { t.Errorf("Failed to sign mock round info: %v", err) } @@ -115,7 +89,7 @@ func Test_attemptSendCmix(t *testing.T) { if err != nil { t.Errorf("Failed to load a key for testing: %v", err) } - rnd := ds.NewRound(ri, pubKey) + rnd := ds.NewRound(ri, pubKey, nil) inst.GetWaitingRounds().Insert(rnd) i := internal.Internal{ Session: sess1, @@ -134,77 +108,21 @@ func Test_attemptSendCmix(t *testing.T) { t.Errorf("%+v", errors.New(err.Error())) return } - m := NewManager(i, params.Messages{ + m := NewManager(i, params.Network{Messages: params.Messages{ MessageReceptionBuffLen: 20, MessageReceptionWorkerPoolSize: 20, MaxChecksGarbledMessage: 20, GarbledMessageWait: time.Hour, - }, nil, sender) + }}, nil, sender) msgCmix := format.NewMessage(m.Session.Cmix().GetGroup().GetP().ByteLen()) msgCmix.SetContents([]byte("test")) e2e.SetUnencrypted(msgCmix, m.Session.User().GetCryptographicIdentity().GetTransmissionID()) - _, _, err = sendCmixHelper(sender, msgCmix, sess2.GetUser().ReceptionID, params.GetDefaultCMIX(), - m.Instance, m.Session, m.nodeRegistration, m.Rng, - m.TransmissionID, &MockSendCMIXComms{t: t}) + _, _, err = sendCmixHelper(sender, msgCmix, sess2.GetUser().ReceptionID, + params.GetDefaultCMIX(), make(map[string]interface{}), m.Instance, m.Session, m.nodeRegistration, + m.Rng, events, m.TransmissionID, &MockSendCMIXComms{t: t}, nil) if err != nil { t.Errorf("Failed to sendcmix: %+v", err) panic("t") return } } - -func getNDF() *ndf.NetworkDefinition { - nodeId := id.NewIdFromString("zezima", id.Node, &testing.T{}) - gwId := nodeId.DeepCopy() - gwId.SetType(id.Gateway) - return &ndf.NetworkDefinition{ - E2E: ndf.Group{ - Prime: "E2EE983D031DC1DB6F1A7A67DF0E9A8E5561DB8E8D49413394C049B" + - "7A8ACCEDC298708F121951D9CF920EC5D146727AA4AE535B0922C688B55B3DD2AE" + - "DF6C01C94764DAB937935AA83BE36E67760713AB44A6337C20E7861575E745D31F" + - "8B9E9AD8412118C62A3E2E29DF46B0864D0C951C394A5CBBDC6ADC718DD2A3E041" + - "023DBB5AB23EBB4742DE9C1687B5B34FA48C3521632C4A530E8FFB1BC51DADDF45" + - "3B0B2717C2BC6669ED76B4BDD5C9FF558E88F26E5785302BEDBCA23EAC5ACE9209" + - "6EE8A60642FB61E8F3D24990B8CB12EE448EEF78E184C7242DD161C7738F32BF29" + - "A841698978825B4111B4BC3E1E198455095958333D776D8B2BEEED3A1A1A221A6E" + - "37E664A64B83981C46FFDDC1A45E3D5211AAF8BFBC072768C4F50D7D7803D2D4F2" + - "78DE8014A47323631D7E064DE81C0C6BFA43EF0E6998860F1390B5D3FEACAF1696" + - "015CB79C3F9C2D93D961120CD0E5F12CBB687EAB045241F96789C38E89D796138E" + - "6319BE62E35D87B1048CA28BE389B575E994DCA755471584A09EC723742DC35873" + - "847AEF49F66E43873", - Generator: "2", - }, - CMIX: ndf.Group{ - Prime: "9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642F0B5C48" + - "C8F7A41AADFA187324B87674FA1822B00F1ECF8136943D7C55757264E5A1A44F" + - "FE012E9936E00C1D3E9310B01C7D179805D3058B2A9F4BB6F9716BFE6117C6B5" + - "B3CC4D9BE341104AD4A80AD6C94E005F4B993E14F091EB51743BF33050C38DE2" + - "35567E1B34C3D6A5C0CEAA1A0F368213C3D19843D0B4B09DCB9FC72D39C8DE41" + - "F1BF14D4BB4563CA28371621CAD3324B6A2D392145BEBFAC748805236F5CA2FE" + - "92B871CD8F9C36D3292B5509CA8CAA77A2ADFC7BFD77DDA6F71125A7456FEA15" + - "3E433256A2261C6A06ED3693797E7995FAD5AABBCFBE3EDA2741E375404AE25B", - Generator: "5C7FF6B06F8F143FE8288433493E4769C4D988ACE5BE25A0E24809670716C613" + - "D7B0CEE6932F8FAA7C44D2CB24523DA53FBE4F6EC3595892D1AA58C4328A06C4" + - "6A15662E7EAA703A1DECF8BBB2D05DBE2EB956C142A338661D10461C0D135472" + - "085057F3494309FFA73C611F78B32ADBB5740C361C9F35BE90997DB2014E2EF5" + - "AA61782F52ABEB8BD6432C4DD097BC5423B285DAFB60DC364E8161F4A2A35ACA" + - "3A10B1C4D203CC76A470A33AFDCBDD92959859ABD8B56E1725252D78EAC66E71" + - "BA9AE3F1DD2487199874393CD4D832186800654760E1E34C09E4D155179F9EC0" + - "DC4473F996BDCE6EED1CABED8B6F116F7AD9CF505DF0F998E34AB27514B0FFE7", - }, - Gateways: []ndf.Gateway{ - { - ID: gwId.Marshal(), - Address: "0.0.0.0", - TlsCertificate: "", - }, - }, - Nodes: []ndf.Node{ - { - ID: nodeId.Marshal(), - Address: "0.0.0.0", - TlsCertificate: "", - }, - }, - } -} diff --git a/network/message/sendE2E.go b/network/message/sendE2E.go index 16c14701d47ead025dae9758fa4697ce6c5ea007..e26dcb6de4ee21a8a72bcebf2dbf7ea26220453d 100644 --- a/network/message/sendE2E.go +++ b/network/message/sendE2E.go @@ -13,6 +13,7 @@ import ( "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/keyExchange" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/crypto/e2e" "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" @@ -21,9 +22,10 @@ import ( "time" ) -func (m *Manager) SendE2E(msg message.Send, param params.E2E) ([]id.Round, e2e.MessageID, error) { +func (m *Manager) SendE2E(msg message.Send, param params.E2E, + stop *stoppable.Single) ([]id.Round, e2e.MessageID, time.Time, error) { if msg.MessageType == message.Raw { - return nil, e2e.MessageID{}, errors.Errorf("Raw (%d) is a reserved "+ + return nil, e2e.MessageID{}, time.Time{}, errors.Errorf("Raw (%d) is a reserved "+ "message type", msg.MessageType) } //timestamp the message @@ -33,7 +35,7 @@ func (m *Manager) SendE2E(msg message.Send, param params.E2E) ([]id.Round, e2e.M partitions, internalMsgId, err := m.partitioner.Partition(msg.Recipient, msg.MessageType, ts, msg.Payload) if err != nil { - return nil, e2e.MessageID{}, errors.WithMessage(err, "failed to send unsafe message") + return nil, e2e.MessageID{}, time.Time{}, errors.WithMessage(err, "failed to send unsafe message") } //encrypt then send the partitions over cmix @@ -43,7 +45,7 @@ func (m *Manager) SendE2E(msg message.Send, param params.E2E) ([]id.Round, e2e.M // get the key manager for the partner partner, err := m.Session.E2e().GetPartner(msg.Recipient) if err != nil { - return nil, e2e.MessageID{}, errors.WithMessagef(err, + return nil, e2e.MessageID{}, time.Time{}, errors.WithMessagef(err, "Could not send End to End encrypted "+ "message, no relationship found with %s", msg.Recipient) } @@ -58,7 +60,7 @@ func (m *Manager) SendE2E(msg message.Send, param params.E2E) ([]id.Round, e2e.M if msg.MessageType != message.KeyExchangeTrigger { // check if any rekeys need to happen and trigger them keyExchange.CheckKeyExchanges(m.Instance, m.SendE2E, - m.Session, partner, 1*time.Minute) + m.Session, partner, 1*time.Minute, stop) } //create the cmix message @@ -81,7 +83,7 @@ func (m *Manager) SendE2E(msg message.Send, param params.E2E) ([]id.Round, e2e.M key, err = partner.GetKeyForSending(param.Type) } if err != nil { - return nil, e2e.MessageID{}, errors.WithMessagef(err, + return nil, e2e.MessageID{}, time.Time{}, errors.WithMessagef(err, "Failed to get key for end to end encryption") } @@ -96,7 +98,7 @@ func (m *Manager) SendE2E(msg message.Send, param params.E2E) ([]id.Round, e2e.M go func(i int) { var err error roundIds[i], _, err = m.SendCMIX(m.sender, msgEnc, msg.Recipient, - param.CMIX) + param.CMIX, stop) if err != nil { errCh <- err } @@ -111,14 +113,14 @@ func (m *Manager) SendE2E(msg message.Send, param params.E2E) ([]id.Round, e2e.M if numFail > 0 { jww.INFO.Printf("Failed to E2E send %d/%d to %s", numFail, len(partitions), msg.Recipient) - return nil, e2e.MessageID{}, errors.Errorf("Failed to E2E send %v/%v sub payloads:"+ + return nil, e2e.MessageID{}, time.Time{}, errors.Errorf("Failed to E2E send %v/%v sub payloads:"+ " %s", numFail, len(partitions), errRtn) } else { - jww.INFO.Printf("Sucesfully E2E sent %d/%d to %s", + jww.INFO.Printf("Successfully E2E sent %d/%d to %s", len(partitions)-numFail, len(partitions), msg.Recipient) } //return the rounds if everything send successfully msgID := e2e.NewMessageID(partner.GetSendRelationshipFingerprint(), internalMsgId) - return roundIds, msgID, nil + return roundIds, msgID, ts, nil } diff --git a/network/message/sendManyCmix.go b/network/message/sendManyCmix.go new file mode 100644 index 0000000000000000000000000000000000000000..d9e098715a5032c90ed2bef7438f15ce2714f890 --- /dev/null +++ b/network/message/sendManyCmix.go @@ -0,0 +1,184 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package message + +import ( + "github.com/golang-collections/collections/set" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/client/network/gateway" + "gitlab.com/elixxir/client/storage" + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/comms/network" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/xx_network/comms/connect" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "gitlab.com/xx_network/primitives/netTime" + "strings" +) + +// SendManyCMIX sends many "raw" cMix message payloads to each of the provided +// recipients. Used to send messages in group chats. Metadata is NOT protected +// with this call and can leak data about yourself. Returns the round ID of the +// round the payload was sent or an error if it fails. +// WARNING: Potentially Unsafe +func (m *Manager) SendManyCMIX(sender *gateway.Sender, + messages map[id.ID]format.Message, p params.CMIX) (id.Round, []ephemeral.Id, + error) { + + // Create message copies + messagesCopy := make(map[id.ID]format.Message, len(messages)) + for rid, msg := range messages { + messagesCopy[rid] = msg.Copy() + } + + return sendManyCmixHelper(sender, messagesCopy, p, m.Instance, m.Session, + m.nodeRegistration, m.Rng, m.TransmissionID, m.Comms) +} + +// sendManyCmixHelper is a helper function for Manager.SendManyCMIX. +// +// NOTE: Payloads sent are not end to end encrypted, metadata is NOT protected +// with this call; see SendE2E for end to end encryption and full privacy +// protection. Internal SendManyCMIX, which bypasses the network check, will +// attempt to send to the network without checking state. It has a built in +// retry system which can be configured through the params object. +// +// If the message is successfully sent, the ID of the round sent it is returned, +// which can be registered with the network instance to get a callback on its +// status. +func sendManyCmixHelper(sender *gateway.Sender, msgs map[id.ID]format.Message, + param params.CMIX, instance *network.Instance, session *storage.Session, + nodeRegistration chan network.NodeGateway, rng *fastRNG.StreamGenerator, + senderId *id.ID, comms sendCmixCommsInterface) (id.Round, []ephemeral.Id, error) { + + timeStart := netTime.Now() + attempted := set.New() + stream := rng.GetStream() + defer stream.Close() + + recipientString, msgDigests := messageMapToStrings(msgs) + + jww.INFO.Printf("Looking for round to send cMix messages to [%s] "+ + "(msgDigest: %s)", recipientString, msgDigests) + + for numRoundTries := uint(0); numRoundTries < param.RoundTries; numRoundTries++ { + elapsed := netTime.Since(timeStart) + + if elapsed > param.Timeout { + jww.INFO.Printf("No rounds to send to %s (msgDigest: %s) were found "+ + "before timeout %s", recipientString, msgDigests, param.Timeout) + return 0, []ephemeral.Id{}, + errors.New("sending cMix message timed out") + } + + if numRoundTries > 0 { + jww.INFO.Printf("Attempt %d to find round to send message to %s "+ + "(msgDigest: %s)", numRoundTries+1, recipientString, msgDigests) + } + + remainingTime := param.Timeout - elapsed + + // Find the best round to send to, excluding attempted rounds + bestRound, _ := instance.GetWaitingRounds().GetUpcomingRealtime( + remainingTime, attempted, sendTimeBuffer) + if bestRound == nil { + continue + } + + // Add the round on to the list of attempted so it is not tried again + attempted.Insert(bestRound) + + // Retrieve host and key information from round + firstGateway, roundKeys, err := processRound(instance, session, + nodeRegistration, bestRound, recipientString, msgDigests) + if err != nil { + jww.WARN.Printf("SendManyCMIX failed to process round %d "+ + "(will retry): %+v", bestRound.ID, err) + continue + } + + // Build a slot for every message and recipient + slots := make([]*pb.GatewaySlot, len(msgs)) + ephemeralIds := make([]ephemeral.Id, len(msgs)) + encMsgs := make([]format.Message, len(msgs)) + i := 0 + for recipient, msg := range msgs { + slots[i], encMsgs[i], ephemeralIds[i], err = buildSlotMessage( + msg, &recipient, firstGateway, stream, senderId, bestRound, roundKeys) + if err != nil { + return 0, []ephemeral.Id{}, errors.Errorf("failed to build "+ + "slot message for %s: %+v", recipient, err) + } + i++ + } + + // Serialize lists into a printable format + ephemeralIdsString := ephemeralIdListToString(ephemeralIds) + encMsgsDigest := messagesToDigestString(encMsgs) + + jww.INFO.Printf("Sending to EphIDs [%s] (%s) on round %d, "+ + "(msgDigest: %s, ecrMsgDigest: %s) via gateway %s", + ephemeralIdsString, recipientString, bestRound.ID, msgDigests, + encMsgsDigest, firstGateway) + + // Wrap slots in the proper message type + wrappedMessage := &pb.GatewaySlots{ + Messages: slots, + RoundID: bestRound.ID, + } + + // Send the payload + sendFunc := func(host *connect.Host, target *id.ID) (interface{}, bool, error) { + wrappedMessage.Target = target.Marshal() + result, err := comms.SendPutManyMessages(host, wrappedMessage) + if err != nil { + warn, err := handlePutMessageError(firstGateway, instance, + session, nodeRegistration, recipientString, bestRound, err) + if warn { + jww.WARN.Printf("SendManyCMIX Failed: %+v", err) + } else { + return result, false, errors.WithMessagef(err, + "SendManyCMIX %s", unrecoverableError) + } + } + return result, false, err + } + result, err := sender.SendToPreferred([]*id.ID{firstGateway}, sendFunc, nil) + + // If the comm errors or the message fails to send, continue retrying + if err != nil { + if !strings.Contains(err.Error(), unrecoverableError) { + jww.ERROR.Printf("SendManyCMIX failed to send to EphIDs [%s] "+ + "(sources: %s) on round %d, trying a new round %+v", + ephemeralIdsString, recipientString, bestRound.ID, err) + continue + } + + return 0, []ephemeral.Id{}, err + } + + // Return if it sends properly + gwSlotResp := result.(*pb.GatewaySlotResponse) + if gwSlotResp.Accepted { + jww.INFO.Printf("Successfully sent to EphIDs %v (sources: [%s]) in "+ + "round %d", ephemeralIdsString, recipientString, bestRound.ID) + return id.Round(bestRound.ID), ephemeralIds, nil + } else { + jww.FATAL.Panicf("Gateway %s returned no error, but failed to "+ + "accept message when sending to EphIDs [%s] (%s) on round %d", + firstGateway, ephemeralIdsString, recipientString, bestRound.ID) + } + } + + return 0, []ephemeral.Id{}, + errors.New("failed to send the message, unknown error") +} diff --git a/network/message/sendManyCmix_test.go b/network/message/sendManyCmix_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1b0da6c083739765496e111628c5c3a74bdce05a --- /dev/null +++ b/network/message/sendManyCmix_test.go @@ -0,0 +1,134 @@ +package message + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/client/network/gateway" + "gitlab.com/elixxir/client/network/internal" + "gitlab.com/elixxir/client/storage" + "gitlab.com/elixxir/client/switchboard" + "gitlab.com/elixxir/comms/client" + "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/comms/network" + ds "gitlab.com/elixxir/comms/network/dataStructures" + "gitlab.com/elixxir/comms/testutils" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/e2e" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/crypto/large" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "testing" + "time" +) + +// Unit test +func Test_attemptSendManyCmix(t *testing.T) { + sess1 := storage.InitTestingSession(t) + + numRecipients := 3 + recipients := make([]*id.ID, numRecipients) + sw := switchboard.New() + l := TestListener{ + ch: make(chan bool), + } + for i := 0; i < numRecipients; i++ { + sess := storage.InitTestingSession(t) + sw.RegisterListener(sess.GetUser().TransmissionID, message.Raw, l) + recipients[i] = sess.GetUser().ReceptionID + } + + comms, err := client.NewClientComms(sess1.GetUser().TransmissionID, nil, nil, nil) + if err != nil { + t.Errorf("Failed to start client comms: %+v", err) + } + inst, err := network.NewInstanceTesting(comms.ProtoComms, getNDF(), nil, nil, nil, t) + if err != nil { + t.Errorf("Failed to start instance: %+v", err) + } + now := netTime.Now() + nid1 := id.NewIdFromString("zezima", id.Node, t) + nid2 := id.NewIdFromString("jakexx360", id.Node, t) + nid3 := id.NewIdFromString("westparkhome", id.Node, t) + grp := cyclic.NewGroup(large.NewInt(7), large.NewInt(13)) + sess1.Cmix().Add(nid1, grp.NewInt(1)) + sess1.Cmix().Add(nid2, grp.NewInt(2)) + sess1.Cmix().Add(nid3, grp.NewInt(3)) + + timestamps := []uint64{ + uint64(now.Add(-30 * time.Second).UnixNano()), // PENDING + uint64(now.Add(-25 * time.Second).UnixNano()), // PRECOMPUTING + uint64(now.Add(-5 * time.Second).UnixNano()), // STANDBY + uint64(now.Add(5 * time.Second).UnixNano()), // QUEUED + 0} // REALTIME + + ri := &mixmessages.RoundInfo{ + ID: 3, + UpdateID: 0, + State: uint32(states.QUEUED), + BatchSize: 0, + Topology: [][]byte{nid1.Marshal(), nid2.Marshal(), nid3.Marshal()}, + Timestamps: timestamps, + Errors: nil, + ClientErrors: nil, + ResourceQueueTimeoutMillis: 0, + Signature: nil, + AddressSpaceSize: 4, + } + + if err = testutils.SignRoundInfoRsa(ri, t); err != nil { + t.Errorf("Failed to sign mock round info: %v", err) + } + + pubKey, err := testutils.LoadPublicKeyTesting(t) + if err != nil { + t.Errorf("Failed to load a key for testing: %v", err) + } + rnd := ds.NewRound(ri, pubKey, nil) + inst.GetWaitingRounds().Insert(rnd) + i := internal.Internal{ + Session: sess1, + Switchboard: sw, + Rng: fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + Comms: comms, + Health: nil, + TransmissionID: sess1.GetUser().TransmissionID, + Instance: inst, + NodeRegistration: nil, + } + p := gateway.DefaultPoolParams() + p.MaxPoolSize = 1 + sender, err := gateway.NewSender(p, i.Rng, getNDF(), &MockSendCMIXComms{t: t}, i.Session, nil) + if err != nil { + t.Errorf("%+v", errors.New(err.Error())) + return + } + m := NewManager(i, params.Network{Messages: params.Messages{ + MessageReceptionBuffLen: 20, + MessageReceptionWorkerPoolSize: 20, + MaxChecksGarbledMessage: 20, + GarbledMessageWait: time.Hour, + }}, nil, sender) + msgCmix := format.NewMessage(m.Session.Cmix().GetGroup().GetP().ByteLen()) + msgCmix.SetContents([]byte("test")) + e2e.SetUnencrypted(msgCmix, m.Session.User().GetCryptographicIdentity().GetTransmissionID()) + messages := make([]format.Message, numRecipients) + for i := 0; i < numRecipients; i++ { + messages[i] = msgCmix + } + + msgMap := make(map[id.ID]format.Message, numRecipients) + for i := 0; i < numRecipients; i++ { + msgMap[*recipients[i]] = msgCmix + } + + _, _, err = sendManyCmixHelper(sender, msgMap, params.GetDefaultCMIX(), m.Instance, + m.Session, m.nodeRegistration, m.Rng, m.TransmissionID, &MockSendCMIXComms{t: t}) + if err != nil { + t.Errorf("Failed to sendcmix: %+v", err) + } +} diff --git a/network/message/sendUnsafe.go b/network/message/sendUnsafe.go index 19f7d5d5ce0bfe6dd14df77b76f0a21c824b2404..938bcc07c8bab9bd24e1bdd53cb1c38c3b4111c4 100644 --- a/network/message/sendUnsafe.go +++ b/network/message/sendUnsafe.go @@ -64,7 +64,7 @@ func (m *Manager) SendUnsafe(msg message.Send, param params.Unsafe) ([]id.Round, wg.Add(1) go func(i int) { var err error - roundIds[i], _, err = m.SendCMIX(m.sender, msgCmix, msg.Recipient, param.CMIX) + roundIds[i], _, err = m.SendCMIX(m.sender, msgCmix, msg.Recipient, param.CMIX, nil) if err != nil { errCh <- err } diff --git a/network/message/utils_test.go b/network/message/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..18284ad0bb6b6693db062f2fb5b048b5e18ca08f --- /dev/null +++ b/network/message/utils_test.go @@ -0,0 +1,106 @@ +package message + +import ( + "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/xx_network/comms/connect" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/ndf" + "testing" +) + +type MockSendCMIXComms struct { + t *testing.T +} + +func (mc *MockSendCMIXComms) GetHost(*id.ID) (*connect.Host, bool) { + nid1 := id.NewIdFromString("zezima", id.Node, mc.t) + gwID := nid1.DeepCopy() + gwID.SetType(id.Gateway) + h, _ := connect.NewHost(gwID, "0.0.0.0", []byte(""), connect.HostParams{ + MaxRetries: 0, + AuthEnabled: false, + }) + return h, true +} + +func (mc *MockSendCMIXComms) AddHost(*id.ID, string, []byte, connect.HostParams) (host *connect.Host, err error) { + host, _ = mc.GetHost(nil) + return host, nil +} + +func (mc *MockSendCMIXComms) RemoveHost(*id.ID) { + +} + +func (mc *MockSendCMIXComms) SendPutMessage(*connect.Host, *mixmessages.GatewaySlot) (*mixmessages.GatewaySlotResponse, error) { + return &mixmessages.GatewaySlotResponse{ + Accepted: true, + RoundID: 3, + }, nil +} + +func (mc *MockSendCMIXComms) SendPutManyMessages(*connect.Host, *mixmessages.GatewaySlots) (*mixmessages.GatewaySlotResponse, error) { + return &mixmessages.GatewaySlotResponse{ + Accepted: true, + RoundID: 3, + }, nil +} + +func getNDF() *ndf.NetworkDefinition { + nodeId := id.NewIdFromString("zezima", id.Node, &testing.T{}) + gwId := nodeId.DeepCopy() + gwId.SetType(id.Gateway) + return &ndf.NetworkDefinition{ + E2E: ndf.Group{ + Prime: "E2EE983D031DC1DB6F1A7A67DF0E9A8E5561DB8E8D49413394C049B7A" + + "8ACCEDC298708F121951D9CF920EC5D146727AA4AE535B0922C688B55B3D" + + "D2AEDF6C01C94764DAB937935AA83BE36E67760713AB44A6337C20E78615" + + "75E745D31F8B9E9AD8412118C62A3E2E29DF46B0864D0C951C394A5CBBDC" + + "6ADC718DD2A3E041023DBB5AB23EBB4742DE9C1687B5B34FA48C3521632C" + + "4A530E8FFB1BC51DADDF453B0B2717C2BC6669ED76B4BDD5C9FF558E88F2" + + "6E5785302BEDBCA23EAC5ACE92096EE8A60642FB61E8F3D24990B8CB12EE" + + "448EEF78E184C7242DD161C7738F32BF29A841698978825B4111B4BC3E1E" + + "198455095958333D776D8B2BEEED3A1A1A221A6E37E664A64B83981C46FF" + + "DDC1A45E3D5211AAF8BFBC072768C4F50D7D7803D2D4F278DE8014A47323" + + "631D7E064DE81C0C6BFA43EF0E6998860F1390B5D3FEACAF1696015CB79C" + + "3F9C2D93D961120CD0E5F12CBB687EAB045241F96789C38E89D796138E63" + + "19BE62E35D87B1048CA28BE389B575E994DCA755471584A09EC723742DC3" + + "5873847AEF49F66E43873", + Generator: "2", + }, + CMIX: ndf.Group{ + Prime: "9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642" + + "F0B5C48C8F7A41AADFA187324B87674FA1822B00F1ECF8136943D7C55757" + + "264E5A1A44FFE012E9936E00C1D3E9310B01C7D179805D3058B2A9F4BB6F" + + "9716BFE6117C6B5B3CC4D9BE341104AD4A80AD6C94E005F4B993E14F091E" + + "B51743BF33050C38DE235567E1B34C3D6A5C0CEAA1A0F368213C3D19843D" + + "0B4B09DCB9FC72D39C8DE41F1BF14D4BB4563CA28371621CAD3324B6A2D3" + + "92145BEBFAC748805236F5CA2FE92B871CD8F9C36D3292B5509CA8CAA77A" + + "2ADFC7BFD77DDA6F71125A7456FEA153E433256A2261C6A06ED3693797E7" + + "995FAD5AABBCFBE3EDA2741E375404AE25B", + Generator: "5C7FF6B06F8F143FE8288433493E4769C4D988ACE5BE25A0E2480" + + "9670716C613D7B0CEE6932F8FAA7C44D2CB24523DA53FBE4F6EC3595892D" + + "1AA58C4328A06C46A15662E7EAA703A1DECF8BBB2D05DBE2EB956C142A33" + + "8661D10461C0D135472085057F3494309FFA73C611F78B32ADBB5740C361" + + "C9F35BE90997DB2014E2EF5AA61782F52ABEB8BD6432C4DD097BC5423B28" + + "5DAFB60DC364E8161F4A2A35ACA3A10B1C4D203CC76A470A33AFDCBDD929" + + "59859ABD8B56E1725252D78EAC66E71BA9AE3F1DD2487199874393CD4D83" + + "2186800654760E1E34C09E4D155179F9EC0DC4473F996BDCE6EED1CABED8" + + "B6F116F7AD9CF505DF0F998E34AB27514B0FFE7", + }, + Gateways: []ndf.Gateway{ + { + ID: gwId.Marshal(), + Address: "0.0.0.0", + TlsCertificate: "", + }, + }, + Nodes: []ndf.Node{ + { + ID: nodeId.Marshal(), + Address: "0.0.0.0", + TlsCertificate: "", + }, + }, + } +} diff --git a/network/node/register.go b/network/node/register.go index c89d2dd55371bef074a3d417f5ca05ae66d4e550..2aa7a7d203a23cd800b202075f6ee169b1e2592e 100644 --- a/network/node/register.go +++ b/network/node/register.go @@ -55,23 +55,28 @@ func StartRegistration(sender *gateway.Sender, session *storage.Session, rngGen return multi } -func registerNodes(sender *gateway.Sender, session *storage.Session, rngGen *fastRNG.StreamGenerator, comms RegisterNodeCommsInterface, +func registerNodes(sender *gateway.Sender, session *storage.Session, + rngGen *fastRNG.StreamGenerator, comms RegisterNodeCommsInterface, stop *stoppable.Single, c chan network.NodeGateway) { u := session.User() regSignature := u.GetTransmissionRegistrationValidationSignature() + // Timestamp in which user has registered with registration + regTimestamp := u.GetRegistrationTimestamp().UnixNano() uci := u.GetCryptographicIdentity() cmix := session.Cmix() rng := rngGen.GetStream() interval := time.Duration(500) * time.Millisecond t := time.NewTicker(interval) - for true { + for { select { case <-stop.Quit(): t.Stop() + stop.ToStopped() return case gw := <-c: - err := registerWithNode(sender, comms, gw, regSignature, uci, cmix, rng) + err := registerWithNode(sender, comms, gw, regSignature, + regTimestamp, uci, cmix, rng, stop) if err != nil { jww.ERROR.Printf("Failed to register node: %+v", err) } @@ -82,8 +87,11 @@ func registerNodes(sender *gateway.Sender, session *storage.Session, rngGen *fas //registerWithNode serves as a helper for RegisterWithNodes // It registers a user with a specific in the client's ndf. -func registerWithNode(sender *gateway.Sender, comms RegisterNodeCommsInterface, ngw network.NodeGateway, regSig []byte, - uci *user.CryptographicIdentity, store *cmix.Store, rng csprng.Source) error { +func registerWithNode(sender *gateway.Sender, comms RegisterNodeCommsInterface, + ngw network.NodeGateway, regSig []byte, registrationTimestampNano int64, + uci *user.CryptographicIdentity, store *cmix.Store, rng csprng.Source, + stop *stoppable.Single) error { + nodeID, err := ngw.Node.GetNodeId() if err != nil { jww.ERROR.Println("registerWithNode() failed to decode nodeId") @@ -118,7 +126,9 @@ func registerWithNode(sender *gateway.Sender, comms RegisterNodeCommsInterface, // keys transmissionHash, _ := hash.NewCMixHash() - nonce, dhPub, err := requestNonce(sender, comms, gatewayID, regSig, uci, store, rng) + nonce, dhPub, err := requestNonce(sender, comms, gatewayID, regSig, + registrationTimestampNano, uci, store, rng, stop) + if err != nil { return errors.Errorf("Failed to request nonce: %+v", err) } @@ -129,7 +139,8 @@ func registerWithNode(sender *gateway.Sender, comms RegisterNodeCommsInterface, // Confirm received nonce jww.INFO.Printf("Register: Confirming received nonce from node %s", nodeID.String()) err = confirmNonce(sender, comms, uci.GetTransmissionID().Bytes(), - nonce, uci.GetTransmissionRSA(), gatewayID) + nonce, uci.GetTransmissionRSA(), gatewayID, stop) + if err != nil { errMsg := fmt.Sprintf("Register: Unable to confirm nonce: %v", err) return errors.New(errMsg) @@ -145,8 +156,10 @@ func registerWithNode(sender *gateway.Sender, comms RegisterNodeCommsInterface, return nil } -func requestNonce(sender *gateway.Sender, comms RegisterNodeCommsInterface, gwId *id.ID, regHash []byte, - uci *user.CryptographicIdentity, store *cmix.Store, rng csprng.Source) ([]byte, []byte, error) { +func requestNonce(sender *gateway.Sender, comms RegisterNodeCommsInterface, gwId *id.ID, + regSig []byte, registrationTimestampNano int64, uci *user.CryptographicIdentity, + store *cmix.Store, rng csprng.Source, stop *stoppable.Single) ([]byte, []byte, error) { + dhPub := store.GetDHPublicKey().Bytes() opts := rsa.NewDefaultOptions() opts.Hash = hash.CMixHash @@ -170,13 +183,15 @@ func requestNonce(sender *gateway.Sender, comms RegisterNodeCommsInterface, gwId Salt: uci.GetTransmissionSalt(), ClientRSAPubKey: string(rsa.CreatePublicKeyPem(uci.GetTransmissionRSA().GetPublic())), ClientSignedByServer: &messages.RSASignature{ - Signature: regHash, + Signature: regSig, }, ClientDHPubKey: dhPub, RequestSignature: &messages.RSASignature{ Signature: clientSig, }, Target: gwId.Marshal(), + // Timestamp in which user has registered with registration + TimeStamp: registrationTimestampNano, }) if err != nil { errMsg := fmt.Sprintf("Register: Failed requesting nonce from gateway: %+v", err) @@ -187,7 +202,8 @@ func requestNonce(sender *gateway.Sender, comms RegisterNodeCommsInterface, gwId return nil, err } return nonceResponse, nil - }) + }, stop) + if err != nil { return nil, nil, err } @@ -200,8 +216,9 @@ func requestNonce(sender *gateway.Sender, comms RegisterNodeCommsInterface, gwId // confirmNonce is a helper for the Register function // It signs a nonce and sends it for confirmation // Returns nil if successful, error otherwise -func confirmNonce(sender *gateway.Sender, comms RegisterNodeCommsInterface, UID, nonce []byte, - privateKeyRSA *rsa.PrivateKey, gwID *id.ID) error { +func confirmNonce(sender *gateway.Sender, comms RegisterNodeCommsInterface, UID, + nonce []byte, privateKeyRSA *rsa.PrivateKey, gwID *id.ID, + stop *stoppable.Single) error { opts := rsa.NewDefaultOptions() opts.Hash = hash.CMixHash h, _ := hash.NewCMixHash() @@ -240,6 +257,7 @@ func confirmNonce(sender *gateway.Sender, comms RegisterNodeCommsInterface, UID, return nil, err } return confirmResponse, nil - }) + }, stop) + return err } diff --git a/network/rounds/historical.go b/network/rounds/historical.go index 45aed7fdfaa7b296a46de79645e19fd010f88634..c3b9b4b9b4296ed776dfa7503f01b301ffc9e46d 100644 --- a/network/rounds/historical.go +++ b/network/rounds/historical.go @@ -8,7 +8,9 @@ package rounds import ( + "fmt" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage/reception" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/xx_network/comms/connect" @@ -41,19 +43,18 @@ type historicalRoundRequest struct { // Long running thread which process historical rounds // Can be killed by sending a signal to the quit channel // takes a comms interface to aid in testing -func (m *Manager) processHistoricalRounds(comm historicalRoundsComms, quitCh <-chan struct{}) { +func (m *Manager) processHistoricalRounds(comm historicalRoundsComms, stop *stoppable.Single) { timerCh := make(<-chan time.Time) rng := m.Rng.GetStream() var roundRequests []historicalRoundRequest - done := false - for !done { + for { shouldProcess := false // wait for a quit or new round to check select { - case <-quitCh: + case <-stop.Quit(): rng.Close() // return all roundRequests in the queue to the input channel so they can // be checked in the future. If the queue is full, disable them as @@ -64,7 +65,8 @@ func (m *Manager) processHistoricalRounds(comm historicalRoundsComms, quitCh <-c default: } } - done = true + stop.ToStopped() + return // if the timer elapses process roundRequests to ensure the delay isn't too long case <-timerCh: if len(roundRequests) > 0 { @@ -72,7 +74,7 @@ func (m *Manager) processHistoricalRounds(comm historicalRoundsComms, quitCh <-c } // get new round to lookup and force a lookup if case r := <-m.historicalRounds: - jww.DEBUG.Printf("Recieved and quing round %d for "+ + jww.DEBUG.Printf("Received and queueing round %d for "+ "historical rounds lookup", r.rid) roundRequests = append(roundRequests, r) if len(roundRequests) > int(m.params.MaxHistoricalRounds) { @@ -96,11 +98,13 @@ func (m *Manager) processHistoricalRounds(comm historicalRoundsComms, quitCh <-c Rounds: rounds, } + var gwHost *connect.Host result, err := m.sender.SendToAny(func(host *connect.Host) (interface{}, error) { jww.DEBUG.Printf("Requesting Historical rounds %v from "+ "gateway %s", rounds, host.GetId()) + gwHost = host return comm.RequestHistoricalRounds(host, hr) - }) + }, stop) if err != nil { jww.ERROR.Printf("Failed to request historical roundRequests "+ @@ -112,29 +116,34 @@ func (m *Manager) processHistoricalRounds(comm historicalRoundsComms, quitCh <-c } response := result.(*pb.HistoricalRoundsResponse) + rids := make([]uint64, 0) // process the returned historical roundRequests. for i, roundInfo := range response.Rounds { // The interface has missing returns returned as nil, such roundRequests // need be be removes as processing so the network follower will // pick them up in the future. if roundInfo == nil { + var errMsg string roundRequests[i].numAttempts++ if roundRequests[i].numAttempts == m.params.MaxHistoricalRoundsRetries { - jww.ERROR.Printf("Failed to retreive historical "+ + errMsg = fmt.Sprintf("Failed to retreive historical "+ "round %d on last attempt, will not try again", roundRequests[i].rid) } else { select { case m.historicalRounds <- roundRequests[i]: - jww.WARN.Printf("Failed to retreive historical "+ + errMsg = fmt.Sprintf("Failed to retreive historical "+ "round %d, will try up to %d more times", roundRequests[i].rid, m.params.MaxHistoricalRoundsRetries-roundRequests[i].numAttempts) default: - jww.WARN.Printf("Failed to retreive historical "+ + errMsg = fmt.Sprintf("Failed to retreive historical "+ "round %d, failed to try again, round will not be "+ "retreived", roundRequests[i].rid) } } + jww.WARN.Printf(errMsg) + m.Internal.Events.Report(5, "HistoricalRounds", + "Error", errMsg) continue } // Successfully retrieved roundRequests are sent to the Message @@ -144,8 +153,14 @@ func (m *Manager) processHistoricalRounds(comm historicalRoundsComms, quitCh <-c identity: roundRequests[i].identity, } m.lookupRoundMessages <- rl + rids = append(rids, roundInfo.ID) } + m.Internal.Events.Report(1, "HistoricalRounds", "Metrics", + fmt.Sprintf("Received %d historical rounds from"+ + " gateway %s: %v", len(response.Rounds), gwHost, + rids)) + //clear the buffer now that all have been checked roundRequests = make([]historicalRoundRequest, 0) } diff --git a/network/rounds/manager.go b/network/rounds/manager.go index 942e86319efe8ab05711b901360edbcd37865978..c695cd7a8c78be622769a4388a669eda5757b304 100644 --- a/network/rounds/manager.go +++ b/network/rounds/manager.go @@ -8,17 +8,16 @@ package rounds import ( - "fmt" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/network/gateway" "gitlab.com/elixxir/client/network/internal" "gitlab.com/elixxir/client/network/message" "gitlab.com/elixxir/client/stoppable" + "strconv" ) type Manager struct { params params.Rounds - internal.Internal sender *gateway.Sender @@ -48,14 +47,20 @@ func (m *Manager) StartProcessors() stoppable.Stoppable { //start the historical rounds thread historicalRoundsStopper := stoppable.NewSingle("ProcessHistoricalRounds") - go m.processHistoricalRounds(m.Comms, historicalRoundsStopper.Quit()) + go m.processHistoricalRounds(m.Comms, historicalRoundsStopper) multi.Add(historicalRoundsStopper) //start the message retrieval worker pool for i := uint(0); i < m.params.NumMessageRetrievalWorkers; i++ { - stopper := stoppable.NewSingle(fmt.Sprintf("Messager Retriever %v", i)) - go m.processMessageRetrieval(m.Comms, stopper.Quit()) + stopper := stoppable.NewSingle("Message Retriever " + strconv.Itoa(int(i))) + go m.processMessageRetrieval(m.Comms, stopper) multi.Add(stopper) } + + // Start the periodic unchecked round worker + stopper := stoppable.NewSingle("UncheckRound") + go m.processUncheckedRounds(m.params.UncheckRoundPeriod, backOffTable, stopper) + multi.Add(stopper) + return multi } diff --git a/network/rounds/remoteFilters.go b/network/rounds/remoteFilters.go index e2a225632c088a9082a63ed08367b90114f80a50..7a434be073e51118d22adae682c27d2e3ad83b50 100644 --- a/network/rounds/remoteFilters.go +++ b/network/rounds/remoteFilters.go @@ -1,3 +1,10 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + package rounds import ( diff --git a/network/rounds/remoteFilters_test.go b/network/rounds/remoteFilters_test.go new file mode 100644 index 0000000000000000000000000000000000000000..73ce8476be59b4c814cb1a3d42ec8d6d8791db3d --- /dev/null +++ b/network/rounds/remoteFilters_test.go @@ -0,0 +1,200 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package rounds + +import ( + bloom "gitlab.com/elixxir/bloomfilter" + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/client/storage/reception" + "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "reflect" + "testing" + "time" +) + +// Unit test NewRemoteFilter +func TestNewRemoteFilter(t *testing.T) { + bloomFilter := &mixmessages.ClientBloom{ + Filter: nil, + FirstRound: 0, + RoundRange: 0, + } + + rf := NewRemoteFilter(bloomFilter) + if !reflect.DeepEqual(rf.data, bloomFilter) { + t.Fatalf("NewRemoteFilter() error: "+ + "RemoteFilter not initialized as expected."+ + "\n\tExpected: %v\n\tReceived: %v", bloomFilter, rf.data) + } +} + +// Unit test GetFilter +func TestRemoteFilter_GetFilter(t *testing.T) { + testFilter, err := bloom.InitByParameters(interfaces.BloomFilterSize, + interfaces.BloomFilterHashes) + if err != nil { + t.Fatalf("GetFilter error: "+ + "Cannot initialize bloom filter for setup: %v", err) + } + + data, err := testFilter.MarshalBinary() + if err != nil { + t.Fatalf("GetFilter error: "+ + "Cannot marshal filter for setup: %v", err) + } + + bloomFilter := &mixmessages.ClientBloom{ + Filter: data, + FirstRound: 0, + RoundRange: 0, + } + + rf := NewRemoteFilter(bloomFilter) + retrievedFilter := rf.GetFilter() + if !reflect.DeepEqual(retrievedFilter, testFilter) { + t.Fatalf("GetFilter error: "+ + "Did not retrieve expected filter."+ + "\n\tExpected: %v\n\tReceived: %v", testFilter, retrievedFilter) + } +} + +// Unit test fro FirstRound and LastRound +func TestRemoteFilter_FirstLastRound(t *testing.T) { + firstRound := uint64(25) + roundRange := uint32(75) + bloomFilter := &mixmessages.ClientBloom{ + Filter: nil, + FirstRound: firstRound, + RoundRange: roundRange, + } + rf := NewRemoteFilter(bloomFilter) + + // Test FirstRound + receivedFirstRound := rf.FirstRound() + if receivedFirstRound != id.Round(firstRound) { + t.Fatalf("FirstRound error: "+ + "Did not receive expected round."+ + "\n\tExpected: %v\n\tReceived: %v", firstRound, receivedFirstRound) + } + + // Test LastRound + receivedLastRound := rf.LastRound() + if receivedLastRound != id.Round(firstRound+uint64(roundRange)) { + t.Fatalf("LastRound error: "+ + "Did not receive expected round."+ + "\n\tExpected: %v\n\tReceived: %v", receivedLastRound, firstRound+uint64(roundRange)) + } + +} + +// In bounds test +func TestValidFilterRange(t *testing.T) { + firstRound := uint64(25) + roundRange := uint32(75) + testFilter, err := bloom.InitByParameters(interfaces.BloomFilterSize, + interfaces.BloomFilterHashes) + if err != nil { + t.Fatalf("GetFilter error: "+ + "Cannot initialize bloom filter for setup: %v", err) + } + + data, err := testFilter.MarshalBinary() + if err != nil { + t.Fatalf("GetFilter error: "+ + "Cannot marshal filter for setup: %v", err) + } + + // Construct an in bounds value + expectedEphID := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} + requestGateway := id.NewIdFromString(ReturningGateway, id.Gateway, t) + iu := reception.IdentityUse{ + Identity: reception.Identity{ + EphId: expectedEphID, + Source: requestGateway, + StartValid: time.Now().Add(12 * time.Hour), + EndValid: time.Now().Add(24 * time.Hour), + }, + } + + bloomFilter := &mixmessages.ClientBloom{ + Filter: data, + FirstRound: firstRound, + RoundRange: roundRange, + } + + msg := &mixmessages.ClientBlooms{ + Period: int64(12 * time.Hour), + FirstTimestamp: time.Now().UnixNano(), + Filters: []*mixmessages.ClientBloom{bloomFilter}, + } + + start, end, outOfBounds := ValidFilterRange(iu, msg) + if outOfBounds { + t.Errorf("ValidFilterRange error: " + + "Range should not be out of bounds") + } + + if start != 0 && end != 1 { + t.Errorf("ValidFilterRange error: "+ + "Unexpected indices returned. "+ + "\n\tExpected start: %v\n\tReceived start: %v"+ + "\n\tExpected end: %v\n\tReceived end: %v", 0, start, 1, end) + } + +} + +// out of bounds test +func TestValidFilterRange_OutBounds(t *testing.T) { + firstRound := uint64(25) + roundRange := uint32(75) + testFilter, err := bloom.InitByParameters(interfaces.BloomFilterSize, + interfaces.BloomFilterHashes) + if err != nil { + t.Fatalf("GetFilter error: "+ + "Cannot initialize bloom filter for setup: %v", err) + } + + data, err := testFilter.MarshalBinary() + if err != nil { + t.Fatalf("GetFilter error: "+ + "Cannot marshal filter for setup: %v", err) + } + + // Construct an in bounds value + expectedEphID := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} + requestGateway := id.NewIdFromString(ReturningGateway, id.Gateway, t) + iu := reception.IdentityUse{ + Identity: reception.Identity{ + EphId: expectedEphID, + Source: requestGateway, + StartValid: time.Now().Add(-24 * time.Hour), + EndValid: time.Now().Add(-36 * time.Hour), + }, + } + + bloomFilter := &mixmessages.ClientBloom{ + Filter: data, + FirstRound: firstRound, + RoundRange: roundRange, + } + + msg := &mixmessages.ClientBlooms{ + Period: int64(12 * time.Hour), + FirstTimestamp: time.Now().UnixNano(), + Filters: []*mixmessages.ClientBloom{bloomFilter}, + } + + _, _, outOfBounds := ValidFilterRange(iu, msg) + if !outOfBounds { + t.Errorf("ValidFilterRange error: " + + "Range should be out of bounds") + } + +} diff --git a/network/rounds/retrieve.go b/network/rounds/retrieve.go index 0d90355c64bb2adaff003442737e09468a5b05f8..caf5ce0e70d2b688a2045feb8c0dd21bcecdee6a 100644 --- a/network/rounds/retrieve.go +++ b/network/rounds/retrieve.go @@ -8,14 +8,18 @@ package rounds import ( + "encoding/binary" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/network/message" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage/reception" pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/crypto/shuffle" "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/primitives/id" + "time" ) type messageRetrievalComms interface { @@ -29,20 +33,27 @@ type roundLookup struct { identity reception.IdentityUse } -const noRoundError = "does not have round" +const noRoundError = "does not have round %d" // processMessageRetrieval received a roundLookup request and pings the gateways // of that round for messages for the requested identity in the roundLookup func (m *Manager) processMessageRetrieval(comms messageRetrievalComms, - quitCh <-chan struct{}) { + stop *stoppable.Single) { - done := false - for !done { + for { select { - case <-quitCh: - done = true + case <-stop.Quit(): + stop.ToStopped() + return case rl := <-m.lookupRoundMessages: ri := rl.roundInfo + jww.DEBUG.Printf("Checking for messages in round %d", ri.ID) + err := m.Session.UncheckedRounds().AddRound(rl.roundInfo, + rl.identity.EphId, rl.identity.Source) + if err != nil { + jww.ERROR.Printf("Could not add round %d in unchecked rounds store: %v", + rl.roundInfo.ID, err) + } // Convert gateways in round to proper ID format gwIds := make([]*id.ID, len(ri.Topology)) @@ -54,21 +65,70 @@ func (m *Manager) processMessageRetrieval(comms messageRetrievalComms, gwId.SetType(id.Gateway) gwIds[i] = gwId } - - // Attempt to request for this gateway - bundle, err := m.getMessagesFromGateway(id.Round(ri.ID), rl.identity, comms, gwIds) - - // After trying all gateways, if none returned we mark the round as a - // failure and print out the last error + // Target the last node in the team first because it has + // messages first, randomize other members of the team + var rndBytes [32]byte + stream := m.Rng.GetStream() + _, err = stream.Read(rndBytes[:]) + stream.Close() if err != nil { - jww.ERROR.Printf("Failed to get pickup round %d "+ + jww.FATAL.Panicf("Failed to randomize shuffle in round %d "+ "from all gateways (%v): %s", id.Round(ri.ID), gwIds, err) } + gwIds[0], gwIds[len(gwIds)-1] = gwIds[len(gwIds)-1], gwIds[0] + shuffle.ShuffleSwap(rndBytes[:], len(gwIds)-1, func(i, j int) { + gwIds[i+1], gwIds[j+1] = gwIds[j+1], gwIds[i+1] + }) + + // If ForceMessagePickupRetry, we are forcing processUncheckedRounds by + // randomly not picking up messages (FOR INTEGRATION TEST). Only done if + // round has not been ignored before + var bundle message.Bundle + if m.params.ForceMessagePickupRetry { + bundle, err = m.forceMessagePickupRetry(ri, rl, comms, gwIds, stop) + + // Exit if the thread has been stopped + if stoppable.CheckErr(err) { + jww.ERROR.Print(err) + continue + } + if err != nil { + jww.ERROR.Printf("Failed to get pickup round %d "+ + "from all gateways (%v): %s", + id.Round(ri.ID), gwIds, err) + } + } else { + // Attempt to request for this gateway + bundle, err = m.getMessagesFromGateway(id.Round(ri.ID), rl.identity, comms, gwIds, stop) + + // Exit if the thread has been stopped + if stoppable.CheckErr(err) { + jww.ERROR.Print(err) + continue + } + + // After trying all gateways, if none returned we mark the round as a + // failure and print out the last error + if err != nil { + jww.ERROR.Printf("Failed to get pickup round %d "+ + "from all gateways (%v): %s", + id.Round(ri.ID), gwIds, err) + } + + } if len(bundle.Messages) != 0 { + jww.DEBUG.Printf("Removing round %d from unchecked store", ri.ID) + err = m.Session.UncheckedRounds().Remove(id.Round(ri.ID)) + if err != nil { + jww.ERROR.Printf("Could not remove round %d "+ + "from unchecked rounds store: %v", ri.ID, err) + } + // If successful and there are messages, we send them to another thread bundle.Identity = rl.identity + bundle.RoundInfo = rl.roundInfo m.messageBundles <- bundle } @@ -78,11 +138,12 @@ func (m *Manager) processMessageRetrieval(comms messageRetrievalComms, // getMessagesFromGateway attempts to get messages from their assigned // gateway host in the round specified. If successful -func (m *Manager) getMessagesFromGateway(roundID id.Round, identity reception.IdentityUse, - comms messageRetrievalComms, gwIds []*id.ID) (message.Bundle, error) { - +func (m *Manager) getMessagesFromGateway(roundID id.Round, + identity reception.IdentityUse, comms messageRetrievalComms, gwIds []*id.ID, + stop *stoppable.Single) (message.Bundle, error) { + start := time.Now() // Send to the gateways using backup proxies - result, err := m.sender.SendToPreferred(gwIds, func(host *connect.Host, target *id.ID) (interface{}, error) { + result, err := m.sender.SendToPreferred(gwIds, func(host *connect.Host, target *id.ID) (interface{}, bool, error) { jww.DEBUG.Printf("Trying to get messages for round %v for ephemeralID %d (%v) "+ "via Gateway: %s", roundID, identity.EphId.Int64(), identity.Source.String(), host.GetId()) @@ -96,12 +157,13 @@ func (m *Manager) getMessagesFromGateway(roundID id.Round, identity reception.Id // If the gateway doesnt have the round, return an error msgResp, err := comms.RequestMessages(host, msgReq) if err == nil && !msgResp.GetHasRound() { - return message.Bundle{}, errors.Errorf(noRoundError) + jww.INFO.Printf("No round error for round %d received from %s", roundID, target) + return message.Bundle{}, false, errors.Errorf(noRoundError, roundID) } - return msgResp, err - }) - + return msgResp, false, err + }, stop) + jww.INFO.Printf("Received message for round %d, processing...", roundID) // Fail the round if an error occurs so it can be tried again later if err != nil { return message.Bundle{}, errors.WithMessagef(err, "Failed to "+ @@ -120,8 +182,8 @@ func (m *Manager) getMessagesFromGateway(roundID id.Round, identity reception.Id return message.Bundle{}, nil } - jww.INFO.Printf("Received %d messages in Round %v for %d (%s)", - len(msgs), roundID, identity.EphId.Int64(), identity.Source) + jww.INFO.Printf("Received %d messages in Round %v for %d (%s) in %s", + len(msgs), roundID, identity.EphId.Int64(), identity.Source, time.Now().Sub(start)) //build the bundle of messages to send to the message processor bundle := message.Bundle{ @@ -142,3 +204,32 @@ func (m *Manager) getMessagesFromGateway(roundID id.Round, identity reception.Id return bundle, nil } + +// Helper function which forces processUncheckedRounds by randomly +// not looking up messages +func (m *Manager) forceMessagePickupRetry(ri *pb.RoundInfo, rl roundLookup, + comms messageRetrievalComms, gwIds []*id.ID, + stop *stoppable.Single) (bundle message.Bundle, err error) { + rnd, _ := m.Session.UncheckedRounds().GetRound(id.Round(ri.ID)) + if rnd.NumChecks == 0 { + // Flip a coin to determine whether to pick up message + stream := m.Rng.GetStream() + defer stream.Close() + b := make([]byte, 8) + _, err = stream.Read(b) + if err != nil { + jww.FATAL.Panic(err.Error()) + } + result := binary.BigEndian.Uint64(b) + if result%2 == 0 { + jww.INFO.Printf("Forcing a message pickup retry for round %d", ri.ID) + // Do not call get message, leaving the round to be picked up + // in unchecked round scheduler process + return + } + + } + + // Attempt to request for this gateway + return m.getMessagesFromGateway(id.Round(ri.ID), rl.identity, comms, gwIds, stop) +} diff --git a/network/rounds/retrieve_test.go b/network/rounds/retrieve_test.go index abd79533dd9189cdef93f8f37aba4b8ad810b8d2..856fa05fa3e9b911f6180b0ed368031993c907a1 100644 --- a/network/rounds/retrieve_test.go +++ b/network/rounds/retrieve_test.go @@ -10,6 +10,7 @@ import ( "bytes" "gitlab.com/elixxir/client/network/gateway" "gitlab.com/elixxir/client/network/message" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage/reception" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/crypto/fastRNG" @@ -28,18 +29,22 @@ func TestManager_ProcessMessageRetrieval(t *testing.T) { testManager := newManager(t) roundId := id.Round(5) mockComms := &mockMessageRetrievalComms{testingSignature: t} - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") testNdf := getNDF() nodeId := id.NewIdFromString(ReturningGateway, id.Node, &testing.T{}) gwId := nodeId.DeepCopy() gwId.SetType(id.Gateway) testNdf.Gateways = []ndf.Gateway{{ID: gwId.Marshal()}} + testManager.Rng = fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG) p := gateway.DefaultPoolParams() p.MaxPoolSize = 1 - testManager.sender, _ = gateway.NewSender(p, - fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + var err error + testManager.sender, err = gateway.NewSender(p, testManager.Rng, testNdf, mockComms, testManager.Session, nil) + if err != nil { + t.Errorf(err.Error()) + } // Create a local channel so reception is possible (testManager.messageBundles is // send only via newManager call above) @@ -47,7 +52,7 @@ func TestManager_ProcessMessageRetrieval(t *testing.T) { testManager.messageBundles = messageBundleChan // Initialize the message retrieval - go testManager.processMessageRetrieval(mockComms, quitChan) + go testManager.processMessageRetrieval(mockComms, stop) // Construct expected values for checking expectedEphID := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} @@ -88,8 +93,10 @@ func TestManager_ProcessMessageRetrieval(t *testing.T) { testBundle = <-messageBundleChan // Close the process - quitChan <- struct{}{} - + err := stop.Close() + if err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } }() // Ensure bundle received and has expected values @@ -127,11 +134,12 @@ func TestManager_ProcessMessageRetrieval_NoRound(t *testing.T) { gwId := nodeId.DeepCopy() gwId.SetType(id.Gateway) testNdf.Gateways = []ndf.Gateway{{ID: gwId.Marshal()}} + testManager.Rng = fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG) testManager.sender, _ = gateway.NewSender(p, - fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + testManager.Rng, testNdf, mockComms, testManager.Session, nil) - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") // Create a local channel so reception is possible (testManager.messageBundles is // send only via newManager call above) @@ -139,7 +147,7 @@ func TestManager_ProcessMessageRetrieval_NoRound(t *testing.T) { testManager.messageBundles = messageBundleChan // Initialize the message retrieval - go testManager.processMessageRetrieval(mockComms, quitChan) + go testManager.processMessageRetrieval(mockComms, stop) expectedEphID := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} @@ -178,8 +186,9 @@ func TestManager_ProcessMessageRetrieval_NoRound(t *testing.T) { testBundle = <-messageBundleChan // Close the process - quitChan <- struct{}{} - + if err := stop.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } }() time.Sleep(2 * time.Second) @@ -197,17 +206,18 @@ func TestManager_ProcessMessageRetrieval_FalsePositive(t *testing.T) { testManager := newManager(t) roundId := id.Round(5) mockComms := &mockMessageRetrievalComms{testingSignature: t} - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") testNdf := getNDF() nodeId := id.NewIdFromString(FalsePositive, id.Node, &testing.T{}) gwId := nodeId.DeepCopy() gwId.SetType(id.Gateway) testNdf.Gateways = []ndf.Gateway{{ID: gwId.Marshal()}} + testManager.Rng = fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG) p := gateway.DefaultPoolParams() p.MaxPoolSize = 1 testManager.sender, _ = gateway.NewSender(p, - fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + testManager.Rng, testNdf, mockComms, testManager.Session, nil) // Create a local channel so reception is possible (testManager.messageBundles is @@ -216,7 +226,7 @@ func TestManager_ProcessMessageRetrieval_FalsePositive(t *testing.T) { testManager.messageBundles = messageBundleChan // Initialize the message retrieval - go testManager.processMessageRetrieval(mockComms, quitChan) + go testManager.processMessageRetrieval(mockComms, stop) // Construct expected values for checking expectedEphID := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} @@ -257,8 +267,9 @@ func TestManager_ProcessMessageRetrieval_FalsePositive(t *testing.T) { testBundle = <-messageBundleChan // Close the process - quitChan <- struct{}{} - + if err := stop.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } }() // Ensure no bundle was received due to false positive test @@ -276,7 +287,7 @@ func TestManager_ProcessMessageRetrieval_Quit(t *testing.T) { testManager := newManager(t) roundId := id.Round(5) mockComms := &mockMessageRetrievalComms{testingSignature: t} - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") // Create a local channel so reception is possible (testManager.messageBundles is // send only via newManager call above) @@ -284,10 +295,16 @@ func TestManager_ProcessMessageRetrieval_Quit(t *testing.T) { testManager.messageBundles = messageBundleChan // Initialize the message retrieval - go testManager.processMessageRetrieval(mockComms, quitChan) + go testManager.processMessageRetrieval(mockComms, stop) // Close the process early, before any logic below can be completed - quitChan <- struct{}{} + if err := stop.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } + + if err := stoppable.WaitForStopped(stop, 300*time.Millisecond); err != nil { + t.Fatalf("Failed to stop stoppable: %+v", err) + } // Construct expected values for checking expectedEphID := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} @@ -342,17 +359,18 @@ func TestManager_ProcessMessageRetrieval_MultipleGateways(t *testing.T) { testManager := newManager(t) roundId := id.Round(5) mockComms := &mockMessageRetrievalComms{testingSignature: t} - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") testNdf := getNDF() nodeId := id.NewIdFromString(ReturningGateway, id.Node, &testing.T{}) gwId := nodeId.DeepCopy() gwId.SetType(id.Gateway) testNdf.Gateways = []ndf.Gateway{{ID: gwId.Marshal()}} + testManager.Rng = fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG) p := gateway.DefaultPoolParams() p.MaxPoolSize = 1 testManager.sender, _ = gateway.NewSender(p, - fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + testManager.Rng, testNdf, mockComms, testManager.Session, nil) // Create a local channel so reception is possible (testManager.messageBundles is @@ -361,7 +379,7 @@ func TestManager_ProcessMessageRetrieval_MultipleGateways(t *testing.T) { testManager.messageBundles = messageBundleChan // Initialize the message retrieval - go testManager.processMessageRetrieval(mockComms, quitChan) + go testManager.processMessageRetrieval(mockComms, stop) // Construct expected values for checking expectedEphID := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} @@ -403,8 +421,9 @@ func TestManager_ProcessMessageRetrieval_MultipleGateways(t *testing.T) { testBundle = <-messageBundleChan // Close the process - quitChan <- struct{}{} - + if err := stop.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } }() // Ensure that expected bundle is still received from happy comm diff --git a/network/rounds/unchecked.go b/network/rounds/unchecked.go new file mode 100644 index 0000000000000000000000000000000000000000..e62bff0c6885d71080b4544cf6602ec684fa2a9a --- /dev/null +++ b/network/rounds/unchecked.go @@ -0,0 +1,101 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package rounds + +import ( + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/stoppable" + "gitlab.com/elixxir/client/storage/reception" + "gitlab.com/xx_network/primitives/netTime" + "time" +) + +// Constants for message retrieval backoff delays +const ( + tryZero = 10 * time.Second + tryOne = 30 * time.Second + tryTwo = 5 * time.Minute + tryThree = 30 * time.Minute + tryFour = 3 * time.Hour + tryFive = 12 * time.Hour + trySix = 24 * time.Hour + // Amount of tries past which the + // backoff will not increase + cappedTries = 7 +) + +var backOffTable = [cappedTries]time.Duration{tryZero, tryOne, tryTwo, tryThree, tryFour, tryFive, trySix} + +// processUncheckedRounds will (periodically) check every checkInterval +// for rounds that failed message retrieval in processMessageRetrieval. +// Rounds will have a backoff duration in which they will be tried again. +// If a round is found to be due on a periodical check, the round is sent +// back to processMessageRetrieval. +func (m *Manager) processUncheckedRounds(checkInterval time.Duration, backoffTable [cappedTries]time.Duration, + stop *stoppable.Single) { + ticker := time.NewTicker(checkInterval) + uncheckedRoundStore := m.Session.UncheckedRounds() + for { + select { + case <-stop.Quit(): + stop.ToStopped() + return + + case <-ticker.C: + // Pull and iterate through uncheckedRound list + roundList := m.Session.UncheckedRounds().GetList() + for rid, rnd := range roundList { + // If this round is due for a round check, send the round over + // to the retrieval thread. If not due, check next round. + if isRoundCheckDue(rnd.NumChecks, rnd.LastCheck, backoffTable) { + jww.INFO.Printf("Round %d due for a message lookup, retrying...", rid) + // Construct roundLookup object to send + rl := roundLookup{ + roundInfo: rnd.Info, + identity: reception.IdentityUse{ + Identity: reception.Identity{ + EphId: rnd.EpdId, + Source: rnd.Source, + }, + }, + } + + // Send to processMessageRetrieval + select { + case m.lookupRoundMessages <- rl: + case <-time.After(1 * time.Second): + jww.WARN.Printf("Timing out, not retrying round %d", rl.roundInfo.ID) + } + + // Update the state of the round for next look-up (if needed) + err := uncheckedRoundStore.IncrementCheck(rid) + if err != nil { + jww.ERROR.Printf("processUncheckedRounds error: Could not "+ + "increment check attempts for round %d: %v", rid, err) + } + + } + + } + } + } +} + +// isRoundCheckDue given the amount of tries and the timestamp the round +// was stored, determines whether this round is due for another check. +// Returns true if a new check is due +func isRoundCheckDue(tries uint64, ts time.Time, backoffTable [cappedTries]time.Duration) bool { + now := netTime.Now() + + if tries > cappedTries { + tries = cappedTries + } + roundCheckTime := ts.Add(backoffTable[tries]) + + return now.After(roundCheckTime) +} diff --git a/network/rounds/unchecked_test.go b/network/rounds/unchecked_test.go new file mode 100644 index 0000000000000000000000000000000000000000..980ca1e6157a583cc239cab4332e358c5b04e84a --- /dev/null +++ b/network/rounds/unchecked_test.go @@ -0,0 +1,104 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package rounds + +import ( + "gitlab.com/elixxir/client/network/gateway" + "gitlab.com/elixxir/client/network/message" + "gitlab.com/elixxir/client/stoppable" + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "gitlab.com/xx_network/primitives/ndf" + "reflect" + "testing" + "time" +) + +// Happy path +func TestUncheckedRoundScheduler(t *testing.T) { + // General initializations + testManager := newManager(t) + roundId := id.Round(5) + mockComms := &mockMessageRetrievalComms{testingSignature: t} + stop1 := stoppable.NewSingle("singleStoppable1") + stop2 := stoppable.NewSingle("singleStoppable2") + testNdf := getNDF() + nodeId := id.NewIdFromString(ReturningGateway, id.Node, &testing.T{}) + gwId := nodeId.DeepCopy() + gwId.SetType(id.Gateway) + testNdf.Gateways = []ndf.Gateway{{ID: gwId.Marshal()}} + p := gateway.DefaultPoolParams() + p.MaxPoolSize = 1 + testManager.sender, _ = gateway.NewSender(p, + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + testNdf, mockComms, testManager.Session, nil) + + // Create a local channel so reception is possible (testManager.messageBundles is + // send only via newManager call above) + messageBundleChan := make(chan message.Bundle) + testManager.messageBundles = messageBundleChan + + testBackoffTable := newTestBackoffTable(t) + checkInterval := 250 * time.Millisecond + // Initialize the message retrieval + go testManager.processMessageRetrieval(mockComms, stop1) + go testManager.processUncheckedRounds(checkInterval, testBackoffTable, stop2) + + requestGateway := id.NewIdFromString(ReturningGateway, id.Gateway, t) + + // Construct expected values for checking + expectedEphID := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} + idList := [][]byte{requestGateway.Bytes()} + roundInfo := &pb.RoundInfo{ + ID: uint64(roundId), + Topology: idList, + } + + // Add round ot check + err := testManager.Session.UncheckedRounds().AddRound(roundInfo, expectedEphID, requestGateway) + if err != nil { + t.Fatalf("Could not add round to session: %v", err) + } + + var testBundle message.Bundle + go func() { + // Receive the bundle over the channel + time.Sleep(1 * time.Second) + testBundle = <-messageBundleChan + + // Close the process + if err := stop1.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } + if err := stop2.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } + + }() + + // Ensure bundle received and has expected values + time.Sleep(2 * time.Second) + if reflect.DeepEqual(testBundle, message.Bundle{}) { + t.Fatalf("Did not receive a message bundle over the channel") + } + + if testBundle.Identity.EphId.Int64() != expectedEphID.Int64() { + t.Errorf("Unexpected ephemeral ID in bundle."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", expectedEphID, testBundle.Identity.EphId) + } + + _, exists := testManager.Session.UncheckedRounds().GetRound(roundId) + if exists { + t.Fatalf("Expected round %d to be removed after being processed", roundId) + } + +} diff --git a/network/rounds/utils_test.go b/network/rounds/utils_test.go index d352078dcd1219b188c8c7fde0b807748d8c3521..e4c41bbf3fe5c69e76ac2aeda3d8766a7f4aaf82 100644 --- a/network/rounds/utils_test.go +++ b/network/rounds/utils_test.go @@ -8,14 +8,18 @@ package rounds import ( "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/network/internal" "gitlab.com/elixxir/client/network/message" "gitlab.com/elixxir/client/storage" pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/xx_network/comms/connect" + "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/ndf" "testing" + "time" ) func newManager(face interface{}) *Manager { @@ -27,6 +31,7 @@ func newManager(face interface{}) *Manager { Internal: internal.Internal{ Session: sess1, TransmissionID: sess1.GetUser().TransmissionID, + Rng: fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), }, } return testManager @@ -102,6 +107,23 @@ func (mmrc *mockMessageRetrievalComms) RequestMessages(host *connect.Host, return nil, nil } +func newTestBackoffTable(face interface{}) [cappedTries]time.Duration { + switch face.(type) { + case *testing.T, *testing.M, *testing.B, *testing.PB: + break + default: + jww.FATAL.Panicf("newTestBackoffTable is restricted to testing only. Got %T", face) + } + + var backoff [cappedTries]time.Duration + for i := 0; i < cappedTries; i++ { + backoff[uint64(i)] = 1 * time.Millisecond + } + + return backoff + +} + func getNDF() *ndf.NetworkDefinition { return &ndf.NetworkDefinition{ E2E: ndf.Group{ diff --git a/network/send.go b/network/send.go index d70a0c6661c2ee6f4c40f313219baa7cbb86fd52..8fff3ac152e65a55822d6e824aba7f7f6d4f09eb 100644 --- a/network/send.go +++ b/network/send.go @@ -12,10 +12,12 @@ import ( jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/crypto/e2e" "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" + "time" ) // SendCMIX sends a "raw" CMIX message payload to the provided @@ -28,7 +30,16 @@ func (m *manager) SendCMIX(msg format.Message, recipient *id.ID, param params.CM "network is not healthy") } - return m.message.SendCMIX(m.GetSender(), msg, recipient, param) + return m.message.SendCMIX(m.GetSender(), msg, recipient, param, nil) +} + +// SendManyCMIX sends many "raw" CMIX message payloads to each of the +// provided recipients. Used for group chat functionality. Returns the +// round ID of the round the payload was sent or an error if it fails. +func (m *manager) SendManyCMIX(messages map[id.ID]format.Message, + p params.CMIX) (id.Round, []ephemeral.Id, error) { + + return m.message.SendManyCMIX(m.sender, messages, p) } // SendUnsafe sends an unencrypted payload to the provided recipient @@ -52,13 +63,13 @@ func (m *manager) SendUnsafe(msg message.Send, param params.Unsafe) ([]id.Round, // SendE2E sends an end-to-end payload to the provided recipient with // the provided msgType. Returns the list of rounds in which parts of // the message were sent or an error if it fails. -func (m *manager) SendE2E(msg message.Send, e2eP params.E2E) ( - []id.Round, e2e.MessageID, error) { +func (m *manager) SendE2E(msg message.Send, e2eP params.E2E, stop *stoppable.Single) ( + []id.Round, e2e.MessageID, time.Time, error) { if !m.Health.IsHealthy() { - return nil, e2e.MessageID{}, errors.New("Cannot send e2e " + + return nil, e2e.MessageID{}, time.Time{}, errors.New("Cannot send e2e " + "message when the network is not healthy") } - return m.message.SendE2E(msg, e2eP) + return m.message.SendE2E(msg, e2eP, stop) } diff --git a/permissioning/permissioning.go b/registration/permissioning.go similarity index 59% rename from permissioning/permissioning.go rename to registration/permissioning.go index ebe00b3c3d41c3c9fcaf844e65d87fc6033dd829..917692eefc0e53c21220665c78494a0d0c7eb3b2 100644 --- a/permissioning/permissioning.go +++ b/registration/permissioning.go @@ -5,7 +5,7 @@ // LICENSE file // /////////////////////////////////////////////////////////////////////////////// -package permissioning +package registration import ( "github.com/pkg/errors" @@ -13,28 +13,38 @@ import ( "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/ndf" + "math" + "time" ) -type Permissioning struct { +type Registration struct { host *connect.Host comms *client.Comms } -func Init(comms *client.Comms, def *ndf.NetworkDefinition) (*Permissioning, error) { +func Init(comms *client.Comms, def *ndf.NetworkDefinition) (*Registration, error) { - perm := Permissioning{ + perm := Registration{ host: nil, comms: comms, } var err error - //add the permissioning host to comms + //add the registration host to comms hParam := connect.GetDefaultHostParams() hParam.AuthEnabled = false - - perm.host, err = comms.AddHost(&id.Permissioning, def.Registration.Address, + // Client will not send KeepAlive packets + hParam.KaClientOpts.Time = time.Duration(math.MaxInt64) + hParam.MaxRetries = 3 + perm.host, err = comms.AddHost(&id.ClientRegistration, def.Registration.ClientRegistrationAddress, []byte(def.Registration.TlsCertificate), hParam) + if err != nil { + return nil, errors.WithMessage(err, "failed to create registration") + } + + _, err = comms.AddHost(&id.Permissioning, def.Registration.Address, // We need to add this for round updates to work + []byte(def.Registration.TlsCertificate), hParam) if err != nil { return nil, errors.WithMessage(err, "failed to create permissioning") } diff --git a/permissioning/permissioning_test.go b/registration/permissioning_test.go similarity index 84% rename from permissioning/permissioning_test.go rename to registration/permissioning_test.go index 7f691f3a419f5a43109206da5a802b3bb2029be3..56083a5dd302c22872d1e172c67124bb56199093 100644 --- a/permissioning/permissioning_test.go +++ b/registration/permissioning_test.go @@ -5,7 +5,7 @@ // LICENSE file // /////////////////////////////////////////////////////////////////////////////// -package permissioning +package registration import ( "gitlab.com/elixxir/comms/client" @@ -14,7 +14,7 @@ import ( "testing" ) -// Init should create a valid Permissioning communications struct +// Init should create a valid Registration communications struct func TestInit(t *testing.T) { // Create dummy comms and ndf comms, err := client.NewClientComms(id.NewIdFromUInt(100, id.User, t), nil, nil, nil) @@ -22,7 +22,9 @@ func TestInit(t *testing.T) { t.Fatal(err) } def := &ndf.NetworkDefinition{ - Registration: ndf.Registration{}, + Registration: ndf.Registration{ + EllipticPubKey: "MqaJJ3GjFisNRM6LRedRnooi14gepMaQxyWctXVU", + }, } reg, err := Init(comms, def) if err != nil { diff --git a/permissioning/register.go b/registration/register.go similarity index 77% rename from permissioning/register.go rename to registration/register.go index 1a64e54798560b270ab0d62267f7b2704a2c8ae1..5a21fd97267ab0ae4880ceb9a996ddbdfb09dbd8 100644 --- a/permissioning/register.go +++ b/registration/register.go @@ -5,7 +5,7 @@ // LICENSE file // /////////////////////////////////////////////////////////////////////////////// -package permissioning +package registration import ( "github.com/pkg/errors" @@ -14,7 +14,8 @@ import ( "gitlab.com/xx_network/crypto/signature/rsa" ) -func (perm *Permissioning) Register(transmissionPublicKey, receptionPublicKey *rsa.PublicKey, registrationCode string) ([]byte, []byte, error) { +func (perm *Registration) Register(transmissionPublicKey, receptionPublicKey *rsa.PublicKey, + registrationCode string) ([]byte, []byte, int64, error) { return register(perm.comms, perm.host, transmissionPublicKey, receptionPublicKey, registrationCode) } @@ -26,7 +27,7 @@ type registrationMessageSender interface { //register registers the user with optional registration code // Returns an error if registration fails. func register(comms registrationMessageSender, host *connect.Host, - transmissionPublicKey, receptionPublicKey *rsa.PublicKey, registrationCode string) ([]byte, []byte, error) { + transmissionPublicKey, receptionPublicKey *rsa.PublicKey, registrationCode string) ([]byte, []byte, int64, error) { response, err := comms. SendRegistrationMessage(host, @@ -37,10 +38,12 @@ func register(comms registrationMessageSender, host *connect.Host, }) if err != nil { err = errors.Wrap(err, "sendRegistrationMessage: Unable to contact Identity Server!") - return nil, nil, err + return nil, nil, 0, err } if response.Error != "" { - return nil, nil, errors.Errorf("sendRegistrationMessage: error handling message: %s", response.Error) + return nil, nil, 0, errors.Errorf("sendRegistrationMessage: error handling message: %s", response.Error) } - return response.ClientSignedByServer.Signature, response.ClientReceptionSignedByServer.Signature, nil + + return response.ClientSignedByServer.Signature, + response.ClientReceptionSignedByServer.Signature, response.Timestamp, nil } diff --git a/permissioning/register_test.go b/registration/register_test.go similarity index 87% rename from permissioning/register_test.go rename to registration/register_test.go index 52dcb46e948b42fdd54a8f45ca6e2f751bf8a85e..9840187ce77010144c0c0ccf8674f62a2c766354 100644 --- a/permissioning/register_test.go +++ b/registration/register_test.go @@ -5,7 +5,7 @@ // LICENSE file // /////////////////////////////////////////////////////////////////////////////// -package permissioning +package registration import ( "github.com/pkg/errors" @@ -51,7 +51,7 @@ func (s *MockRegistrationSender) GetHost(*id.ID) (*connect.Host, bool) { } // Shows that we get expected result from happy path -// Shows that permissioning gets RPCs with the correct parameters +// Shows that registration gets RPCs with the correct parameters func TestRegisterWithPermissioning(t *testing.T) { rng := csprng.NewSystemRNG() key, err := rsa.GenerateKey(rng, 256) @@ -68,7 +68,7 @@ func TestRegisterWithPermissioning(t *testing.T) { } regCode := "flooble doodle" - sig1, sig2, err := register(&sender, sender.getHost, key.GetPublic(), key.GetPublic(), regCode) + sig1, sig2, _, err := register(&sender, sender.getHost, key.GetPublic(), key.GetPublic(), regCode) if err != nil { t.Error(err) } @@ -94,7 +94,7 @@ func TestRegisterWithPermissioning(t *testing.T) { } } -// Shows that returning an error from the permissioning server results in an +// Shows that returning an error from the registration server results in an // error from register func TestRegisterWithPermissioning_ResponseErr(t *testing.T) { rng := csprng.NewSystemRNG() @@ -104,10 +104,10 @@ func TestRegisterWithPermissioning_ResponseErr(t *testing.T) { } var sender MockRegistrationSender sender.succeedGetHost = true - sender.errInReply = "failure occurred on permissioning" - _, _, err = register(&sender, nil, key.GetPublic(), key.GetPublic(), "") + sender.errInReply = "failure occurred on registration" + _, _, _, err = register(&sender, nil, key.GetPublic(), key.GetPublic(), "") if err == nil { - t.Error("no error if registration fails on permissioning") + t.Error("no error if registration fails on registration") } } @@ -122,7 +122,7 @@ func TestRegisterWithPermissioning_ConnectionErr(t *testing.T) { var sender MockRegistrationSender sender.succeedGetHost = true sender.errSendRegistration = errors.New("connection problem") - _, _, err = register(&sender, nil, key.GetPublic(), key.GetPublic(), "") + _, _, _, err = register(&sender, nil, key.GetPublic(), key.GetPublic(), "") if err == nil { t.Error("no error if e.g. context deadline exceeded") } diff --git a/single/manager.go b/single/manager.go index 78481829180017f242ca45e43b2bacc4175cf4dd..9562137bbe4dbb0ad3fa9885a241d79d9825a5ea 100644 --- a/single/manager.go +++ b/single/manager.go @@ -69,25 +69,25 @@ func newManager(client *api.Client, reception *reception.Store) *Manager { // StartProcesses starts the process of receiving single-use transmissions and // replies. -func (m *Manager) StartProcesses() stoppable.Stoppable { +func (m *Manager) StartProcesses() (stoppable.Stoppable, error) { // Start waiting for single-use transmission transmissionStop := stoppable.NewSingle(singleUseTransmission) transmissionChan := make(chan message.Receive, rawMessageBuffSize) m.swb.RegisterChannel(singleUseReceiveTransmission, &id.ID{}, message.Raw, transmissionChan) - go m.receiveTransmissionHandler(transmissionChan, transmissionStop.Quit()) + go m.receiveTransmissionHandler(transmissionChan, transmissionStop) // Start waiting for single-use response responseStop := stoppable.NewSingle(singleUseResponse) responseChan := make(chan message.Receive, rawMessageBuffSize) m.swb.RegisterChannel(singleUseReceiveResponse, &id.ID{}, message.Raw, responseChan) - go m.receiveResponseHandler(responseChan, responseStop.Quit()) + go m.receiveResponseHandler(responseChan, responseStop) // Create a multi stoppable singleUseMulti := stoppable.NewMulti(singleUseStop) singleUseMulti.Add(transmissionStop) singleUseMulti.Add(responseStop) - return singleUseMulti + return singleUseMulti, nil } // RegisterCallback registers a callback for received messages. diff --git a/single/manager_test.go b/single/manager_test.go index 2ce788f7b86ba44fd61a2aaa68814d1d62a49a02..ac5dd4253820f4fa8f2f50d5cdba079f0c94147c 100644 --- a/single/manager_test.go +++ b/single/manager_test.go @@ -90,7 +90,7 @@ func TestManager_StartProcesses(t *testing.T) { m.callbackMap.registerCallback(tag, callback) - _ = m.StartProcesses() + _, _ = m.StartProcesses() m.swb.(*switchboard.Switchboard).Speak(receiveMsg) timer := time.NewTimer(50 * time.Millisecond) @@ -176,12 +176,12 @@ func TestManager_StartProcesses_Stop(t *testing.T) { m.callbackMap.registerCallback(tag, callback) - stop := m.StartProcesses() + stop, _ := m.StartProcesses() if !stop.IsRunning() { t.Error("Stoppable is not running.") } - err = stop.Close(1 * time.Millisecond) + err = stop.Close() if err != nil { t.Errorf("Failed to close: %+v", err) } @@ -283,8 +283,8 @@ func (tnm *testNetworkManager) GetMsg(i int) format.Message { return tnm.msgs[i] } -func (tnm *testNetworkManager) SendE2E(_ message.Send, _ params.E2E) ([]id.Round, e2e.MessageID, error) { - return nil, [32]byte{}, nil +func (tnm *testNetworkManager) SendE2E(message.Send, params.E2E, *stoppable.Single) ([]id.Round, e2e.MessageID, time.Time, error) { + return nil, e2e.MessageID{}, time.Time{}, nil } func (tnm *testNetworkManager) SendUnsafe(_ message.Send, _ params.Unsafe) ([]id.Round, error) { @@ -306,10 +306,34 @@ func (tnm *testNetworkManager) SendCMIX(msg format.Message, _ *id.ID, _ params.C return id.Round(rand.Uint64()), ephemeral.Id{}, nil } +func (tnm *testNetworkManager) SendManyCMIX(messages map[id.ID]format.Message, p params.CMIX) (id.Round, []ephemeral.Id, error) { + if tnm.cmixTimeout != 0 { + time.Sleep(tnm.cmixTimeout) + } else if tnm.cmixErr { + return 0, []ephemeral.Id{}, errors.New("sendCMIX error") + } + + tnm.Lock() + defer tnm.Unlock() + + for _, msg := range messages { + tnm.msgs = append(tnm.msgs, msg) + } + + return id.Round(rand.Uint64()), []ephemeral.Id{}, nil +} + func (tnm *testNetworkManager) GetInstance() *network.Instance { return tnm.instance } +type dummyEventMgr struct{} + +func (d *dummyEventMgr) Report(p int, a, b, c string) {} +func (t *testNetworkManager) GetEventManager() interfaces.EventManager { + return &dummyEventMgr{} +} + func (tnm *testNetworkManager) GetHealthTracker() interfaces.HealthTracker { return nil } @@ -324,10 +348,19 @@ func (tnm *testNetworkManager) InProgressRegistrations() int { return 0 } -func (t *testNetworkManager) GetSender() *gateway.Sender { +func (tnm *testNetworkManager) GetSender() *gateway.Sender { return nil } +func (tnm *testNetworkManager) GetAddressSize() uint8 { return 16 } + +func (tnm *testNetworkManager) RegisterAddressSizeNotification(string) (chan uint8, error) { + return nil, nil +} + +func (tnm *testNetworkManager) UnregisterAddressSizeNotification(string) {} +func (tnm *testNetworkManager) SetPoolFilter(gateway.Filter) {} + func getNDF() *ndf.NetworkDefinition { return &ndf.NetworkDefinition{ E2E: ndf.Group{ diff --git a/single/receiveResponse.go b/single/receiveResponse.go index d25880dae86dee70d882463aeba08d0310daa08b..831b25b358196ddc4806991610b51a0548ab56eb 100644 --- a/single/receiveResponse.go +++ b/single/receiveResponse.go @@ -8,9 +8,11 @@ package single import ( + "fmt" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/crypto/e2e/auth" "gitlab.com/elixxir/crypto/e2e/singleUse" "gitlab.com/elixxir/primitives/format" @@ -20,13 +22,14 @@ import ( // receiveResponseHandler handles the reception of single-use response messages. func (m *Manager) receiveResponseHandler(rawMessages chan message.Receive, - quitChan <-chan struct{}) { + stop *stoppable.Single) { jww.DEBUG.Print("Waiting to receive single-use response messages.") for { select { - case <-quitChan: + case <-stop.Quit(): jww.DEBUG.Printf("Stopping waiting to receive single-use " + "response message.") + stop.ToStopped() return case msg := <-rawMessages: jww.DEBUG.Printf("Received CMIX message; checking if it is a " + @@ -35,8 +38,13 @@ func (m *Manager) receiveResponseHandler(rawMessages chan message.Receive, // Process CMIX message err := m.processesResponse(msg.RecipientID, msg.EphemeralID, msg.Payload) if err != nil { - jww.WARN.Printf("Failed to read single-use CMIX message "+ - "response: %+v", err) + em := fmt.Sprintf("Failed to read single-use "+ + "CMIX message response: %+v", err) + jww.WARN.Print(em) + if m.client != nil { + m.client.ReportEvent(9, "SingleUse", + "Error", em) + } } } } @@ -87,6 +95,11 @@ func (m *Manager) processesResponse(rid *id.ID, ephID ephemeral.Id, // Once all message parts have been received delete and close everything if collated { + if m.client != nil { + m.client.ReportEvent(1, "SingleUse", "MessageReceived", + fmt.Sprintf("Single use response received "+ + "from %s", rid)) + } jww.DEBUG.Print("Received all parts of single-use response message.") // Exit the timeout handler state.quitChan <- struct{}{} diff --git a/single/receiveResponse_test.go b/single/receiveResponse_test.go index 4e7e8c352e8196f0ff24d5a0d60633f78439e8dd..ea75c5e3ec10cb0af1971af162b4e0fc69b78871 100644 --- a/single/receiveResponse_test.go +++ b/single/receiveResponse_test.go @@ -10,6 +10,7 @@ package single import ( "bytes" "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/crypto/e2e/auth" "gitlab.com/elixxir/crypto/e2e/singleUse" "gitlab.com/elixxir/primitives/format" @@ -26,7 +27,7 @@ import ( func TestManager_ReceiveResponseHandler(t *testing.T) { m := newTestManager(0, false, t) rawMessages := make(chan message.Receive, rawMessageBuffSize) - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") partner := NewContact(id.NewIdFromString("recipientID", id.User, t), m.store.E2e().GetGroup().NewInt(43), m.store.E2e().GetGroup().NewInt(42), singleUse.TagFP{}, 8) @@ -52,7 +53,7 @@ func TestManager_ReceiveResponseHandler(t *testing.T) { } }() - go m.receiveResponseHandler(rawMessages, quitChan) + go m.receiveResponseHandler(rawMessages, stop) for _, msg := range msgs { rawMessages <- message.Receive{ @@ -78,14 +79,16 @@ func TestManager_ReceiveResponseHandler(t *testing.T) { t.Errorf("Callback failed to be called.") } - quitChan <- struct{}{} + if err := stop.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } } // Error path: invalid CMIX message. func TestManager_ReceiveResponseHandler_CmixMessageError(t *testing.T) { m := newTestManager(0, false, t) rawMessages := make(chan message.Receive, rawMessageBuffSize) - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") partner := NewContact(id.NewIdFromString("recipientID", id.User, t), m.store.E2e().GetGroup().NewInt(43), m.store.E2e().GetGroup().NewInt(42), singleUse.TagFP{}, 8) @@ -106,7 +109,7 @@ func TestManager_ReceiveResponseHandler_CmixMessageError(t *testing.T) { } }() - go m.receiveResponseHandler(rawMessages, quitChan) + go m.receiveResponseHandler(rawMessages, stop) rawMessages <- message.Receive{ Payload: make([]byte, format.MinimumPrimeSize*2), @@ -124,7 +127,9 @@ func TestManager_ReceiveResponseHandler_CmixMessageError(t *testing.T) { case <-timer.C: } - quitChan <- struct{}{} + if err := stop.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } } // Happy path. diff --git a/single/reception.go b/single/reception.go index 53b6c12eda52386a3544b547bee0b54b91bc1d2a..23ca6f8bef891b8ae4426fe24172d4a88c2510c5 100644 --- a/single/reception.go +++ b/single/reception.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/stoppable" cAuth "gitlab.com/elixxir/crypto/e2e/auth" "gitlab.com/elixxir/crypto/e2e/singleUse" "gitlab.com/elixxir/primitives/format" @@ -19,14 +20,15 @@ import ( // receiveTransmissionHandler waits to receive single-use transmissions. When // a message is received, its is returned via its registered callback. func (m *Manager) receiveTransmissionHandler(rawMessages chan message.Receive, - quitChan <-chan struct{}) { + stop *stoppable.Single) { fp := singleUse.NewTransmitFingerprint(m.store.E2e().GetDHPublicKey()) jww.DEBUG.Print("Waiting to receive single-use transmission messages.") for { select { - case <-quitChan: + case <-stop.Quit(): jww.DEBUG.Printf("Stopping waiting to receive single-use " + "transmission message.") + stop.ToStopped() return case msg := <-rawMessages: jww.DEBUG.Printf("Received CMIX message; checking if it is a " + diff --git a/single/reception_test.go b/single/reception_test.go index 3266d9904b2fb51dd592c766b61a9c40409e0925..27a87b9a181e94437b8a3b471492123c2451547b 100644 --- a/single/reception_test.go +++ b/single/reception_test.go @@ -3,6 +3,7 @@ package single import ( "bytes" "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/stoppable" contact2 "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/crypto/e2e/singleUse" "gitlab.com/elixxir/primitives/format" @@ -17,7 +18,6 @@ import ( func TestManager_receiveTransmissionHandler(t *testing.T) { m := newTestManager(0, false, t) rawMessages := make(chan message.Receive, rawMessageBuffSize) - quitChan := make(chan struct{}) partner := contact2.Contact{ ID: id.NewIdFromString("recipientID", id.User, t), DhPubKey: m.store.E2e().GetDHPublicKey(), @@ -35,7 +35,7 @@ func TestManager_receiveTransmissionHandler(t *testing.T) { m.callbackMap.registerCallback(tag, callback) - go m.receiveTransmissionHandler(rawMessages, quitChan) + go m.receiveTransmissionHandler(rawMessages, stoppable.NewSingle("singleStoppable")) rawMessages <- message.Receive{ Payload: msg.Marshal(), } @@ -57,7 +57,7 @@ func TestManager_receiveTransmissionHandler(t *testing.T) { func TestManager_receiveTransmissionHandler_QuitChan(t *testing.T) { m := newTestManager(0, false, t) rawMessages := make(chan message.Receive, rawMessageBuffSize) - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") tag := "Test tag" payload := make([]byte, 132) rand.New(rand.NewSource(42)).Read(payload) @@ -65,8 +65,11 @@ func TestManager_receiveTransmissionHandler_QuitChan(t *testing.T) { m.callbackMap.registerCallback(tag, callback) - go m.receiveTransmissionHandler(rawMessages, quitChan) - quitChan <- struct{}{} + go m.receiveTransmissionHandler(rawMessages, stop) + + if err := stop.Close(); err != nil { + t.Errorf("Failed to signal close to process: %+v", err) + } timer := time.NewTimer(50 * time.Millisecond) @@ -82,7 +85,7 @@ func TestManager_receiveTransmissionHandler_QuitChan(t *testing.T) { func TestManager_receiveTransmissionHandler_FingerPrintError(t *testing.T) { m := newTestManager(0, false, t) rawMessages := make(chan message.Receive, rawMessageBuffSize) - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") partner := contact2.Contact{ ID: id.NewIdFromString("recipientID", id.User, t), DhPubKey: m.store.E2e().GetGroup().NewInt(42), @@ -100,7 +103,7 @@ func TestManager_receiveTransmissionHandler_FingerPrintError(t *testing.T) { m.callbackMap.registerCallback(tag, callback) - go m.receiveTransmissionHandler(rawMessages, quitChan) + go m.receiveTransmissionHandler(rawMessages, stop) rawMessages <- message.Receive{ Payload: msg.Marshal(), } @@ -119,7 +122,7 @@ func TestManager_receiveTransmissionHandler_FingerPrintError(t *testing.T) { func TestManager_receiveTransmissionHandler_ProcessMessageError(t *testing.T) { m := newTestManager(0, false, t) rawMessages := make(chan message.Receive, rawMessageBuffSize) - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") partner := contact2.Contact{ ID: id.NewIdFromString("recipientID", id.User, t), DhPubKey: m.store.E2e().GetDHPublicKey(), @@ -139,7 +142,7 @@ func TestManager_receiveTransmissionHandler_ProcessMessageError(t *testing.T) { m.callbackMap.registerCallback(tag, callback) - go m.receiveTransmissionHandler(rawMessages, quitChan) + go m.receiveTransmissionHandler(rawMessages, stop) rawMessages <- message.Receive{ Payload: msg.Marshal(), } @@ -158,7 +161,7 @@ func TestManager_receiveTransmissionHandler_ProcessMessageError(t *testing.T) { func TestManager_receiveTransmissionHandler_TagFpError(t *testing.T) { m := newTestManager(0, false, t) rawMessages := make(chan message.Receive, rawMessageBuffSize) - quitChan := make(chan struct{}) + stop := stoppable.NewSingle("singleStoppable") partner := contact2.Contact{ ID: id.NewIdFromString("recipientID", id.User, t), DhPubKey: m.store.E2e().GetDHPublicKey(), @@ -173,7 +176,7 @@ func TestManager_receiveTransmissionHandler_TagFpError(t *testing.T) { t.Fatalf("Failed to create tranmission CMIX message: %+v", err) } - go m.receiveTransmissionHandler(rawMessages, quitChan) + go m.receiveTransmissionHandler(rawMessages, stop) rawMessages <- message.Receive{ Payload: msg.Marshal(), } diff --git a/single/singleUseMap_test.go b/single/singleUseMap_test.go index 3b4a18171640935735b5d3b28042141c77ded768..1f91c755f048fa2a2444109a521931b21cdc6867 100644 --- a/single/singleUseMap_test.go +++ b/single/singleUseMap_test.go @@ -117,7 +117,7 @@ func Test_pending_addState_TimeoutError(t *testing.T) { *expectedState, *state) } - timer := time.NewTimer(timeout * 2) + timerTimeout := timeout * 4 select { case results := <-callbackChan: @@ -132,8 +132,8 @@ func Test_pending_addState_TimeoutError(t *testing.T) { if results.err == nil || !strings.Contains(results.err.Error(), "timed out") { t.Errorf("Callback did not return a time out error on return: %+v", results.err) } - case <-timer.C: - t.Error("Failed to time out.") + case <-time.NewTimer(timerTimeout).C: + t.Errorf("Failed to time out after %s.", timerTimeout) } } diff --git a/single/transmission.go b/single/transmission.go index d5289f339b1120c87256228d87fd7b7514c0a9d7..25cd73276fb76eb2226ce7f44d1140cc0b9134b7 100644 --- a/single/transmission.go +++ b/single/transmission.go @@ -64,12 +64,9 @@ type roundEvents interface { func (m *Manager) transmitSingleUse(partner contact2.Contact, payload []byte, tag string, MaxMsgs uint8, rng io.Reader, callback ReplyComm, timeout time.Duration, roundEvents roundEvents) error { - // Get ephemeral ID address size; this will block until the client knows the - // address size if it is currently unknown - if m.store.Reception().IsIdSizeDefault() { - m.store.Reception().WaitForIdSizeUpdate() - } - addressSize := m.store.Reception().GetIDSize() + // Get ephemeral ID address space size; this blocks until the address space + // size is set for the first time + addressSize := m.net.GetAddressSize() // Create new CMIX message containing the transmission payload cmixMsg, dhKey, rid, ephID, err := m.makeTransmitCmixMessage(partner, @@ -93,11 +90,11 @@ func (m *Manager) transmitSingleUse(partner contact2.Contact, payload []byte, err = m.reception.AddIdentity(reception.Identity{ EphId: ephID, Source: rid, + AddressSize: addressSize, End: timeStart.Add(2 * timeout), ExtraChecks: 10, StartValid: timeStart.Add(-2 * timeout), EndValid: timeStart.Add(2 * timeout), - RequestMask: 48*time.Hour - timeout, Ephemeral: true, }) if err != nil { @@ -116,7 +113,8 @@ func (m *Manager) transmitSingleUse(partner contact2.Contact, payload []byte, go func() { // Send Message - jww.DEBUG.Printf("Sending single-use transmission CMIX message to %s.", partner.ID) + jww.DEBUG.Printf("Sending single-use transmission CMIX "+ + "message to %s.", partner.ID) round, _, err := m.net.SendCMIX(cmixMsg, partner.ID, params.GetDefaultCMIX()) if err != nil { errorString := fmt.Sprintf("failed to send single-use transmission "+ @@ -140,15 +138,20 @@ func (m *Manager) transmitSingleUse(partner contact2.Contact, payload []byte, } // Update the timeout for the elapsed time - roundEventTimeout := timeout - netTime.Now().Sub(timeStart) - time.Millisecond + roundEventTimeout := timeout - netTime.Since(timeStart) - time.Millisecond // Check message delivery sendResults := make(chan ds.EventReturn, 1) roundEvents.AddRoundEventChan(round, sendResults, roundEventTimeout, states.COMPLETED, states.FAILED) - jww.DEBUG.Printf("Sent single-use transmission CMIX message to %s and "+ - "ephemeral ID %d on round %d.", partner.ID, ephID.Int64(), round) + im := fmt.Sprintf("Sent single-use transmission CMIX "+ + "message to %s and ephemeral ID %d on round %d.", + partner.ID, ephID.Int64(), round) + jww.DEBUG.Print(im) + if m.client != nil { + m.client.ReportEvent(1, "SingleUse", "MessageSend", im) + } // Wait until the result tracking responds success, numRoundFail, numTimeOut := utility.TrackResults(sendResults, 1) @@ -175,7 +178,7 @@ func (m *Manager) transmitSingleUse(partner contact2.Contact, payload []byte, // makeTransmitCmixMessage generates a CMIX message containing the transmission message, // which contains the encrypted payload. func (m *Manager) makeTransmitCmixMessage(partner contact2.Contact, - payload []byte, tag string, maxMsgs uint8, addressSize uint, + payload []byte, tag string, maxMsgs uint8, addressSize uint8, timeout time.Duration, timeNow time.Time, rng io.Reader) (format.Message, *cyclic.Int, *id.ID, ephemeral.Id, error) { e2eGrp := m.store.E2e().GetGroup() @@ -255,8 +258,9 @@ func generateDhKeys(grp *cyclic.Group, dhPubKey *cyclic.Int, // contains a nonce. If the generated ephemeral ID has a window that is not // within +/- the given 2*timeout from now, then the IDs are generated again // using a new nonce. -func makeIDs(msg *transmitMessagePayload, publicKey *cyclic.Int, addressSize uint, - timeout time.Duration, timeNow time.Time, rng io.Reader) (*id.ID, ephemeral.Id, error) { +func makeIDs(msg *transmitMessagePayload, publicKey *cyclic.Int, + addressSize uint8, timeout time.Duration, timeNow time.Time, + rng io.Reader) (*id.ID, ephemeral.Id, error) { var rid *id.ID var ephID ephemeral.Id @@ -277,7 +281,7 @@ func makeIDs(msg *transmitMessagePayload, publicKey *cyclic.Int, addressSize uin rid = msg.GetRID(publicKey) // Generate the ephemeral ID - ephID, start, end, err = ephemeral.GetId(rid, addressSize, timeNow.UnixNano()) + ephID, start, end, err = ephemeral.GetId(rid, uint(addressSize), timeNow.UnixNano()) if err != nil { return nil, ephemeral.Id{}, errors.Errorf("failed to generate "+ "ephemeral ID from newly generated ID: %+v", err) diff --git a/single/transmission_test.go b/single/transmission_test.go index 367a8b3bfa4bd07df0e266045281624934ea3ee3..761f65d9fc5e757aa84710cf7f3ef476f0ae91ac 100644 --- a/single/transmission_test.go +++ b/single/transmission_test.go @@ -366,7 +366,7 @@ func Test_makeIDs_Consistency(t *testing.T) { if err != nil { t.Fatalf("Failed to generate public key: %+v", err) } - addressSize := uint(32) + addressSize := uint8(32) expectedPayload, err := unmarshalTransmitMessagePayload(msgPayload.Marshal()) if err != nil { @@ -397,7 +397,7 @@ func Test_makeIDs_Consistency(t *testing.T) { } expectedEphID, _, _, err := ephemeral.GetId(expectedPayload.GetRID(publicKey), - addressSize, timeNow.UnixNano()) + uint(addressSize), timeNow.UnixNano()) if err != nil { t.Fatalf("Failed to generate expected ephemeral ID: %+v", err) } diff --git a/stoppable/bindings.go b/stoppable/bindings.go deleted file mode 100644 index 55784d6522bcb0567d8eb86b282bb9895302ab57..0000000000000000000000000000000000000000 --- a/stoppable/bindings.go +++ /dev/null @@ -1,37 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// -// Copyright © 2020 xx network SEZC // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file // -/////////////////////////////////////////////////////////////////////////////// - -package stoppable - -import "time" - -type Bindings interface { - Close(timeoutMS int) error - IsRunning() bool - Name() string -} - -func WrapForBindings(s Stoppable) Bindings { - return &bindingsStoppable{s: s} -} - -type bindingsStoppable struct { - s Stoppable -} - -func (bs *bindingsStoppable) Close(timeoutMS int) error { - timeout := time.Duration(timeoutMS) * time.Millisecond - return bs.s.Close(timeout) -} - -func (bs *bindingsStoppable) IsRunning() bool { - return bs.s.IsRunning() -} - -func (bs *bindingsStoppable) Name() string { - return bs.s.Name() -} diff --git a/stoppable/cleanup.go b/stoppable/cleanup.go deleted file mode 100644 index b1e2c4561dc87c2af1ec654d48b787d47ca8182e..0000000000000000000000000000000000000000 --- a/stoppable/cleanup.go +++ /dev/null @@ -1,95 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// -// Copyright © 2020 xx network SEZC // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file // -/////////////////////////////////////////////////////////////////////////////// - -package stoppable - -import ( - "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - "gitlab.com/xx_network/primitives/netTime" - "sync" - "sync/atomic" - "time" -) - -// Cleanup wraps any stoppable and runs a callback after to stop for cleanup -// behavior. The cleanup is run under the remainder of the timeout but will not -// be canceled if the timeout runs out. The cleanup function does not run if the -// thread does not stop. -type Cleanup struct { - stop Stoppable - // the clean function receives how long it has to run before the timeout, - // this is nto expected to be used in most cases - clean func(duration time.Duration) error - running uint32 - once sync.Once -} - -// NewCleanup creates a new Cleanup from the passed stoppable and function. -func NewCleanup(stop Stoppable, clean func(duration time.Duration) error) *Cleanup { - return &Cleanup{ - stop: stop, - clean: clean, - running: 0, - } -} - -// IsRunning returns true if the thread is still running and its cleanup has -// completed. -func (c *Cleanup) IsRunning() bool { - return atomic.LoadUint32(&c.running) == 1 -} - -// Name returns the name of the stoppable denoting it has cleanup. -func (c *Cleanup) Name() string { - return c.stop.Name() + " with cleanup" -} - -// Close stops the contained stoppable and runs the cleanup function after. The -// cleanup function does not run if the thread does not stop. -func (c *Cleanup) Close(timeout time.Duration) error { - var err error - - c.once.Do( - func() { - defer atomic.StoreUint32(&c.running, 0) - start := netTime.Now() - - // Run the stoppable - if err := c.stop.Close(timeout); err != nil { - err = errors.WithMessagef(err, "Cleanup for %s not executed", - c.stop.Name()) - return - } - - // Run the cleanup function with the remaining time as a timeout - elapsed := time.Since(start) - - complete := make(chan error, 1) - go func() { - complete <- c.clean(elapsed) - }() - - timer := time.NewTimer(elapsed) - - select { - case err := <-complete: - if err != nil { - err = errors.WithMessagef(err, "Cleanup for %s failed", - c.stop.Name()) - } - case <-timer.C: - err = errors.Errorf("Clean up for %s timeout", c.stop.Name()) - } - }) - - if err != nil { - jww.ERROR.Printf(err.Error()) - } - - return err -} diff --git a/stoppable/cleanup_test.go b/stoppable/cleanup_test.go deleted file mode 100644 index 8bc7fe0be09e69b0809cbd97194dbbe58902918f..0000000000000000000000000000000000000000 --- a/stoppable/cleanup_test.go +++ /dev/null @@ -1,62 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// -// Copyright © 2020 xx network SEZC // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file // -/////////////////////////////////////////////////////////////////////////////// - -package stoppable - -import ( - "testing" -) - -// Tests happy path of NewCleanup(). -func TestNewCleanup(t *testing.T) { - single := NewSingle("test name") - cleanup := NewCleanup(single, single.Close) - - if cleanup.stop != single || cleanup.running != 0 { - t.Errorf("NewCleanup() returned Single with incorrect values."+ - "\n\texpected: stop: %v running: %d\n\treceived: stop: %v running: %d", - single, cleanup.stop, 0, cleanup.running) - } -} - -// Tests happy path of Cleanup.IsRunning(). -func TestCleanup_IsRunning(t *testing.T) { - single := NewSingle("test name") - cleanup := NewCleanup(single, single.Close) - - if cleanup.IsRunning() { - t.Errorf("IsRunning() returned false when it should be running.") - } - - cleanup.running = 1 - if !cleanup.IsRunning() { - t.Errorf("IsRunning() returned true when it should not be running.") - } -} - -// Tests happy path of Cleanup.Name(). -func TestCleanup_Name(t *testing.T) { - name := "test name" - single := NewSingle(name) - cleanup := NewCleanup(single, single.Close) - - if name+" with cleanup" != cleanup.Name() { - t.Errorf("Name() returned the incorrect string."+ - "\n\texpected: %s\n\treceived: %s", name+" with cleanup", cleanup.Name()) - } -} - -// Tests happy path of Cleanup.Close(). -func TestCleanup_Close(t *testing.T) { - single := NewSingle("test name") - cleanup := NewCleanup(single, single.Close) - - err := cleanup.Close(0) - if err != nil { - t.Errorf("Close() returned an error: %v", err) - } -} diff --git a/stoppable/multi.go b/stoppable/multi.go index 0636b84fa6e4a3d17e5a74431e42e796d84de45b..60d1c8530300f2d52babf469d923627f236a6f1d 100644 --- a/stoppable/multi.go +++ b/stoppable/multi.go @@ -8,91 +8,120 @@ package stoppable import ( - "fmt" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "strings" "sync" "sync/atomic" - "time" ) +// Error message. +const closeMultiErr = "multi stoppable %q failed to close %d/%d stoppables" + type Multi struct { stoppables []Stoppable name string - running uint32 mux sync.RWMutex once sync.Once } -// NewMulti returns a new multi stoppable. +// NewMulti returns a new multi Stoppable. func NewMulti(name string) *Multi { return &Multi{ - name: name, - running: 1, + name: name, } } -// IsRunning returns true if the thread is still running. -func (m *Multi) IsRunning() bool { - return atomic.LoadUint32(&m.running) == 1 -} - -// Add adds the given stoppable to the list of stoppables. +// Add adds the given Stoppable to the list of stoppables. func (m *Multi) Add(stoppable Stoppable) { m.mux.Lock() m.stoppables = append(m.stoppables, stoppable) m.mux.Unlock() } -// Name returns the name of the multi stoppable and the names of all stoppables +// Name returns the name of the Multi Stoppable and the names of all stoppables // it contains. func (m *Multi) Name() string { m.mux.RLock() - names := m.name + ": {" - for _, s := range m.stoppables { - names += s.Name() + ", " + + names := make([]string, len(m.stoppables)) + for i, s := range m.stoppables { + names[i] = s.Name() } - if len(m.stoppables) > 0 { - names = names[:len(names)-2] + + m.mux.RUnlock() + + return m.name + "{" + strings.Join(names, ", ") + "}" +} + +// GetStatus returns the lowest status of all of the Stoppable children. The +// status is not the status of all Stoppables, but the status of the Stoppable +// with the lowest status. +func (m *Multi) GetStatus() Status { + lowestStatus := Stopped + m.mux.RLock() + + for _, s := range m.stoppables { + status := s.GetStatus() + if status < lowestStatus { + lowestStatus = status + } } - names += "}" + m.mux.RUnlock() - return names + return lowestStatus } -// Close closes all child stoppers. It does not return their errors and assumes -// they print them to the log. -func (m *Multi) Close(timeout time.Duration) error { - var err error - m.once.Do( - func() { - atomic.StoreUint32(&m.running, 0) - - numErrors := uint32(0) - wg := &sync.WaitGroup{} - - m.mux.Lock() - for _, stoppable := range m.stoppables { - wg.Add(1) - go func(stoppable Stoppable) { - if stoppable.Close(timeout) != nil { - atomic.AddUint32(&numErrors, 1) - } - wg.Done() - }(stoppable) - } - m.mux.Unlock() - - wg.Wait() - - if numErrors > 0 { - errStr := fmt.Sprintf("MultiStopper %s failed to close "+ - "%v/%v stoppers", m.name, numErrors, len(m.stoppables)) - jww.ERROR.Println(errStr) - err = errors.New(errStr) - } - }) - - return err +// IsRunning returns true if Stoppable is marked as running. +func (m *Multi) IsRunning() bool { + return m.GetStatus() == Running +} + +// IsStopping returns true if Stoppable is marked as stopping. +func (m *Multi) IsStopping() bool { + return m.GetStatus() == Stopping +} + +// IsStopped returns true if Stoppable is marked as stopped. +func (m *Multi) IsStopped() bool { + return m.GetStatus() == Stopped +} + +// Close issues a close signal to all child stoppables and marks the status of +// the Multi Stoppable as stopping. Returns an error if one or more child +// stoppables failed to close but it does not return their specific errors and +// assumes they print them to the log. +func (m *Multi) Close() error { + var numErrors uint32 + + m.once.Do(func() { + var wg sync.WaitGroup + + jww.TRACE.Printf("Sending on quit channel to multi stoppable %q.", + m.Name()) + + m.mux.Lock() + // Attempt to stop each stoppable in its own goroutine + for _, stoppable := range m.stoppables { + wg.Add(1) + go func(stoppable Stoppable) { + if stoppable.Close() != nil { + atomic.AddUint32(&numErrors, 1) + } + wg.Done() + }(stoppable) + } + m.mux.Unlock() + + wg.Wait() + }) + + if numErrors > 0 { + err := errors.Errorf(closeMultiErr, m.name, numErrors, len(m.stoppables)) + jww.ERROR.Print(err.Error()) + return err + } + + return nil } diff --git a/stoppable/multi_test.go b/stoppable/multi_test.go index 5999f838ae44d0b64a96cd27c0fc1d8e28ee49fd..4a7eaf0873019d7ab9bd89e4f6aac2ba8f1661c9 100644 --- a/stoppable/multi_test.go +++ b/stoppable/multi_test.go @@ -8,114 +8,349 @@ package stoppable import ( + "fmt" "reflect" + "strconv" + "strings" + "sync" + "sync/atomic" "testing" "time" ) -// Tests happy path of NewMulti(). +// Tests that NewMulti returns a Multi that is running with the given name. func TestNewMulti(t *testing.T) { - name := "test name" + name := "testMulti" multi := NewMulti(name) - if multi.name != name || multi.running != 1 { - t.Errorf("NewMulti() returned Multi with incorrect values."+ - "\n\texpected: name: %s running: %d\n\treceived: name: %s running: %d", - name, 1, multi.name, multi.running) + if multi.name != name { + t.Errorf("NewMulti returned Multi with incorrect name."+ + "\nexpected: %s\nreceived: %s", name, multi.name) } } -// Tests happy path of Multi.IsRunning(). +// Tests that Multi.Add adds all the stoppables to the list. +func TestMulti_Add(t *testing.T) { + multi := NewMulti("testMulti") + expected := []Stoppable{ + NewSingle("testSingle0"), + NewMulti("testMulti0"), + NewSingle("testSingle1"), + NewMulti("testMulti1"), + } + + for _, stoppable := range expected { + multi.Add(stoppable) + } + + if !reflect.DeepEqual(multi.stoppables, expected) { + t.Errorf("Add did not add the correct Stoppables."+ + "\nexpected: %+v\nreceived: %+v", multi.stoppables, expected) + } +} + +// Unit test of Multi.Name. +func TestMulti_Name(t *testing.T) { + name := "testMulti" + multi := NewMulti(name) + + // Add stoppables and created list of their names + var nameList []string + for i := 0; i < 10; i++ { + newName := "" + if i%2 == 0 { + newName = "single" + strconv.Itoa(i) + multi.Add(NewSingle(newName)) + } else { + newMulti := NewMulti("multi" + strconv.Itoa(i)) + if i != 5 { + newMulti.Add(NewMulti("multiA")) + newMulti.Add(NewMulti("multiB")) + } + multi.Add(newMulti) + newName = newMulti.Name() + } + nameList = append(nameList, newName) + } + + expected := name + "{" + strings.Join(nameList, ", ") + "}" + + if multi.Name() != expected { + t.Errorf("Name failed to return the expected string."+ + "\nexpected: %s\nreceived: %s", expected, multi.Name()) + } +} + +// Tests that Multi.Name returns the expected string when it has no stoppables. +func TestMulti_Name_NoStoppables(t *testing.T) { + name := "testMulti" + multi := NewMulti(name) + + expected := name + "{}" + + if multi.Name() != expected { + t.Errorf("Name failed to return the expected string."+ + "\nexpected: %s\nreceived: %s", expected, multi.Name()) + } +} + +// Tests that Multi.GetStatus returns the expected Status. +func TestMulti_GetStatus(t *testing.T) { + multi := NewMulti("testMulti") + single1 := NewSingle("testSingle1") + single2 := NewSingle("testSingle2") + atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopped)) + multi.Add(single1) + multi.Add(single2) + + status := multi.GetStatus() + if status != Running { + t.Errorf("GetStatus returned the wrong status."+ + "\nexpected: %s\nreceived: %s", Running, status) + } + + atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopping)) + status = multi.GetStatus() + if status != Stopping { + t.Errorf("GetStatus returned the wrong status."+ + "\nexpected: %s\nreceived: %s", Stopping, status) + } + + atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopped)) + status = multi.GetStatus() + if status != Stopped { + t.Errorf("GetStatus returned the wrong status."+ + "\nexpected: %s\nreceived: %s", Stopped, status) + } +} + +// Tests that Multi.GetStatus returns the expected Status when it has no +// children. +func TestMulti_GetStatus_NoChildren(t *testing.T) { + multi := NewMulti("testMulti") + + status := multi.GetStatus() + if status != Stopped { + t.Errorf("GetStatus returned the wrong status."+ + "\nexpected: %s\nreceived: %s", Stopped, status) + } +} + +// Tests that Multi.IsRunning returns the expected value when the Multi is +// marked as running, stopping, and stopped. func TestMulti_IsRunning(t *testing.T) { - multi := NewMulti("name") + multi := NewMulti("testMulti") + single1 := NewSingle("testSingle1") + single2 := NewSingle("testSingle2") + atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopping)) + multi.Add(single1) + multi.Add(single2) + + if result := multi.IsRunning(); !result { + t.Errorf("IsRunning returned the wrong value when running."+ + "\nexpected: %t\nreceived: %t", true, result) + } - if !multi.IsRunning() { - t.Errorf("IsRunning() returned false when it should be running.") + atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopping)) + atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopped)) + if result := multi.IsRunning(); result { + t.Errorf("IsRunning returned the wrong value when stopping."+ + "\nexpected: %t\nreceived: %t", false, result) } - multi.running = 0 - if multi.IsRunning() { - t.Errorf("IsRunning() returned true when it should not be running.") + atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopped)) + if result := multi.IsRunning(); result { + t.Errorf("IsRunning returned the wrong value when stopped."+ + "\nexpected: %t\nreceived: %t", false, result) } } -// Tests happy path of Multi.Add(). -func TestMulti_Add(t *testing.T) { - multi := NewMulti("multi name") - singles := []*Single{ - NewSingle("single name 1"), - NewSingle("single name 2"), - NewSingle("single name 3"), +// Tests that Multi.IsStopping returns the expected value when the Multi is +// marked as running, stopping, and stopped. +func TestMulti_IsStopping(t *testing.T) { + multi := NewMulti("testMulti") + single1 := NewSingle("testSingle1") + single2 := NewSingle("testSingle2") + atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopped)) + multi.Add(single1) + multi.Add(single2) + + if result := multi.IsStopping(); result { + t.Errorf("IsStopping returned the wrong value when running."+ + "\nexpected: %t\nreceived: %t", true, result) } - for _, single := range singles { - multi.Add(single) + atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopping)) + if result := multi.IsStopping(); !result { + t.Errorf("IsStopping returned the wrong value when stopping."+ + "\nexpected: %t\nreceived: %t", false, result) } - for i, single := range singles { - if !reflect.DeepEqual(single, multi.stoppables[i]) { - t.Errorf("Add() did not add the correct Stoppables."+ - "\n\texpected: %#v\n\treceived: %#v", single, multi.stoppables[i]) - } + atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopped)) + if result := multi.IsStopping(); result { + t.Errorf("IsStopping returned the wrong value when stopped."+ + "\nexpected: %t\nreceived: %t", false, result) } } -// Tests happy path of Multi.Name(). -func TestMulti_Name(t *testing.T) { - name := "test name" - multi := NewMulti(name) +// Tests that Multi.IsStopped returns the expected value when the Multi is +// marked as running, stopping, and stopped. +func TestMulti_IsStopped(t *testing.T) { + multi := NewMulti("testMulti") + single1 := NewSingle("testSingle1") + single2 := NewSingle("testSingle2") + atomic.StoreUint32((*uint32)(&single2.status), uint32(Stopped)) + multi.Add(single1) + multi.Add(single2) + + if result := multi.IsStopped(); result { + t.Errorf("IsStopped returned the wrong value when running."+ + "\nexpected: %t\nreceived: %t", true, result) + } + + atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopping)) + if result := multi.IsStopped(); result { + t.Errorf("IsStopped returned the wrong value when stopping."+ + "\nexpected: %t\nreceived: %t", false, result) + } + + atomic.StoreUint32((*uint32)(&single1.status), uint32(Stopped)) + if result := multi.IsStopped(); !result { + t.Errorf("IsStopped returned the wrong value when stopped."+ + "\nexpected: %t\nreceived: %t", false, result) + } +} + +// Tests that Multi.IsStopped returns true when all of the child stoppables are +// stopped. +func TestMulti_IsStopped_StoppedStatus(t *testing.T) { + multi := NewMulti("testMulti") singles := []*Single{ - NewSingle("single name 1"), - NewSingle("single name 2"), - NewSingle("single name 3"), + NewSingle("testSingle0"), + NewSingle("testSingle1"), + NewSingle("testSingle2"), + NewSingle("testSingle3"), + NewSingle("testSingle4"), + } + for _, single := range singles[:3] { + atomic.StoreUint32((*uint32)(&single.status), uint32(Stopped)) + multi.Add(single) } - expectedNames := []string{ - name + ": {}", - name + ": {" + singles[0].name + "}", - name + ": {" + singles[0].name + ", " + singles[1].name + "}", - name + ": {" + singles[0].name + ", " + singles[1].name + ", " + singles[2].name + "}", + subMulti := NewMulti("subMulti") + for _, single := range singles[3:] { + atomic.StoreUint32((*uint32)(&single.status), uint32(Stopped)) + subMulti.Add(single) } + multi.Add(subMulti) - for i, single := range singles { - if expectedNames[i] != multi.Name() { - t.Errorf("Name() returned the incorrect string."+ - "\n\texpected: %s\n\treceived: %s", expectedNames[0], multi.Name()) - } + if !multi.IsStopped() { + t.Error("IsStopped did not find all stoppables as stopped.") + } +} + +// Error path: tests that Multi.IsStopped returns false when not all of the +// child stoppables are stopped. +func TestMulti_IsStopped_NotStoppedError(t *testing.T) { + multi := NewMulti("testMulti") + singles := []*Single{ + NewSingle("testSingle0"), + NewSingle("testSingle1"), + NewSingle("testSingle2"), + NewSingle("testSingle3"), + NewSingle("testSingle4"), + } + for _, single := range singles { multi.Add(single) } + + for _, single := range singles[:4] { + atomic.StoreUint32((*uint32)(&single.status), uint32(Stopped)) + } + + if multi.IsStopped() { + t.Error("IsStopped found all the stoppables as stopped when some are " + + "still running") + } } -// Tests happy path of Multi.Close(). +// Tests that Multi.Close sends on all Single quit channels. func TestMulti_Close(t *testing.T) { - // Create new Multi and add Singles to it - multi := NewMulti("name") + multi := NewMulti("testMulti") singles := []*Single{ - NewSingle("single name 1"), - NewSingle("single name 2"), - NewSingle("single name 3"), + NewSingle("testSingle0"), + NewSingle("testSingle1"), + NewSingle("testSingle2"), + NewSingle("testSingle3"), + NewSingle("testSingle4"), } - for _, single := range singles { + for _, single := range singles[:3] { multi.Add(single) } + subMulti := NewMulti("subMulti") + for _, single := range singles[3:] { + subMulti.Add(single) + } + multi.Add(subMulti) - go func() { - select { - case <-singles[0].quit: - } - select { - case <-singles[1].quit: - } - select { - case <-singles[2].quit: - } - }() + for _, single := range singles { + go func(single *Single) { + select { + case <-time.NewTimer(5 * time.Millisecond).C: + t.Errorf("Single %s failed to quit.", single.Name()) + case <-single.Quit(): + } + }(single) + } + + err := multi.Close() + if err != nil { + t.Errorf("Close() returned an error: %v", err) + } - err := multi.Close(5 * time.Millisecond) + err = multi.Close() if err != nil { t.Errorf("Close() returned an error: %v", err) } +} + +// Error path: tests that Multi.Close returns the expected error when the Single +// stoppables are not running. +func TestMulti_Close_StoppableCloseError(t *testing.T) { + multi := NewMulti("testMulti") + var singles []*Single + for i := 0; i < 5; i++ { + single := NewSingle("testSingle" + strconv.Itoa(i)) + singles = append(singles, single) + multi.Add(single) + atomic.StoreUint32((*uint32)(&single.status), uint32(Stopped)) + } + + var wg sync.WaitGroup + for _, single := range singles { + wg.Add(1) + go func(single *Single) { + select { + case <-time.NewTimer(15 * time.Millisecond).C: + case <-single.Quit(): + t.Errorf("Single %s to quit when it should have failed.", + single.Name()) + } + wg.Done() + }(single) + } + + expectedErr := fmt.Sprintf(closeMultiErr, multi.name, 0, 0) + expectedErr = strings.SplitN(expectedErr, " 0/0", 2)[0] + + err := multi.Close() + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("Close() did not return the expected error."+ + "\nexpected: %s\nreceived: %v", expectedErr, err) + } + + wg.Wait() - err = multi.Close(0) + err = multi.Close() if err != nil { t.Errorf("Close() returned an error: %v", err) } diff --git a/stoppable/single.go b/stoppable/single.go index 2e8fa78a2a1c3f4f12752f045cb93c541da8412b..dfde7242ed83f0af975efa0656933b56d0b8145a 100644 --- a/stoppable/single.go +++ b/stoppable/single.go @@ -12,55 +12,109 @@ import ( jww "github.com/spf13/jwalterweatherman" "sync" "sync/atomic" - "time" ) -// Single allows stopping a single goroutine using a channel. -// It adheres to the stoppable interface. +// Error message. +const toStoppingErr = "failed to set the status of single stoppable %q to " + + "stopped when status is %s instead of %s" + +// Single allows stopping a single goroutine using a channel. It adheres to the +// Stoppable interface. type Single struct { - name string - quit chan struct{} - running uint32 - once sync.Once + name string + quit chan struct{} + status Status + once sync.Once } -// NewSingle returns a new single stoppable. +// NewSingle returns a new single Stoppable. func NewSingle(name string) *Single { return &Single{ - name: name, - quit: make(chan struct{}), - running: 1, + name: name, + quit: make(chan struct{}, 1), + status: Running, } } -// IsRunning returns true if the thread is still running. +// Name returns the name of the Single Stoppable. +func (s *Single) Name() string { + return s.name +} + +// GetStatus returns the status of the Stoppable. +func (s *Single) GetStatus() Status { + return Status(atomic.LoadUint32((*uint32)(&s.status))) +} + +// IsRunning returns true if Stoppable is marked as running. func (s *Single) IsRunning() bool { - return atomic.LoadUint32(&s.running) == 1 + return s.GetStatus() == Running } -// Quit returns the read only channel it will send the stop signal on. -func (s *Single) Quit() <-chan struct{} { - return s.quit +// IsStopping returns true if Stoppable is marked as stopping. +func (s *Single) IsStopping() bool { + return s.GetStatus() == Stopping } -// Name returns the name of the thread. This is designed to be -func (s *Single) Name() string { - return s.name +// IsStopped returns true if Stoppable is marked as stopped. +func (s *Single) IsStopped() bool { + return s.GetStatus() == Stopped +} + +// toStopping changes the status from running to stopping. An error is returned +// if the status is not already set to running. +func (s *Single) toStopping() error { + if !atomic.CompareAndSwapUint32((*uint32)(&s.status), uint32(Running), uint32(Stopping)) { + return errors.Errorf(toStoppingErr, s.Name(), s.GetStatus(), Running) + } + + jww.TRACE.Printf("Switched status of single stoppable %q from %s to %s.", + s.Name(), Running, Stopping) + + return nil +} + +// ToStopped changes the status from stopping to stopped. Panics if the status +// is not already set to stopping. +func (s *Single) ToStopped() { + if !atomic.CompareAndSwapUint32((*uint32)(&s.status), uint32(Stopping), uint32(Stopped)) { + jww.FATAL.Panicf("Failed to set the status of single stoppable %q to "+ + "stopped when status is %s instead of %s.", + s.Name(), s.GetStatus(), Stopping) + } + + jww.TRACE.Printf("Switched status of single stoppable %q from %s to %s.", + s.Name(), Stopping, Stopped) } -// Close signals the thread to time out and closes if it is still running. -func (s *Single) Close(timeout time.Duration) error { +// Quit returns a receive-only channel that will be triggered when the Stoppable +// quits. +func (s *Single) Quit() <-chan struct{} { + return s.quit +} + +// Close signals the Single to close via the quit channel. Returns an error if +// the status of the Single is not Running. +func (s *Single) Close() error { var err error + s.once.Do(func() { - timer := time.NewTimer(timeout) - select { - case <-timer.C: - jww.ERROR.Printf("Stopper for %s failed to stop after "+ - "timeout of %s", s.name, timeout) - err = errors.Errorf("%s failed to close", s.name) - case s.quit <- struct{}{}: + // Attempt to set status to stopping or return an error if unable + err = s.toStopping() + if err != nil { + return } - atomic.StoreUint32(&s.running, 0) + + jww.TRACE.Printf("Sending on quit channel to single stoppable %q.", + s.Name()) + + // Send on quit channel + s.quit <- struct{}{} }) + + if err != nil { + jww.ERROR.Print(err.Error()) + } + return err } diff --git a/stoppable/single_test.go b/stoppable/single_test.go index ceb5a9ecf6235a5de1884ed4d469f0a46aa0c0f4..c93a1ebfcc1815b2d5c82e7f2e2dc43ff96a076c 100644 --- a/stoppable/single_test.go +++ b/stoppable/single_test.go @@ -8,96 +8,254 @@ package stoppable import ( + "fmt" + "sync/atomic" "testing" "time" ) -// Tests happy path of NewSingle(). +// Tests that NewSingle returns a Single with the correct name and running. func TestNewSingle(t *testing.T) { - name := "test name" + name := "threadName" single := NewSingle(name) - if single.name != name || single.running != 1 { - t.Errorf("NewSingle() returned Single with incorrect values."+ - "\n\texpected: name: %s running: %d\n\treceived: name: %s running: %d", - name, 1, single.name, single.running) + if single.name != name { + t.Errorf("NewSingle returned Single with incorrect name."+ + "\nexpected: %s\nreceived: %s", name, single.name) + } + + if single.status != Running { + t.Errorf("NewSingle returned Single with incorrect status."+ + "\nexpected: %s\nreceived: %s", Running, single.status) + } +} + +// Unit test of Single.Name. +func TestSingle_Name(t *testing.T) { + name := "threadName" + single := NewSingle(name) + + if name != single.Name() { + t.Errorf("Name did not return the expected name."+ + "\nexpected: %s\nreceived: %s", name, single.Name()) } } -// Tests happy path of Single.IsRunning(). +// Tests that Single.GetStatus returns the expected Status. +func TestSingle_GetStatus(t *testing.T) { + single := NewSingle("threadName") + + status := single.GetStatus() + if status != Running { + t.Errorf("GetStatus returned the wrong status."+ + "\nexpected: %s\nreceived: %s", Running, status) + } + + atomic.StoreUint32((*uint32)(&single.status), uint32(Stopping)) + status = single.GetStatus() + if status != Stopping { + t.Errorf("GetStatus returned the wrong status."+ + "\nexpected: %s\nreceived: %s", Stopping, status) + } + + atomic.StoreUint32((*uint32)(&single.status), uint32(Stopped)) + status = single.GetStatus() + if status != Stopped { + t.Errorf("GetStatus returned the wrong status."+ + "\nexpected: %s\nreceived: %s", Stopped, status) + } +} + +// Tests that Single.IsRunning returns the expected value when the Single is +// marked as running, stopping, and stopped. func TestSingle_IsRunning(t *testing.T) { - single := NewSingle("name") + single := NewSingle("threadName") - if !single.IsRunning() { - t.Errorf("IsRunning() returned false when it should be running.") + if result := single.IsRunning(); !result { + t.Errorf("IsRunning returned the wrong value when running."+ + "\nexpected: %t\nreceived: %t", true, result) } - single.running = 0 - if single.IsRunning() { - t.Errorf("IsRunning() returned true when it should not be running.") + single.status = Stopping + if result := single.IsRunning(); result { + t.Errorf("IsRunning returned the wrong value when stopping."+ + "\nexpected: %t\nreceived: %t", false, result) + } + + single.status = Stopped + if result := single.IsRunning(); result { + t.Errorf("IsRunning returned the wrong value when stopped."+ + "\nexpected: %t\nreceived: %t", false, result) } } -// Tests happy path of Single.Quit(). -func TestSingle_Quit(t *testing.T) { - single := NewSingle("name") +// Tests that Single.IsStopping returns the expected value when the Single is +// marked as running, stopping, and stopped. +func TestSingle_IsStopping(t *testing.T) { + single := NewSingle("threadName") - go func() { - time.Sleep(150 * time.Nanosecond) - single.quit <- struct{}{} - }() + if result := single.IsStopping(); result { + t.Errorf("IsStopping returned the wrong value when running."+ + "\nexpected: %t\nreceived: %t", true, result) + } - timer := time.NewTimer(2 * time.Millisecond) - select { - case <-timer.C: - t.Errorf("Quit signal not received.") - case <-single.quit: + single.status = Stopping + if result := single.IsStopping(); !result { + t.Errorf("IsStopping returned the wrong value when stopping."+ + "\nexpected: %t\nreceived: %t", false, result) + } + + single.status = Stopped + if result := single.IsStopping(); result { + t.Errorf("IsStopping returned the wrong value when stopped."+ + "\nexpected: %t\nreceived: %t", false, result) } } -// Tests happy path of Single.Name(). -func TestSingle_Name(t *testing.T) { - name := "test name" - single := NewSingle(name) +// Tests that Single.IsStopped returns the expected value when the Single is +// marked as running, stopping, and stopped. +func TestSingle_IsStopped(t *testing.T) { + single := NewSingle("threadName") - if name != single.Name() { - t.Errorf("Name() returned the incorrect string."+ - "\n\texpected: %s\n\treceived: %s", name, single.Name()) + if result := single.IsStopped(); result { + t.Errorf("IsStopped returned the wrong value when running."+ + "\nexpected: %t\nreceived: %t", true, result) + } + + single.status = Stopping + if result := single.IsStopped(); result { + t.Errorf("IsStopped returned the wrong value when stopping."+ + "\nexpected: %t\nreceived: %t", false, result) + } + + single.status = Stopped + if result := single.IsStopped(); !result { + t.Errorf("IsStopped returned the wrong value when stopped."+ + "\nexpected: %t\nreceived: %t", false, result) } } -// Test happy path of Single.Close(). -func TestSingle_Close(t *testing.T) { - single := NewSingle("name") +// Tests that Single.toStopping changes the status to stopping. +func TestSingle_toStopping(t *testing.T) { + single := NewSingle("threadName") + + err := single.toStopping() + if err != nil { + t.Errorf("toStopping returned an error: %+v", err) + } + + if single.status != Stopping { + t.Errorf("toStopping failed to set the status correctly."+ + "\nexpected: %s\nreceived: %s", Stopping, single.status) + } +} + +// Error path: tests that Single.toStopping returns an error when failing to +// change the status to stopping when the current status is not running. +func TestSingle_toStopping_StatusError(t *testing.T) { + single := NewSingle("threadName") + single.status = Stopped + expectedErr := fmt.Sprintf( + toStoppingErr, single.Name(), single.GetStatus(), Running) + + err := single.toStopping() + if err == nil || err.Error() != expectedErr { + t.Errorf("toStopping failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } + + if single.status != Stopped { + t.Errorf("toStopping changed the status when the compare failed."+ + "\nexpected: %s\nreceived: %s", Stopped, single.status) + } +} + +// Tests that Single.ToStopped changes the status to stopped. +func TestSingle_ToStopped(t *testing.T) { + single := NewSingle("threadName") + + single.status = Stopping + single.ToStopped() + + if single.status != Stopped { + t.Errorf("ToStopped failed to set the status correctly."+ + "\nexpected: %s\nreceived: %s", Stopped, single.status) + } +} + +// Panic path: tests that Single.ToStopped panics when failing to change the +// status to stopped when the current status is not stopping. +func TestSingle_ToStopped_StatusPanic(t *testing.T) { + single := NewSingle("threadName") + + defer func() { + if r := recover(); r == nil { + t.Errorf("ToStopped failed to panic when the status should not " + + "have changed.") + } else { + if single.status != Running { + t.Errorf("ToStopped changed the status when the compare failed."+ + "\nexpected: %s\nreceived: %s", Running, single.status) + } + } + }() + + single.status = Running + single.ToStopped() +} + +// Tests that Single.Quit returns a channel that is triggered when the Single +// quit channel is triggered. +func TestSingle_Quit(t *testing.T) { + single := NewSingle("threadName") go func() { - time.Sleep(150 * time.Nanosecond) select { - case <-single.quit: + case <-time.NewTimer(5 * time.Millisecond).C: + t.Error("Timed out waiting for quit channel.") + case <-single.Quit(): } }() - err := single.Close(5 * time.Millisecond) - if err != nil { - t.Errorf("Close() returned an error: %v", err) - } + single.quit <- struct{}{} } -// Tests that Single.Close() returns an error when the timeout is reached. -func TestSingle_Close_Error(t *testing.T) { - single := NewSingle("name") - expectedErr := single.name + " failed to close" +// Test happy path of Single.Close(). +func TestSingle_Close(t *testing.T) { + single := NewSingle("threadName") + timeout := 10 * time.Millisecond go func() { - time.Sleep(3 * time.Millisecond) select { - case <-single.quit: + case <-time.NewTimer(timeout).C: + t.Errorf("Timed out waiting to receive on quit channel after %s.", + timeout) + case <-single.Quit(): + if !single.IsStopping() { + t.Errorf("Status of stoppable incorrect."+ + "\nexpected: %s\nreceived: %s", Stopping, single.status) + } + atomic.StoreUint32((*uint32)(&single.status), uint32(Stopped)) } }() - err := single.Close(2 * time.Millisecond) - if err == nil { - t.Errorf("Close() did not return the expected error."+ - "\n\texpected: %v\n\treceived: %v", expectedErr, err) + err := single.Close() + if err != nil { + t.Errorf("Close returned an error: %v", err) + } +} + +// Error path: tests that Single.Close returns an error when the status fails +// to change to stopping. +func TestSingle_Close_Error(t *testing.T) { + single := NewSingle("threadName") + single.status = Stopped + expectedErr := fmt.Sprintf( + toStoppingErr, single.Name(), single.GetStatus(), Running) + + err := single.Close() + if err == nil || err.Error() != expectedErr { + t.Errorf("Close did not return the expected error."+ + "\nexpected: %s\nreceived: %v", expectedErr, err) } } diff --git a/stoppable/status.go b/stoppable/status.go new file mode 100644 index 0000000000000000000000000000000000000000..1b306bd69a13394d8321c52b742ef5ecf82a83a9 --- /dev/null +++ b/stoppable/status.go @@ -0,0 +1,36 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package stoppable + +import ( + "strconv" +) + +const ( + Running Status = iota + Stopping + Stopped +) + +// Status holds the current status of a Stoppable. +type Status uint32 + +// String prints a string representation of the current Status. This functions +// satisfies the fmt.Stringer interface. +func (s Status) String() string { + switch s { + case Running: + return "running" + case Stopping: + return "stopping" + case Stopped: + return "stopped" + default: + return "INVALID STATUS: " + strconv.FormatUint(uint64(s), 10) + } +} diff --git a/stoppable/status_test.go b/stoppable/status_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c2f4bcd05f106e1f0bc6e6083a88096a3cdde1f7 --- /dev/null +++ b/stoppable/status_test.go @@ -0,0 +1,32 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package stoppable + +import ( + "testing" +) + +// Unit test of Status.String. +func TestStatus_String(t *testing.T) { + testValues := []struct { + status Status + expected string + }{ + {Running, "running"}, + {Stopping, "stopping"}, + {Stopped, "stopped"}, + {100, "INVALID STATUS: 100"}, + } + + for i, val := range testValues { + if val.status.String() != val.expected { + t.Errorf("String did not return the expected value (%d)."+ + "\nexpected: %s\nreceived: %s", i, val.status.String(), val.expected) + } + } +} diff --git a/stoppable/stoppable.go b/stoppable/stoppable.go index 06947eb3bf5ff5ae3523b927e8fb7792848fd762..b5b072d1424feddf97cd029fa08d519ef2998c01 100644 --- a/stoppable/stoppable.go +++ b/stoppable/stoppable.go @@ -7,11 +7,80 @@ package stoppable -import "time" +import ( + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "strings" + "time" +) -// Interface for stopping a goroutine. +// Error message returned after a comms operations ends and finds that its +// parent thread is stopping or stopped. +const ( + errKey = "[StoppableNotRunning]" + ErrMsg = "stoppable %q is not running, exiting %s early " + errKey + timeoutErr = "timed out after %s waiting for the stoppable to stop for %q" +) + +// pollPeriod is the duration to wait between polls to see of stoppables are +// stopped. +const pollPeriod = 100 * time.Millisecond + +// Stoppable interface for stopping a goroutine. All functions are thread safe. type Stoppable interface { - Close(timeout time.Duration) error - IsRunning() bool + // Name returns the name of the Stoppable. Name() string + + // GetStatus returns the status of the Stoppable. + GetStatus() Status + + // IsRunning returns true if the Stoppable is running. + IsRunning() bool + + // IsStopping returns true if Stoppable is marked as stopping. + IsStopping() bool + + // IsStopped returns true if Stoppable is marked as stopped. + IsStopped() bool + + // Close marks the Stoppable as stopping and issues a close signal to the + // Stoppable or any children it may have. + Close() error +} + +// WaitForStopped polls the stoppable and all its children to see if they are +// stopped. Returns an error if its times out waiting for all children to stop. +func WaitForStopped(s Stoppable, timeout time.Duration) error { + done := make(chan struct{}) + + // Launch the processes to check if all stoppables are stopped in separate + // goroutine so that when the timeout is reached, no time is wasted exiting + go func() { + for !s.IsStopped() { + time.Sleep(pollPeriod) + } + + select { + case done <- struct{}{}: + case <-time.NewTimer(50 * time.Millisecond).C: + } + }() + + select { + case <-done: + jww.INFO.Printf("All stoppables have stopped for %q.", s.Name()) + return nil + case <-time.NewTimer(timeout).C: + return errors.Errorf(timeoutErr, timeout, s.Name()) + } +} + +// CheckErr returns true if the error contains a stoppable error message. This +// function is used by callers to determine if a sub function quit due to a +// stoppable closing and tells the caller to exit. +func CheckErr(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), errKey) } diff --git a/stoppable/stoppable_test.go b/stoppable/stoppable_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0f070317aedef9fca29a0b66d023b4e7d151517f --- /dev/null +++ b/stoppable/stoppable_test.go @@ -0,0 +1,123 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package stoppable + +import ( + "fmt" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "os" + "strconv" + "testing" + "time" +) + +func TestMain(m *testing.M) { + jww.SetStdoutThreshold(jww.LevelTrace) + + os.Exit(m.Run()) +} + +// Tests that WaitForStopped does not return an error when all children are +// stopped. +func TestWaitForStopped(t *testing.T) { + m := newTestMulti() + + err := m.Close() + if err != nil { + t.Errorf("Failed to close multi stoppable: %+v", err) + } + + err = WaitForStopped(m, 2*time.Second) + if err != nil { + t.Errorf("WaitForStopped returned an error: %+v", err) + } +} + +// Error path: tests that WaitForStopped returns an error if the timeout is +// reached before all stoppables are checked. +func TestWaitForStopped_TimeoutError(t *testing.T) { + m := newTestMulti() + + err := m.Close() + if err != nil { + t.Errorf("Failed to close multi stoppable: %+v", err) + } + + expectedErr := fmt.Sprintf(timeoutErr, time.Duration(0), m.Name()) + + err = WaitForStopped(m, 0) + if err == nil || err.Error() != expectedErr { + t.Errorf("WaitForStopped did not return the expected error."+ + "\nexpected: %s\nrecieved: %+v", expectedErr, err) + } +} + +// Tests that TestCheckErr returns true for stoppable errors and false for all +// other errors +func TestCheckErr(t *testing.T) { + testValues := []struct { + err error + expected bool + }{ + {errors.Errorf(ErrMsg, "testThre", "testFunc"), true}, + {errors.Errorf(ErrMsg, "", ""), true}, + {errors.Errorf(errKey), true}, + {errors.Errorf("Random error"), false}, + {errors.Errorf(""), false}, + {nil, false}, + } + + for i, val := range testValues { + result := CheckErr(val.err) + if result != val.expected { + t.Errorf("CheckErr failed to return the expected value (%d)."+ + "\nexpected: %t\nreceived: %t", i, val.expected, result) + } + } +} + +// newTestMulti creates a new Multi Stoppable that has many Single and Multi +// stoppable children. +func newTestMulti() *Multi { + singles := make([]*Single, 15) + for i := range singles { + singles[i] = NewSingle("testSingle_" + strconv.Itoa(i)) + go func(single *Single) { + <-single.Quit() + time.Sleep(600 * time.Millisecond) + single.ToStopped() + }(singles[i]) + } + + m := NewMulti("testMulti") + for _, s := range singles[:5] { + m.Add(s) + } + m0 := NewMulti("testMulti_0") + for _, s := range singles[5:8] { + m0.Add(s) + } + m.Add(m0) + m1 := NewMulti("testMulti_1") + for _, s := range singles[8:10] { + m1.Add(s) + } + m2 := NewMulti("testMulti_2") + for _, s := range singles[10:13] { + m2.Add(s) + } + m1.Add(m2) + m.Add(m1) + for _, s := range singles[13:] { + m.Add(s) + } + m.Add(NewMulti("testMulti_3")) + + return m +} diff --git a/storage/auth/sentRequest.go b/storage/auth/sentRequest.go index 77901412056ca674b8756d9e661edff7bd4d8076..af09eb475484cd1fc25f9fd18df197c5a2ff07e0 100644 --- a/storage/auth/sentRequest.go +++ b/storage/auth/sentRequest.go @@ -8,8 +8,10 @@ package auth import ( + "encoding/hex" "encoding/json" "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/primitives/format" @@ -51,8 +53,8 @@ func loadSentRequest(kv *versioned.KV, partner *id.ID, grp *cyclic.Group) (*Sent "SentRequest Auth with %s", partner) } - historicalPrivKey := grp.NewInt(1) - if err = historicalPrivKey.GobDecode(srd.PartnerHistoricalPubKey); err != nil { + historicalPubKey := grp.NewInt(1) + if err = historicalPubKey.GobDecode(srd.PartnerHistoricalPubKey); err != nil { return nil, errors.WithMessagef(err, "Failed to decode historical "+ "private key with %s for SentRequest Auth", partner) } @@ -72,10 +74,21 @@ func loadSentRequest(kv *versioned.KV, partner *id.ID, grp *cyclic.Group) (*Sent fp := format.Fingerprint{} copy(fp[:], srd.Fingerprint) + jww.INFO.Printf("loadSentRequest partner: %s", + hex.EncodeToString(partner[:])) + jww.INFO.Printf("loadSentRequest historicalPubKey: %s", + hex.EncodeToString(historicalPubKey.Bytes())) + jww.INFO.Printf("loadSentRequest myPrivKey: %s", + hex.EncodeToString(myPrivKey.Bytes())) + jww.INFO.Printf("loadSentRequest myPubKey: %s", + hex.EncodeToString(myPubKey.Bytes())) + jww.INFO.Printf("loadSentRequest fingerprint: %s", + hex.EncodeToString(fp[:])) + return &SentRequest{ kv: kv, partner: partner, - partnerHistoricalPubKey: historicalPrivKey, + partnerHistoricalPubKey: historicalPubKey, myPrivKey: myPrivKey, myPubKey: myPubKey, fingerprint: fp, @@ -93,13 +106,24 @@ func (sr *SentRequest) save() error { return err } - historicalPrivKey, err := sr.partnerHistoricalPubKey.GobEncode() + historicalPubKey, err := sr.partnerHistoricalPubKey.GobEncode() if err != nil { return err } + jww.INFO.Printf("saveSentRequest partner: %s", + hex.EncodeToString(sr.partner[:])) + jww.INFO.Printf("saveSentRequest historicalPubKey: %s", + hex.EncodeToString(sr.partnerHistoricalPubKey.Bytes())) + jww.INFO.Printf("saveSentRequest myPrivKey: %s", + hex.EncodeToString(sr.myPrivKey.Bytes())) + jww.INFO.Printf("saveSentRequest myPubKey: %s", + hex.EncodeToString(sr.myPubKey.Bytes())) + jww.INFO.Printf("saveSentRequest fingerprint: %s", + hex.EncodeToString(sr.fingerprint[:])) + ipd := sentRequestDisk{ - PartnerHistoricalPubKey: historicalPrivKey, + PartnerHistoricalPubKey: historicalPubKey, MyPrivKey: privKey, MyPubKey: pubKey, Fingerprint: sr.fingerprint[:], diff --git a/storage/auth/store.go b/storage/auth/store.go index 9ce2e6154e3b5bce28f4df6c36cbffca43255fc8..341bc74c01639163b9c22666263ab23b7812b1a8 100644 --- a/storage/auth/store.go +++ b/storage/auth/store.go @@ -92,7 +92,7 @@ func LoadStore(kv *versioned.KV, grp *cyclic.Group, privKeys []*cyclic.Int) (*St return nil, errors.WithMessagef(err, "Failed to "+ "unmarshal SentRequestMap") } - + jww.TRACE.Printf("%d found when loading AuthStore", len(requestList)) for _, rDisk := range requestList { r := &request{ rt: RequestType(rDisk.T), @@ -117,7 +117,6 @@ func LoadStore(kv *versioned.KV, grp *cyclic.Group, privKeys []*cyclic.Int) (*St PrivKey: nil, Request: r, } - rid = sr.partner r.sent = sr @@ -143,7 +142,6 @@ func LoadStore(kv *versioned.KV, grp *cyclic.Group, privKeys []*cyclic.Int) (*St func (s *Store) save() error { requestIDList := make([]requestDisk, len(s.requests)) - index := 0 for pid, r := range s.requests { rDisk := requestDisk{ @@ -158,7 +156,6 @@ func (s *Store) save() error { if err != nil { return err } - obj := versioned.Object{ Version: requestMapVersion, Timestamp: netTime.Now(), @@ -206,6 +203,7 @@ func (s *Store) AddSent(partner *id.ID, partnerHistoricalPubKey, myPrivKey, jww.INFO.Printf("AddSent PUBKEY FINGERPRINT: %v", sr.fingerprint) jww.INFO.Printf("AddSent PUBKEY: %v", sr.myPubKey.Bytes()) + jww.INFO.Printf("AddSent Partner: %s", partner) s.fingerprints[sr.fingerprint] = fingerprint{ Type: Specific, @@ -219,7 +217,7 @@ func (s *Store) AddSent(partner *id.ID, partnerHistoricalPubKey, myPrivKey, func (s *Store) AddReceived(c contact.Contact) error { s.mux.Lock() defer s.mux.Unlock() - + jww.DEBUG.Printf("AddReceived new contact: %s", c.ID) if _, ok := s.requests[*c.ID]; ok { return errors.Errorf("Cannot add contact for partner "+ "%s, one already exists", c.ID) diff --git a/storage/cmix/roundKeys_test.go b/storage/cmix/roundKeys_test.go index 6f69bd6628cdbe2e7bbda3a58b42adcdc01f2bce..0b701375bab6fee74bd509bab1d208f33dcde61b 100644 --- a/storage/cmix/roundKeys_test.go +++ b/storage/cmix/roundKeys_test.go @@ -23,40 +23,28 @@ import ( func TestRoundKeys_Encrypt_Consistency(t *testing.T) { const numKeys = 5 - expectedPayload := []byte{107, 20, 177, 34, 255, 243, 201, 126, 124, 105, 4, - 62, 204, 52, 56, 2, 60, 196, 105, 167, 80, 78, 189, 83, 248, 113, 207, - 34, 255, 55, 37, 48, 75, 130, 200, 218, 88, 16, 29, 171, 26, 26, 77, 59, - 244, 111, 117, 236, 102, 86, 32, 31, 223, 26, 151, 112, 191, 183, 152, - 18, 104, 58, 49, 42, 77, 233, 163, 193, 36, 7, 44, 173, 99, 65, 24, 127, - 197, 96, 51, 69, 8, 154, 35, 119, 147, 80, 113, 55, 173, 129, 151, 195, - 56, 11, 92, 2, 181, 135, 1, 114, 12, 197, 55, 252, 123, 89, 92, 185, 87, - 215, 193, 203, 199, 224, 58, 173, 193, 159, 166, 22, 60, 138, 97, 15, - 173, 213, 45, 236, 7, 66, 39, 168, 21, 26, 210, 66, 176, 135, 131, 113, - 157, 53, 120, 128, 187, 167, 127, 170, 248, 215, 158, 18, 61, 158, 137, - 62, 120, 254, 114, 93, 78, 11, 13, 104, 94, 232, 98, 108, 238, 42, 181, - 221, 128, 124, 188, 119, 13, 101, 7, 61, 85, 19, 20, 140, 32, 101, 39, - 151, 93, 134, 78, 155, 100, 110, 192, 76, 62, 249, 91, 105, 225, 180, - 95, 197, 101, 80, 8, 93, 139, 78, 109, 197, 255, 218, 6, 167, 49, 61, - 184, 178, 174, 155, 147, 238, 228, 169, 27, 175, 119, 76, 217, 240, 1, - 134, 114, 3, 179, 223, 152, 68, 152, 221, 44, 128, 55, 165, 206, 116, - 88, 188, 72, 41, 41, 9, 67, 188, 182, 118, 213, 25, 237, 146, 170, 80, - 42, 101, 230, 87, 244, 170, 176, 110, 94, 43, 110, 200, 54, 126, 206, - 252, 182, 21, 207, 142, 170, 150, 34, 155, 99, 110, 131, 120, 137, 255, - 200, 132, 249, 213, 180, 121, 235, 126, 30, 149, 18, 8, 159, 153, 73, - 71, 104, 246, 231, 168, 201, 108, 42, 10, 110, 35, 183, 160, 15, 11, - 171, 117, 0, 87, 251, 218, 121, 155, 237, 58, 24, 139, 217, 62, 238, - 255, 116, 172, 135, 221, 207, 163, 214, 62, 1, 144, 245, 233, 147, 188, - 67, 97, 161, 79, 109, 129, 114, 21, 183, 66, 54, 242, 120, 91, 158, 35, - 110, 167, 44, 54, 87, 208, 145, 212, 59, 160, 115, 214, 146, 201, 199, - 104, 86, 140, 131, 189, 146, 47, 165, 197, 90, 100, 105, 16, 223, 96, - 86, 132, 221, 190, 175, 241, 121, 157, 19, 190, 243, 191, 116, 92, 31, - 209, 147, 7, 233, 188, 114, 88, 225, 180, 52, 139, 70, 88, 193, 111, - 49, 209, 4, 19, 135, 206, 56, 164, 230, 222, 219, 153, 94, 163, 168, - 181, 185, 206, 124, 13, 179, 32, 93, 85, 6, 179, 57, 197, 89, 254, - 180, 133, 147, 174, 182, 38, 8, 127, 20, 133, 100, 20, 228, 62, 252, - 175, 50, 239, 179, 108, 59, 222, 29, 113, 140, 2, 104, 167, 175, 193, - 208, 149, 24, 135, 165, 106, 249, 164, 122, 139, 169, 193, 39, 209, 132, - 238, 23, 153, 115, 200, 104, 31} + expectedPayload := []byte{240, 199, 83, 226, 28, 164, 104, 139, 171, 255, 234, 86, 170, 65, 29, 254, 100, 4, 81, + 112, 154, 115, 224, 245, 29, 60, 226, 209, 135, 75, 108, 62, 95, 185, 211, 56, 83, 55, 250, 159, 173, 176, 137, + 181, 1, 155, 228, 223, 170, 232, 71, 225, 55, 27, 189, 218, 146, 74, 134, 133, 105, 17, 69, 105, 160, 60, 206, + 32, 244, 175, 98, 142, 217, 27, 92, 132, 225, 146, 171, 59, 2, 191, 220, 125, 212, 81, 114, 98, 75, 253, 93, + 126, 48, 230, 249, 118, 215, 90, 231, 126, 43, 235, 151, 191, 23, 77, 147, 98, 212, 86, 89, 42, 189, 24, 124, + 189, 201, 184, 82, 152, 255, 137, 119, 21, 74, 118, 157, 114, 229, 232, 36, 185, 104, 101, 132, 23, 79, 65, 195, + 53, 222, 27, 66, 80, 123, 252, 109, 254, 44, 120, 114, 126, 237, 159, 252, 185, 187, 95, 255, 31, 41, 245, 225, + 95, 101, 118, 190, 233, 44, 5, 42, 239, 140, 70, 216, 211, 129, 43, 189, 1, 11, 111, 2, 64, 254, 44, 87, 164, + 28, 188, 227, 1, 32, 134, 183, 156, 84, 222, 79, 27, 210, 124, 46, 153, 56, 122, 117, 17, 171, 85, 232, 112, + 170, 10, 31, 115, 17, 119, 233, 150, 200, 183, 198, 74, 70, 179, 135, 27, 195, 190, 56, 126, 143, 226, 93, 16, + 46, 147, 248, 128, 124, 182, 254, 187, 223, 187, 54, 181, 62, 89, 202, 176, 25, 249, 139, 167, 26, 98, 143, 3, + 78, 54, 116, 201, 6, 33, 158, 225, 254, 106, 15, 6, 175, 96, 2, 63, 0, 59, 188, 124, 120, 147, 95, 24, 26, 115, + 235, 154, 240, 65, 226, 133, 91, 249, 223, 55, 122, 0, 76, 225, 104, 101, 242, 46, 136, 122, 127, 159, 0, 9, + 210, 42, 181, 31, 94, 20, 106, 175, 195, 56, 223, 165, 217, 164, 93, 55, 190, 253, 192, 249, 117, 226, 222, 65, + 82, 136, 36, 58, 3, 246, 76, 101, 24, 20, 50, 89, 22, 144, 184, 38, 82, 103, 2, 48, 59, 73, 75, 58, 33, 206, 49, + 88, 201, 44, 176, 242, 248, 254, 127, 101, 62, 57, 103, 75, 213, 73, 30, 146, 223, 118, 104, 126, 189, 179, 132, + 25, 183, 178, 65, 131, 72, 121, 42, 170, 40, 186, 65, 73, 175, 234, 52, 10, 171, 36, 165, 24, 156, 12, 198, 100, + 77, 137, 91, 221, 152, 219, 207, 244, 44, 126, 178, 119, 133, 147, 158, 54, 188, 52, 10, 63, 138, 180, 44, 29, + 40, 236, 255, 163, 208, 2, 212, 184, 50, 157, 82, 199, 90, 1, 205, 214, 143, 123, 92, 210, 88, 98, 182, 197, 49, + 170, 100, 143, 145, 9, 156, 0, 45, 59, 196, 6, 8, 157, 98, 15, 111, 162, 51, 12, 223, 0, 173, 187, 178, 1, 156, + 68, 183, 64, 178, 250, 40, 65, 50, 161, 96, 163, 106, 14, 43, 179, 75, 199, 15, 223, 192, 121, 144, 223, 167, + 254, 150, 188} expectedKmacs := [][]byte{{110, 235, 79, 128, 16, 94, 181, 95, 101, 152, 187, 204, 87, 236, 211, 102, 88, 130, 191, 103, 23, 229, diff --git a/storage/e2e/manager.go b/storage/e2e/manager.go index 802e81ebe2329f5bd3714500e0229071eaea3bd3..a978beeae77c2fffa723c15f495ebf1fa4925c5c 100644 --- a/storage/e2e/manager.go +++ b/storage/e2e/manager.go @@ -8,6 +8,8 @@ package e2e import ( + "bytes" + "encoding/base64" "fmt" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -17,6 +19,8 @@ import ( "gitlab.com/elixxir/crypto/cyclic" dh "gitlab.com/elixxir/crypto/diffieHellman" "gitlab.com/xx_network/primitives/id" + "golang.org/x/crypto/blake2b" + "sort" ) const managerPrefix = "Manager{partner:%s}" @@ -108,6 +112,25 @@ func loadManager(ctx *context, kv *versioned.KV, partnerID *id.ID) (*Manager, er return m, nil } +// clearManager removes the relationship between the partner +// and deletes the Send and Receive sessions. This includes the +// sessions and the key vectors +func clearManager(m *Manager, kv *versioned.KV) error { + kv = kv.Prefix(fmt.Sprintf(managerPrefix, m.partner)) + + if err := DeleteRelationship(m); err != nil { + return errors.WithMessage(err, + "Failed to delete relationship") + } + + if err := utility.DeleteCyclicKey(m.kv, originPartnerPubKey); err != nil { + jww.FATAL.Panicf("Failed to delete %s: %+v", originPartnerPubKey, + err) + } + + return nil +} + // NewReceiveSession creates a new Receive session using the latest private key // this user has sent and the new public key received from the partner. If the // session already exists, then it will not be overwritten and the extant @@ -198,3 +221,24 @@ func (m *Manager) GetMyOriginPrivateKey() *cyclic.Int { func (m *Manager) GetPartnerOriginPublicKey() *cyclic.Int { return m.originPartnerPubKey.DeepCopy() } + +const relationshipFpLength = 15 + +// GetRelationshipFingerprint returns a unique fingerprint for an E2E +// relationship. The fingerprint is a base 64 encoded hash of of the two +// relationship fingerprints truncated to 15 characters. +func (m *Manager) GetRelationshipFingerprint() string { + // Sort fingerprints + fps := [][]byte{m.receive.fingerprint, m.send.fingerprint} + less := func(i, j int) bool { return bytes.Compare(fps[i], fps[j]) == -1 } + sort.Slice(fps, less) + + // Hash fingerprints + h, _ := blake2b.New256(nil) + for _, fp := range fps { + h.Write(fp) + } + + // Base 64 encode hash and truncate + return base64.StdEncoding.EncodeToString(h.Sum(nil))[:relationshipFpLength] +} diff --git a/storage/e2e/manager_test.go b/storage/e2e/manager_test.go index 907bbff91b561d2267b71371356ae7bf6f332b86..114afac841407a4c4f8a0116ac452b7bd01c68cc 100644 --- a/storage/e2e/manager_test.go +++ b/storage/e2e/manager_test.go @@ -9,12 +9,14 @@ package e2e import ( "bytes" + "encoding/base64" "fmt" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/elixxir/ekv" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" + "golang.org/x/crypto/blake2b" "math/rand" "reflect" "testing" @@ -68,6 +70,30 @@ func TestLoadManager(t *testing.T) { } } +// Unit test for clearManager +func TestManager_ClearManager(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatalf("clearManager error: " + + "Did not panic when loading deleted manager") + } + }() + + // Set up expected and test values + expectedM, kv := newTestManager(t) + + err := clearManager(expectedM, kv) + if err != nil { + t.Fatalf("clearManager returned an error: %v", err) + } + + // Attempt to load relationship + _, err = loadManager(expectedM.ctx, kv, expectedM.partner) + if err != nil { + t.Errorf("loadManager() returned an error: %v", err) + } +} + // Tests happy path of Manager.NewReceiveSession. func TestManager_NewReceiveSession(t *testing.T) { // Set up test values @@ -283,3 +309,65 @@ func managersEqual(expected, received *Manager, t *testing.T) bool { return equal } + +// Unit test of Manager.GetRelationshipFingerprint. +func TestManager_GetRelationshipFingerprint(t *testing.T) { + m, _ := newTestManager(t) + m.receive.fingerprint = []byte{5} + m.send.fingerprint = []byte{10} + h, _ := blake2b.New256(nil) + h.Write(append(m.receive.fingerprint, m.send.fingerprint...)) + expected := base64.StdEncoding.EncodeToString(h.Sum(nil))[:relationshipFpLength] + + fp := m.GetRelationshipFingerprint() + if fp != expected { + t.Errorf("GetRelationshipFingerprint did not return the expected "+ + "fingerprint.\nexpected: %s\nreceived: %s", expected, fp) + } + + // Flip the order and show that the output is the same. + m.receive.fingerprint, m.send.fingerprint = m.send.fingerprint, m.receive.fingerprint + + fp = m.GetRelationshipFingerprint() + if fp != expected { + t.Errorf("GetRelationshipFingerprint did not return the expected "+ + "fingerprint.\nexpected: %s\nreceived: %s", expected, fp) + } +} + +// Tests the consistency of the output of Manager.GetRelationshipFingerprint. +func TestManager_GetRelationshipFingerprint_Consistency(t *testing.T) { + m, _ := newTestManager(t) + prng := rand.New(rand.NewSource(42)) + expectedFps := []string{ + "GmeTCfxGOqRqeID", "gbpJjHd3tIe8BKy", "2/ZdG+WNzODJBiF", + "+V1ySeDLQfQNSkv", "23OMC+rBmCk+gsu", "qHu5MUVs83oMqy8", + "kuXqxsezI0kS9Bc", "SlEhsoZ4BzAMTtr", "yG8m6SPQfV/sbTR", + "j01ZSSm762TH7mj", "SKFDbFvsPcohKPw", "6JB5HK8DHGwS4uX", + "dU3mS1ujduGD+VY", "BDXAy3trbs8P4mu", "I4HoXW45EwWR0oD", + "661YH2l2jfOkHbA", "cSS9ZyTOQKVx67a", "ojfubzDIsMNYc/t", + "2WrEw83Yz6Rhq9I", "TQILxBIUWMiQS2j", "rEqdieDTXJfCQ6I", + } + + for i, expected := range expectedFps { + prng.Read(m.receive.fingerprint) + prng.Read(m.send.fingerprint) + + fp := m.GetRelationshipFingerprint() + if fp != expected { + t.Errorf("GetRelationshipFingerprint did not return the expected "+ + "fingerprint (%d).\nexpected: %s\nreceived: %s", i, expected, fp) + } + + // Flip the order and show that the output is the same. + m.receive.fingerprint, m.send.fingerprint = m.send.fingerprint, m.receive.fingerprint + + fp = m.GetRelationshipFingerprint() + if fp != expected { + t.Errorf("GetRelationshipFingerprint did not return the expected "+ + "fingerprint (%d).\nexpected: %s\nreceived: %s", i, expected, fp) + } + + // fmt.Printf("\"%s\",\n", fp) // Uncomment to reprint expected values + } +} diff --git a/storage/e2e/relationship.go b/storage/e2e/relationship.go index a492a72ed51d0f48f08657d786bc8dc6a3c861ed..e0e7723d6c332c36a1ac9ceae0753e3174da4ba6 100644 --- a/storage/e2e/relationship.go +++ b/storage/e2e/relationship.go @@ -71,7 +71,7 @@ func NewRelationship(manager *Manager, t RelationshipType, if err := s.save(); err != nil { jww.FATAL.Panicf("Failed to Send session after setting to "+ - "confimred: %+v", err) + "confirmed: %+v", err) } r.addSession(s) @@ -84,6 +84,33 @@ func NewRelationship(manager *Manager, t RelationshipType, return r } +// DeleteRelationship removes all relationship and +// relationship adjacent information from storage +func DeleteRelationship(manager *Manager) error { + + // Delete the send information + sendKv := manager.kv.Prefix(Send.prefix()) + manager.send.Delete() + if err := deleteRelationshipFingerprint(sendKv); err != nil { + return err + } + if err := sendKv.Delete(relationshipKey, currentRelationshipVersion); err != nil { + return errors.Errorf("Could not delete send relationship: %v", err) + } + + // Delete the receive information + receiveKv := manager.kv.Prefix(Receive.prefix()) + manager.receive.Delete() + if err := deleteRelationshipFingerprint(receiveKv); err != nil { + return err + } + if err := receiveKv.Delete(relationshipKey, currentRelationshipVersion); err != nil { + return errors.Errorf("Could not delete receive relationship: %v", err) + } + + return nil +} + func LoadRelationship(manager *Manager, t RelationshipType) (*relationship, error) { kv := manager.kv.Prefix(t.prefix()) @@ -166,6 +193,16 @@ func (r *relationship) unmarshal(b []byte) error { return nil } +func (r *relationship) Delete() { + r.mux.Lock() + defer r.mux.Unlock() + for _, s := range r.sessions { + delete(r.sessionByID, s.GetID()) + s.Delete() + } + +} + func (r *relationship) AddSession(myPrivKey, partnerPubKey, baseKey *cyclic.Int, trigger SessionID, negotiationStatus Negotiation, e2eParams params.E2ESessionParams) *Session { diff --git a/storage/e2e/relationshipFingerprint.go b/storage/e2e/relationshipFingerprint.go index a3702655319ccf0f14323d31f5b32b3fe59cb825..29b80896dfc078b8ab1a7cefa348a7860498aec2 100644 --- a/storage/e2e/relationshipFingerprint.go +++ b/storage/e2e/relationshipFingerprint.go @@ -57,3 +57,9 @@ func loadRelationshipFingerprint(kv *versioned.KV) []byte { } return obj.Data } + +// deleteRelationshipFingerprint is a helper function which deletes a fingerprint from store +func deleteRelationshipFingerprint(kv *versioned.KV) error { + return kv.Delete(relationshipFingerprintKey, + currentRelationshipVersion) +} diff --git a/storage/e2e/relationship_test.go b/storage/e2e/relationship_test.go index 457e4f6dcd508abd2014248db7738f46badf947a..1ad211fcd55a389e758711a22ac8bcff24fdd76e 100644 --- a/storage/e2e/relationship_test.go +++ b/storage/e2e/relationship_test.go @@ -68,6 +68,64 @@ func TestLoadRelationship(t *testing.T) { } } +// Shows that a deleted Relationship can no longer be pulled from store +func TestDeleteRelationship(t *testing.T) { + mgr := makeTestRelationshipManager(t) + + // Generate send relationship + mgr.send = NewRelationship(mgr, Send, params.GetDefaultE2ESessionParams()) + if err := mgr.send.save(); err != nil { + t.Fatal(err) + } + + // Generate receive relationship + mgr.receive = NewRelationship(mgr, Receive, params.GetDefaultE2ESessionParams()) + if err := mgr.receive.save(); err != nil { + t.Fatal(err) + } + + err := DeleteRelationship(mgr) + if err != nil { + t.Fatalf("DeleteRelationship error: Could not delete manager: %v", err) + } + + _, err = LoadRelationship(mgr, Send) + if err == nil { + t.Fatalf("DeleteRelationship error: Should not have loaded deleted relationship: %v", err) + } + + _, err = LoadRelationship(mgr, Receive) + if err == nil { + t.Fatalf("DeleteRelationship error: Should not have loaded deleted relationship: %v", err) + } +} + +// Shows that a deleted relationship fingerprint can no longer be pulled from store +func TestRelationship_deleteRelationshipFingerprint(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatalf("deleteRelationshipFingerprint error: " + + "Did not panic when loading deleted fingerprint") + } + }() + + mgr := makeTestRelationshipManager(t) + sb := NewRelationship(mgr, Send, params.GetDefaultE2ESessionParams()) + + err := sb.save() + if err != nil { + t.Fatal(err) + } + + err = deleteRelationshipFingerprint(mgr.kv) + if err != nil { + t.Fatalf("deleteRelationshipFingerprint error: "+ + "Could not delete fingerprint: %v", err) + } + + loadRelationshipFingerprint(mgr.kv) +} + // Shows that Relationship returns a valid session buff func TestNewRelationshipBuff(t *testing.T) { mgr := makeTestRelationshipManager(t) diff --git a/storage/e2e/session.go b/storage/e2e/session.go index ad684725a04918389f706566e44642c6c1fbaf16..9f74ebf35efe310214237e6da36f9f4029c872be 100644 --- a/storage/e2e/session.go +++ b/storage/e2e/session.go @@ -105,8 +105,8 @@ func newSession(ship *relationship, t RelationshipType, myPrivKey, partnerPubKey negotiationStatus Negotiation, e2eParams params.E2ESessionParams) *Session { if e2eParams.MinKeys < 10 { - jww.FATAL.Panicf("Cannot create a session with a minnimum number " + - "of keys less than 10") + jww.FATAL.Panicf("Cannot create a session with a minimum number "+ + "of keys (%d) less than 10", e2eParams.MinKeys) } session := &Session{ @@ -201,7 +201,8 @@ func (s *Session) save() error { /*METHODS*/ // Done all unused key fingerprints -// delete this session and its key states from the storage + +// Delete removes this session and its key states from the storage func (s *Session) Delete() { s.mux.Lock() defer s.mux.Unlock() @@ -221,7 +222,7 @@ func (s *Session) Delete() { } } -//Gets the base key. +// GetBaseKey retrieves the base key. func (s *Session) GetBaseKey() *cyclic.Int { // no lock is needed because this cannot be edited return s.baseKey.DeepCopy() diff --git a/storage/e2e/store.go b/storage/e2e/store.go index e647cacab042388b667d383f670c8082c450b591..2a8006902a9adf5a94704175f55f548f61eb0d7a 100644 --- a/storage/e2e/store.go +++ b/storage/e2e/store.go @@ -14,6 +14,7 @@ import ( "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/storage/utility" "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/diffieHellman" "gitlab.com/elixxir/crypto/fastRNG" @@ -180,13 +181,28 @@ func (s *Store) AddPartner(partnerID *id.ID, partnerPubKey, myPrivKey *cyclic.In s.managers[*partnerID] = m if err := s.save(); err != nil { - jww.FATAL.Printf("Failed to add Parter %s: Save of store failed: %s", + jww.FATAL.Printf("Failed to add Partner %s: Save of store failed: %s", partnerID, err) } return nil } +// DeletePartner removes the associated contact from the E2E store +func (s *Store) DeletePartner(partnerId *id.ID) error { + m, ok := s.managers[*partnerId] + if !ok { + return errors.New(NoPartnerErrorStr) + } + + if err := clearManager(m, s.kv); err != nil { + return errors.WithMessagef(err, "Could not remove partner %s from store", partnerId) + } + + delete(s.managers, *partnerId) + return s.save() +} + func (s *Store) GetPartner(partnerID *id.ID) (*Manager, error) { s.mux.RLock() defer s.mux.RUnlock() @@ -200,6 +216,28 @@ func (s *Store) GetPartner(partnerID *id.ID) (*Manager, error) { return m, nil } +// GetPartnerContact find the partner with the given ID and assembles and +// returns a contact.Contact with their ID and DH key. An error is returned if +// no partner exists for the given ID. +func (s *Store) GetPartnerContact(partnerID *id.ID) (contact.Contact, error) { + s.mux.RLock() + defer s.mux.RUnlock() + + // Get partner + m, exists := s.managers[*partnerID] + if !exists { + return contact.Contact{}, errors.New(NoPartnerErrorStr) + } + + // Assemble Contact + c := contact.Contact{ + ID: m.GetPartnerID(), + DhPubKey: m.GetPartnerOriginPublicKey(), + } + + return c, nil +} + // PopKey pops a key for use based upon its fingerprint. func (s *Store) PopKey(f format.Fingerprint) (*Key, bool) { return s.fingerprints.Pop(f) diff --git a/storage/e2e/store_test.go b/storage/e2e/store_test.go index 944b2517df666e536db085d6ce3f0a001316aa2d..016184b58e803a9b92d763be3c880b8a1b4dc05e 100644 --- a/storage/e2e/store_test.go +++ b/storage/e2e/store_test.go @@ -11,6 +11,7 @@ import ( "bytes" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/diffieHellman" "gitlab.com/elixxir/crypto/fastRNG" @@ -97,7 +98,10 @@ func TestStore_AddPartner(t *testing.T) { expectedManager := newManager(s.context, s.kv, partnerID, s.dhPrivateKey, pubKey, p, p) - s.AddPartner(partnerID, pubKey, s.dhPrivateKey, p, p) + err := s.AddPartner(partnerID, pubKey, s.dhPrivateKey, p, p) + if err != nil { + t.Fatalf("AddPartner returned an error: %v", err) + } m, exists := s.managers[*partnerID] if !exists { @@ -110,6 +114,30 @@ func TestStore_AddPartner(t *testing.T) { } } +// Unit test for DeletePartner +func TestStore_DeletePartner(t *testing.T) { + s, _, _ := makeTestStore() + partnerID := id.NewIdFromUInt(rand.Uint64(), id.User, t) + pubKey := diffieHellman.GeneratePublicKey(s.dhPrivateKey, s.grp) + p := params.GetDefaultE2ESessionParams() + + err := s.AddPartner(partnerID, pubKey, s.dhPrivateKey, p, p) + if err != nil { + t.Fatalf("DeletePartner error: Could not add partner in set up: %v", err) + } + + err = s.DeletePartner(partnerID) + if err != nil { + t.Fatalf("DeletePartner received an error: %v", err) + } + + _, err = s.GetPartner(partnerID) + if err == nil { + t.Errorf("DeletePartner error: Should not be able to pull deleted partner from store") + } + +} + // Tests happy path of Store.GetPartner. func TestStore_GetPartner(t *testing.T) { s, _, _ := makeTestStore() @@ -118,7 +146,7 @@ func TestStore_GetPartner(t *testing.T) { p := params.GetDefaultE2ESessionParams() expectedManager := newManager(s.context, s.kv, partnerID, s.dhPrivateKey, pubKey, p, p) - s.AddPartner(partnerID, pubKey, s.dhPrivateKey, p, p) + _ = s.AddPartner(partnerID, pubKey, s.dhPrivateKey, p, p) m, err := s.GetPartner(partnerID) if err != nil { @@ -147,6 +175,41 @@ func TestStore_GetPartner_Error(t *testing.T) { } } +// Tests happy path of Store.GetPartnerContact. +func TestStore_GetPartnerContact(t *testing.T) { + s, _, _ := makeTestStore() + partnerID := id.NewIdFromUInt(rand.Uint64(), id.User, t) + pubKey := diffieHellman.GeneratePublicKey(s.dhPrivateKey, s.grp) + p := params.GetDefaultE2ESessionParams() + expected := contact.Contact{ + ID: partnerID, + DhPubKey: pubKey, + } + _ = s.AddPartner(partnerID, pubKey, s.dhPrivateKey, p, p) + + c, err := s.GetPartnerContact(partnerID) + if err != nil { + t.Errorf("GetPartnerContact() produced an error: %+v", err) + } + + if !reflect.DeepEqual(expected, c) { + t.Errorf("GetPartnerContact() returned wrong Contact."+ + "\nexpected: %s\nreceived: %s", expected, c) + } +} + +// Tests that Store.GetPartnerContact returns an error for non existent partnerID. +func TestStore_GetPartnerContact_Error(t *testing.T) { + s, _, _ := makeTestStore() + partnerID := id.NewIdFromUInt(rand.Uint64(), id.User, t) + + _, err := s.GetPartnerContact(partnerID) + if err == nil || err.Error() != NoPartnerErrorStr { + t.Errorf("GetPartnerContact() did not produce the expected error."+ + "\nexpected: %s\nreceived: %+v", NoPartnerErrorStr, err) + } +} + // Tests happy path of Store.PopKey. func TestStore_PopKey(t *testing.T) { s, _, _ := makeTestStore() diff --git a/storage/hostList/hostList.go b/storage/hostList/hostList.go new file mode 100644 index 0000000000000000000000000000000000000000..5242e7b8483ace3820111aa6c22455c7ba5a0257 --- /dev/null +++ b/storage/hostList/hostList.go @@ -0,0 +1,116 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package hostList + +import ( + "bytes" + "github.com/pkg/errors" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" +) + +// Storage values. +const ( + hostListPrefix = "hostLists" + hostListKey = "hostListIDs" + hostListVersion = 0 +) + +// Error messages. +const ( + getStorageErr = "failed to get host list from storage: %+v" + unmarshallIdErr = "unmarshal host list error: %+v" + unmarshallLenErr = "malformed data: length of data %d incorrect" +) + +type Store struct { + kv *versioned.KV +} + +// NewStore creates a new Store with a prefixed KV. +func NewStore(kv *versioned.KV) *Store { + return &Store{ + kv: kv.Prefix(hostListPrefix), + } +} + +// Store saves the list of host IDs to storage. +func (s *Store) Store(list []*id.ID) error { + obj := &versioned.Object{ + Version: hostListVersion, + Data: marshalHostList(list), + Timestamp: netTime.Now(), + } + + return s.kv.Set(hostListKey, hostListVersion, obj) +} + +// Get returns the host list from storage. +func (s *Store) Get() ([]*id.ID, error) { + obj, err := s.kv.Get(hostListKey, hostListVersion) + if err != nil { + return nil, errors.Errorf(getStorageErr, err) + } + + return unmarshalHostList(obj.Data) +} + +// marshalHostList marshals the list of IDs into a byte slice. +func marshalHostList(list []*id.ID) []byte { + buff := bytes.NewBuffer(nil) + buff.Grow(len(list) * id.ArrIDLen) + + for _, hid := range list { + if hid != nil { + buff.Write(hid.Marshal()) + } else { + buff.Write((&id.ID{}).Marshal()) + } + } + + return buff.Bytes() +} + +// unmarshalHostList unmarshal the host list data into an ID list. An error is +// returned if an ID cannot be unmarshalled or if the data is not of the correct +// length. +func unmarshalHostList(data []byte) ([]*id.ID, error) { + // Return an error if the data is not of the required length + if len(data)%id.ArrIDLen != 0 { + return nil, errors.Errorf(unmarshallLenErr, len(data)) + } + + buff := bytes.NewBuffer(data) + list := make([]*id.ID, 0, len(data)/id.ArrIDLen) + + // Read each ID from data, unmarshal, and add to list + length := id.ArrIDLen + for n := buff.Next(length); len(n) == length; n = buff.Next(length) { + hid, err := id.Unmarshal(n) + if err != nil { + return nil, errors.Errorf(unmarshallIdErr, err) + } + + // If the ID is all zeroes, then treat it as a nil ID. + if *hid == (id.ID{}) { + hid = nil + } + + list = append(list, hid) + } + + return list, nil +} diff --git a/storage/hostList/hostList_test.go b/storage/hostList/hostList_test.go new file mode 100644 index 0000000000000000000000000000000000000000..32780fae0a09db487520596e3748611a1bc5636c --- /dev/null +++ b/storage/hostList/hostList_test.go @@ -0,0 +1,114 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package hostList + +import ( + "fmt" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/primitives/id" + "reflect" + "strings" + "testing" +) + +// Unit test of NewStore. +func TestNewStore(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + expected := &Store{kv: kv.Prefix(hostListPrefix)} + + s := NewStore(kv) + + if !reflect.DeepEqual(expected, s) { + t.Errorf("NewStore did not return the expected object."+ + "\nexpected: %+v\nreceived: %+v", expected, s) + } +} + +// Tests that a host list saved by Store.Store matches the host list returned +// by Store.Get. +func TestStore_Store_Get(t *testing.T) { + s := NewStore(versioned.NewKV(make(ekv.Memstore))) + list := []*id.ID{ + id.NewIdFromString("histID_1", id.Node, t), + nil, + id.NewIdFromString("histID_2", id.Node, t), + id.NewIdFromString("histID_3", id.Node, t), + } + + err := s.Store(list) + if err != nil { + t.Errorf("Store returned an error: %+v", err) + } + + newList, err := s.Get() + if err != nil { + t.Errorf("Get returned an error: %+v", err) + } + + if !reflect.DeepEqual(list, newList) { + t.Errorf("Failed to save and load host list."+ + "\nexpected: %+v\nreceived: %+v", list, newList) + } +} + +// Error path: tests that Store.Get returns an error if not host list is +// saved in storage. +func TestStore_Get_StorageError(t *testing.T) { + s := NewStore(versioned.NewKV(make(ekv.Memstore))) + expectedErr := strings.SplitN(getStorageErr, "%", 2)[0] + + _, err := s.Get() + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("Get failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Tests that a list of IDs that is marshalled using marshalHostList and +// unmarshalled using unmarshalHostList matches the original. +func Test_marshalHostList_unmarshalHostList(t *testing.T) { + list := []*id.ID{ + id.NewIdFromString("histID_1", id.Node, t), + nil, + id.NewIdFromString("histID_2", id.Node, t), + id.NewIdFromString("histID_3", id.Node, t), + } + + data := marshalHostList(list) + + newList, err := unmarshalHostList(data) + if err != nil { + t.Errorf("unmarshalHostList produced an error: %+v", err) + } + + if !reflect.DeepEqual(list, newList) { + t.Errorf("Failed to marshal and unmarshal ID list."+ + "\nexpected: %+v\nreceived: %+v", list, newList) + } +} + +// Error path: tests that unmarshalHostList returns an error if the data is not +// of the correct length. +func Test_unmarshalHostList_InvalidDataErr(t *testing.T) { + data := []byte("Invalid Data") + expectedErr := fmt.Sprintf(unmarshallLenErr, len(data)) + + _, err := unmarshalHostList(data) + if err == nil || err.Error() != expectedErr { + t.Errorf("unmarshalHostList failed to return the expected error."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} diff --git a/storage/ndf.go b/storage/ndf.go index 14cb4147b89a4fa53de52e505627c12dbd3abc15..1b081fd0f68c32b140492cc5a61b4f8052eb2134 100644 --- a/storage/ndf.go +++ b/storage/ndf.go @@ -13,25 +13,25 @@ import ( "gitlab.com/xx_network/primitives/ndf" ) -const baseNdfKey = "baseNdf" +const ndfKey = "ndf" -func (s *Session) SetBaseNDF(def *ndf.NetworkDefinition) { - err := utility.SaveNDF(s.kv, baseNdfKey, def) +func (s *Session) SetNDF(def *ndf.NetworkDefinition) { + err := utility.SaveNDF(s.kv, ndfKey, def) if err != nil { - jww.FATAL.Printf("Failed to dave the base NDF: %s", err) + jww.FATAL.Printf("Failed to dave the NDF: %+v", err) } - s.baseNdf = def + s.ndf = def } -func (s *Session) GetBaseNDF() *ndf.NetworkDefinition { - if s.baseNdf != nil { - return s.baseNdf +func (s *Session) GetNDF() *ndf.NetworkDefinition { + if s.ndf != nil { + return s.ndf } - def, err := utility.LoadNDF(s.kv, baseNdfKey) + def, err := utility.LoadNDF(s.kv, ndfKey) if err != nil { - jww.FATAL.Printf("Could not load the base NDF: %s", err) + jww.FATAL.Printf("Could not load the NDF: %+v", err) } - s.baseNdf = def + s.ndf = def return def } diff --git a/storage/ndf_test.go b/storage/ndf_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ece36461e5368cd110b0004d95fc09bf3f28837f --- /dev/null +++ b/storage/ndf_test.go @@ -0,0 +1,33 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package storage + +import ( + "reflect" + "testing" +) + +func TestSession_SetGetNDF(t *testing.T) { + sess := InitTestingSession(t) + testNdf := getNDF() + sess.SetNDF(testNdf) + + if !reflect.DeepEqual(testNdf, sess.ndf) { + t.Errorf("SetNDF error: "+ + "Unexpected value after setting ndf:"+ + "Expected: %v\n\tReceived: %v", testNdf, sess.ndf) + } + + receivedNdf := sess.GetNDF() + if !reflect.DeepEqual(testNdf, receivedNdf) { + t.Errorf("GetNDF error: "+ + "Unexpected value retrieved from GetNdf:"+ + "Expected: %v\n\tReceived: %v", testNdf, receivedNdf) + + } +} diff --git a/storage/partition/multiPartMessage.go b/storage/partition/multiPartMessage.go index 754c524702498c2c6586d54a42adedf3351a7cc5..3aa8cffb556c3bd23f8afb6f8730830919b3c07f 100644 --- a/storage/partition/multiPartMessage.go +++ b/storage/partition/multiPartMessage.go @@ -30,8 +30,11 @@ type multiPartMessage struct { MessageID uint64 NumParts uint8 PresentParts uint8 - Timestamp time.Time - MessageType message.Type + // Timestamp of message from sender + SenderTimestamp time.Time + // Timestamp in which message was stored in RAM + StorageTimestamp time.Time + MessageType message.Type parts [][]byte kv *versioned.KV @@ -48,13 +51,13 @@ func loadOrCreateMultiPartMessage(sender *id.ID, messageID uint64, if err != nil { if !ekv.Exists(err) { mpm := &multiPartMessage{ - Sender: sender, - MessageID: messageID, - NumParts: 0, - PresentParts: 0, - Timestamp: time.Time{}, - MessageType: 0, - kv: kv, + Sender: sender, + MessageID: messageID, + NumParts: 0, + PresentParts: 0, + SenderTimestamp: time.Time{}, + MessageType: 0, + kv: kv, } if err = mpm.save(); err != nil { jww.FATAL.Panicf("Failed to save new multi part "+ @@ -119,7 +122,7 @@ func (mpm *multiPartMessage) Add(partNumber uint8, part []byte) { } func (mpm *multiPartMessage) AddFirst(mt message.Type, partNumber uint8, - numParts uint8, timestamp time.Time, part []byte) { + numParts uint8, senderTimestamp, storageTimestamp time.Time, part []byte) { mpm.mux.Lock() defer mpm.mux.Unlock() @@ -129,10 +132,11 @@ func (mpm *multiPartMessage) AddFirst(mt message.Type, partNumber uint8, } mpm.NumParts = numParts - mpm.Timestamp = timestamp + mpm.SenderTimestamp = senderTimestamp mpm.MessageType = mt mpm.parts[partNumber] = part mpm.PresentParts++ + mpm.StorageTimestamp = storageTimestamp if err := savePart(mpm.kv, partNumber, part); err != nil { jww.FATAL.Panicf("Failed to save multi part "+ @@ -159,27 +163,8 @@ func (mpm *multiPartMessage) IsComplete(relationshipFingerprint []byte) (message mpm.parts = append(mpm.parts, make([][]byte, int(mpm.NumParts)-len(mpm.parts))...) } - var err error - lenMsg := 0 - // Load all parts from disk, deleting files from disk as we go along - for i := uint8(0); i < mpm.NumParts; i++ { - if mpm.parts[i] == nil { - if mpm.parts[i], err = loadPart(mpm.kv, i); err != nil { - jww.FATAL.Panicf("Failed to load multi part "+ - "message part %v from %s messageID %v: %s", i, mpm.Sender, - mpm.MessageID, err) - } - if err = deletePart(mpm.kv, i); err != nil { - jww.FATAL.Panicf("Failed to delete multi part "+ - "message part %v from %s messageID %v: %s", i, mpm.Sender, - mpm.MessageID, err) - } - } - lenMsg += len(mpm.parts[i]) - } - // delete the multipart message - mpm.delete() + lenMsg := mpm.delete() mpm.mux.Unlock() // Reconstruct the message @@ -200,7 +185,7 @@ func (mpm *multiPartMessage) IsComplete(relationshipFingerprint []byte) (message Payload: reconstructed, MessageType: mpm.MessageType, Sender: mpm.Sender, - Timestamp: mpm.Timestamp, + Timestamp: mpm.SenderTimestamp, // Encryption will be set externally Encryption: 0, ID: mid, @@ -209,7 +194,27 @@ func (mpm *multiPartMessage) IsComplete(relationshipFingerprint []byte) (message return m, true } -func (mpm *multiPartMessage) delete() { +// deletes all parts from disk and RAM. Returns the message length for reconstruction +func (mpm *multiPartMessage) delete() int { + // Load all parts from disk, deleting files from disk as we go along + var err error + lenMsg := 0 + for i := uint8(0); i < mpm.NumParts; i++ { + if mpm.parts[i] == nil { + if mpm.parts[i], err = loadPart(mpm.kv, i); err != nil { + jww.FATAL.Panicf("Failed to load multi part "+ + "message part %v from %s messageID %v: %s", i, mpm.Sender, + mpm.MessageID, err) + } + if err = deletePart(mpm.kv, i); err != nil { + jww.FATAL.Panicf("Failed to delete multi part "+ + "message part %v from %s messageID %v: %s", i, mpm.Sender, + mpm.MessageID, err) + } + } + lenMsg += len(mpm.parts[i]) + } + //key := makeMultiPartMessageKey(mpm.MessageID) if err := mpm.kv.Delete(messageKey, currentMultiPartMessageVersion); err != nil { @@ -217,4 +222,6 @@ func (mpm *multiPartMessage) delete() { "message from %s messageID %v: %s", mpm.Sender, mpm.MessageID, err) } + + return lenMsg } diff --git a/storage/partition/multiPartMessage_test.go b/storage/partition/multiPartMessage_test.go index dff3c0fabb41cffd20f714497b487ea8bcbd4644..752d53272bb121f566e2917d746087020f68d14b 100644 --- a/storage/partition/multiPartMessage_test.go +++ b/storage/partition/multiPartMessage_test.go @@ -27,13 +27,13 @@ func Test_loadOrCreateMultiPartMessage_Create(t *testing.T) { // Set up expected test value prng := rand.New(rand.NewSource(netTime.Now().UnixNano())) expectedMpm := &multiPartMessage{ - Sender: id.NewIdFromUInt(prng.Uint64(), id.User, t), - MessageID: prng.Uint64(), - NumParts: 0, - PresentParts: 0, - Timestamp: time.Time{}, - MessageType: 0, - kv: versioned.NewKV(make(ekv.Memstore)), + Sender: id.NewIdFromUInt(prng.Uint64(), id.User, t), + MessageID: prng.Uint64(), + NumParts: 0, + PresentParts: 0, + SenderTimestamp: time.Time{}, + MessageType: 0, + kv: versioned.NewKV(make(ekv.Memstore)), } expectedData, err := json.Marshal(expectedMpm) if err != nil { @@ -63,13 +63,13 @@ func Test_loadOrCreateMultiPartMessage_Load(t *testing.T) { // Set up expected test value prng := rand.New(rand.NewSource(netTime.Now().UnixNano())) expectedMpm := &multiPartMessage{ - Sender: id.NewIdFromUInt(prng.Uint64(), id.User, t), - MessageID: prng.Uint64(), - NumParts: 0, - PresentParts: 0, - Timestamp: time.Time{}, - MessageType: 0, - kv: versioned.NewKV(make(ekv.Memstore)), + Sender: id.NewIdFromUInt(prng.Uint64(), id.User, t), + MessageID: prng.Uint64(), + NumParts: 0, + PresentParts: 0, + SenderTimestamp: time.Time{}, + MessageType: 0, + kv: versioned.NewKV(make(ekv.Memstore)), } err := expectedMpm.save() if err != nil { @@ -85,8 +85,8 @@ func Test_loadOrCreateMultiPartMessage_Load(t *testing.T) { func CheckMultiPartMessages(expectedMpm *multiPartMessage, mpm *multiPartMessage, t *testing.T) { // The kv differs because it has prefix called, so we compare fields individually - if expectedMpm.Timestamp != mpm.Timestamp { - t.Errorf("timestamps mismatch: expected %v, got %v", expectedMpm.Timestamp, mpm.Timestamp) + if expectedMpm.SenderTimestamp != mpm.SenderTimestamp { + t.Errorf("timestamps mismatch: expected %v, got %v", expectedMpm.SenderTimestamp, mpm.SenderTimestamp) } if expectedMpm.MessageType != mpm.MessageType { t.Errorf("messagetype mismatch: expected %v, got %v", expectedMpm.MessageID, mpm.MessageID) @@ -159,21 +159,21 @@ func TestMultiPartMessage_AddFirst(t *testing.T) { // Generate test values prng := rand.New(rand.NewSource(netTime.Now().UnixNano())) expectedMpm := &multiPartMessage{ - Sender: id.NewIdFromUInt(prng.Uint64(), id.User, t), - MessageID: prng.Uint64(), - NumParts: uint8(prng.Uint32()), - PresentParts: 1, - Timestamp: netTime.Now(), - MessageType: message.NoType, - parts: make([][]byte, 3), - kv: versioned.NewKV(make(ekv.Memstore)), + Sender: id.NewIdFromUInt(prng.Uint64(), id.User, t), + MessageID: prng.Uint64(), + NumParts: uint8(prng.Uint32()), + PresentParts: 1, + SenderTimestamp: netTime.Now(), + MessageType: message.NoType, + parts: make([][]byte, 3), + kv: versioned.NewKV(make(ekv.Memstore)), } expectedMpm.parts[2] = []byte{5, 8, 78, 9} npm := loadOrCreateMultiPartMessage(expectedMpm.Sender, expectedMpm.MessageID, expectedMpm.kv) npm.AddFirst(expectedMpm.MessageType, 2, expectedMpm.NumParts, - expectedMpm.Timestamp, expectedMpm.parts[2]) + expectedMpm.SenderTimestamp, netTime.Now(), expectedMpm.parts[2]) CheckMultiPartMessages(expectedMpm, npm, t) @@ -203,7 +203,7 @@ func TestMultiPartMessage_IsComplete(t *testing.T) { t.Error("IsComplete() returned true when NumParts == 0.") } - mpm.AddFirst(message.Text, partNums[0], 75, netTime.Now(), parts[0]) + mpm.AddFirst(message.Text, partNums[0], 75, netTime.Now(), netTime.Now(), parts[0]) for i := range partNums { if i > 0 { mpm.Add(partNums[i], parts[i]) diff --git a/storage/partition/store.go b/storage/partition/store.go index de601693828ccd159dbdaefad11139942df2c15c..f8a0e8e2c8054e6bbad61ee764c18b7664cee57f 100644 --- a/storage/partition/store.go +++ b/storage/partition/store.go @@ -8,11 +8,14 @@ package partition import ( - "crypto/md5" "encoding/binary" + "encoding/json" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "golang.org/x/crypto/blake2b" "sync" "time" ) @@ -20,29 +23,58 @@ import ( type multiPartID [16]byte const packagePrefix = "Partition" +const clearPartitionThreshold = 24 * time.Hour +const activePartitions = "activePartitions" +const activePartitionVersion = 0 type Store struct { - multiParts map[multiPartID]*multiPartMessage - kv *versioned.KV - mux sync.Mutex + multiParts map[multiPartID]*multiPartMessage + activeParts map[*multiPartMessage]bool + kv *versioned.KV + mux sync.Mutex } func New(kv *versioned.KV) *Store { return &Store{ - multiParts: make(map[multiPartID]*multiPartMessage), - kv: kv.Prefix(packagePrefix), + multiParts: make(map[multiPartID]*multiPartMessage), + activeParts: make(map[*multiPartMessage]bool), + kv: kv.Prefix(packagePrefix), } } +func Load(kv *versioned.KV) *Store { + partitionStore := &Store{ + multiParts: make(map[multiPartID]*multiPartMessage), + activeParts: make(map[*multiPartMessage]bool), + kv: kv.Prefix(packagePrefix), + } + + partitionStore.loadActivePartitions() + + partitionStore.prune() + + return partitionStore +} + func (s *Store) AddFirst(partner *id.ID, mt message.Type, messageID uint64, - partNum, numParts uint8, timestamp time.Time, + partNum, numParts uint8, senderTimestamp, storageTimestamp time.Time, part []byte, relationshipFingerprint []byte) (message.Receive, bool) { mpm := s.load(partner, messageID) - mpm.AddFirst(mt, partNum, numParts, timestamp, part) + mpm.AddFirst(mt, partNum, numParts, senderTimestamp, storageTimestamp, part) + msg, ok := mpm.IsComplete(relationshipFingerprint) + s.mux.Lock() + defer s.mux.Unlock() + if !ok { + s.activeParts[mpm] = true + s.saveActiveParts() + } else { + mpID := getMultiPartID(mpm.Sender, mpm.MessageID) + delete(s.multiParts, mpID) + } - return mpm.IsComplete(relationshipFingerprint) + return msg, ok } func (s *Store) Add(partner *id.ID, messageID uint64, partNum uint8, @@ -52,7 +84,33 @@ func (s *Store) Add(partner *id.ID, messageID uint64, partNum uint8, mpm.Add(partNum, part) - return mpm.IsComplete(relationshipFingerprint) + msg, ok := mpm.IsComplete(relationshipFingerprint) + if !ok { + s.activeParts[mpm] = true + s.saveActiveParts() + } else { + mpID := getMultiPartID(mpm.Sender, mpm.MessageID) + delete(s.multiParts, mpID) + } + + return msg, ok +} + +// Prune clear old messages on it's stored timestamp +func (s *Store) prune() { + s.mux.Lock() + defer s.mux.Unlock() + now := netTime.Now() + for mpm, _ := range s.activeParts { + if now.Sub(mpm.StorageTimestamp) >= clearPartitionThreshold { + jww.INFO.Printf("prune partition: %v", mpm) + mpm.mux.Lock() + mpm.delete() + mpID := getMultiPartID(mpm.Sender, mpm.MessageID) + mpm.mux.Unlock() + delete(s.multiParts, mpID) + } + } } func (s *Store) load(partner *id.ID, messageID uint64) *multiPartMessage { @@ -68,8 +126,66 @@ func (s *Store) load(partner *id.ID, messageID uint64) *multiPartMessage { return mpm } +func (s *Store) saveActiveParts() { + jww.INFO.Printf("Saving %d active partitions", len(s.activeParts)) + activeList := make([]*multiPartMessage, 0, len(s.activeParts)) + for mpm := range s.activeParts { + mpm.mux.Lock() + jww.INFO.Printf("saveActiveParts saving %v", mpm) + activeList = append(activeList, mpm) + mpm.mux.Unlock() + } + + data, err := json.Marshal(&activeList) + if err != nil { + jww.FATAL.Panicf("Could not save active partitions: %v", err) + } + + obj := versioned.Object{ + Version: activePartitionVersion, + Timestamp: netTime.Now(), + Data: data, + } + + err = s.kv.Set(activePartitions, activePartitionVersion, &obj) + if err != nil { + jww.FATAL.Panicf("Could not save active partitions: %v", err) + } +} + +func (s *Store) loadActivePartitions() { + s.mux.Lock() + defer s.mux.Unlock() + obj, err := s.kv.Get(activePartitions, activePartitionVersion) + if err != nil { + jww.DEBUG.Printf("Could not load active partitions: %v", err) + return + } + + activeList := make([]*multiPartMessage, 0) + if err := json.Unmarshal(obj.Data, &activeList); err != nil { + jww.FATAL.Panicf("Failed to "+ + "unmarshal active partitions: %v", err) + } + jww.INFO.Printf("loadActivePartitions found %d active", len(activeList)) + + for _, activeMpm := range activeList { + mpm := loadOrCreateMultiPartMessage(activeMpm.Sender, activeMpm.MessageID, s.kv) + s.activeParts[mpm] = true + } + +} + func getMultiPartID(partner *id.ID, messageID uint64) multiPartID { + h, _ := blake2b.New256(nil) + + h.Write(partner.Bytes()) b := make([]byte, 8) binary.BigEndian.PutUint64(b, messageID) - return md5.Sum(append(partner[:], b...)) + h.Write(b) + + var mpID multiPartID + copy(mpID[:], h.Sum(nil)) + + return mpID } diff --git a/storage/partition/store_test.go b/storage/partition/store_test.go index 9de6327d989d032e6ed987db0aff891d6be0fa3b..4e3d221a159be0f1fd73664c1d6d6a9e7b5097aa 100644 --- a/storage/partition/store_test.go +++ b/storage/partition/store_test.go @@ -22,8 +22,9 @@ import ( func TestNew(t *testing.T) { rootKv := versioned.NewKV(make(ekv.Memstore)) expectedStore := &Store{ - multiParts: make(map[multiPartID]*multiPartMessage), - kv: rootKv.Prefix(packagePrefix), + multiParts: make(map[multiPartID]*multiPartMessage), + activeParts: make(map[*multiPartMessage]bool), + kv: rootKv.Prefix(packagePrefix), } store := New(rootKv) @@ -40,7 +41,7 @@ func TestStore_AddFirst(t *testing.T) { s := New(versioned.NewKV(ekv.Memstore{})) msg, complete := s.AddFirst(id.NewIdFromString("User", id.User, t), - message.Text, 5, 0, 1, netTime.Now(), part, + message.Text, 5, 0, 1, netTime.Now(), netTime.Now(), part, []byte{0}) if !complete { @@ -60,7 +61,7 @@ func TestStore_Add(t *testing.T) { s := New(versioned.NewKV(ekv.Memstore{})) msg, complete := s.AddFirst(id.NewIdFromString("User", id.User, t), - message.Text, 5, 0, 2, netTime.Now(), part1, + message.Text, 5, 0, 2, netTime.Now(), netTime.Now(), part1, []byte{0}) if complete { @@ -79,3 +80,44 @@ func TestStore_Add(t *testing.T) { "\n\texpected: %v\n\treceived: %v", part, msg.Payload) } } + +// Unit test of prune +func TestStore_ClearMessages(t *testing.T) { + // Setup: Add 2 message to store: an old message past the threshold and a new message + part1 := []byte("Test message.") + part2 := []byte("Second Sentence.") + s := New(versioned.NewKV(ekv.Memstore{})) + + partner1 := id.NewIdFromString("User", id.User, t) + messageId1 := uint64(5) + oldTimestamp := netTime.Now().Add(-2 * clearPartitionThreshold) + s.AddFirst(partner1, + message.Text, messageId1, 0, 2, netTime.Now(), + oldTimestamp, part1, + []byte{0}) + s.Add(partner1, messageId1, 1, part2, []byte{0}) + + partner2 := id.NewIdFromString("User1", id.User, t) + messageId2 := uint64(6) + newTimestamp := netTime.Now() + s.AddFirst(partner2, message.Text, messageId2, 0, 2, netTime.Now(), + newTimestamp, part1, + []byte{0}) + + // Call clear messages + s.prune() + + // Check if old message cleared + mpmId := getMultiPartID(partner1, messageId1) + if _, ok := s.multiParts[mpmId]; ok { + t.Errorf("Prune error: " + + "Expected old message to be cleared out of store") + } + + // Check if new message remains + mpmId2 := getMultiPartID(partner2, messageId2) + if _, ok := s.multiParts[mpmId2]; !ok { + t.Errorf("Prune error: " + + "Expected new message to be remain in store") + } +} diff --git a/storage/reception/IdentityUse.go b/storage/reception/IdentityUse.go index 2c8b9df3b64601651d7489e96935ff1a3d360db9..92d1ed8c95ab33dfad5c5ff2fa9ec9be2d7e3a63 100644 --- a/storage/reception/IdentityUse.go +++ b/storage/reception/IdentityUse.go @@ -1,22 +1,15 @@ package reception import ( - "github.com/pkg/errors" + "fmt" "gitlab.com/elixxir/client/storage/rounds" - "gitlab.com/elixxir/crypto/hash" - "gitlab.com/xx_network/crypto/randomness" - "io" - "math/big" - "time" + "strconv" + "strings" ) type IdentityUse struct { Identity - // Randomly generated time to poll between - StartRequest time.Time // Timestamp to request the start of bloom filters - EndRequest time.Time // Timestamp to request the End of bloom filters - // Denotes if the identity is fake, in which case we do not process messages Fake bool @@ -25,26 +18,16 @@ type IdentityUse struct { CR *rounds.CheckedRounds } -// setSamplingPeriod add the Request mask as a random buffer around the sampling -// time to obfuscate it. -func (iu IdentityUse) setSamplingPeriod(rng io.Reader) (IdentityUse, error) { - - // Generate the seed - seed := make([]byte, 32) - if _, err := rng.Read(seed); err != nil { - return IdentityUse{}, errors.WithMessage(err, "Failed to choose ID "+ - "due to rng failure") - } +func (iu IdentityUse) GoString() string { + str := make([]string, 0, 7) - h, err := hash.NewCMixHash() - if err != nil { - return IdentityUse{}, err - } + str = append(str, "Identity:"+iu.Identity.GoString()) + str = append(str, "StartValid:"+iu.StartValid.String()) + str = append(str, "EndValid:"+iu.EndValid.String()) + str = append(str, "Fake:"+strconv.FormatBool(iu.Fake)) + str = append(str, "UR:"+fmt.Sprintf("%+v", iu.UR)) + str = append(str, "ER:"+fmt.Sprintf("%+v", iu.ER)) + str = append(str, "CR:"+fmt.Sprintf("%+v", iu.CR)) - // Calculate the period offset - periodOffset := randomness.RandInInterval( - big.NewInt(iu.RequestMask.Nanoseconds()), seed, h).Int64() - iu.StartRequest = iu.StartValid.Add(-time.Duration(periodOffset)) - iu.EndRequest = iu.EndValid.Add(iu.RequestMask - time.Duration(periodOffset)) - return iu, nil + return "{" + strings.Join(str, ", ") + "}" } diff --git a/storage/reception/fake.go b/storage/reception/fake.go index 38cb63a241cc25122b98ff8b5a1762da0f3994a5..1e4e0a31c01b8d5b4df90cc84864937824b9a5bf 100644 --- a/storage/reception/fake.go +++ b/storage/reception/fake.go @@ -10,7 +10,8 @@ import ( // generateFakeIdentity generates a fake identity of the given size with the // given random number generator -func generateFakeIdentity(rng io.Reader, idSize uint, now time.Time) (IdentityUse, error) { +func generateFakeIdentity(rng io.Reader, addressSize uint8, + now time.Time) (IdentityUse, error) { // Randomly generate an identity randIdBytes := make([]byte, id.ArrIDLen-1) if _, err := rng.Read(randIdBytes); err != nil { @@ -23,7 +24,8 @@ func generateFakeIdentity(rng io.Reader, idSize uint, now time.Time) (IdentityUs randID.SetType(id.User) // Generate the current ephemeral ID from the random identity - ephID, start, end, err := ephemeral.GetId(randID, idSize, now.UnixNano()) + ephID, start, end, err := ephemeral.GetId( + randID, uint(addressSize), now.UnixNano()) if err != nil { return IdentityUse{}, errors.WithMessage(err, "failed to generate an "+ "ephemeral ID for random identity when none is available") @@ -33,11 +35,11 @@ func generateFakeIdentity(rng io.Reader, idSize uint, now time.Time) (IdentityUs Identity: Identity{ EphId: ephID, Source: randID, + AddressSize: addressSize, End: end, ExtraChecks: 0, StartValid: start, EndValid: end, - RequestMask: 24 * time.Hour, Ephemeral: true, }, Fake: true, diff --git a/storage/reception/fake_test.go b/storage/reception/fake_test.go index 2c748ac2258e0950e8901a2748c0057c498770ef..3e9ab209cd7279aeb97ade25a2227a82b1c42940 100644 --- a/storage/reception/fake_test.go +++ b/storage/reception/fake_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "math" "math/rand" + "strconv" "strings" "testing" "time" @@ -13,22 +14,23 @@ import ( func Test_generateFakeIdentity(t *testing.T) { rng := rand.New(rand.NewSource(42)) + addressSize := uint8(15) end, _ := json.Marshal(time.Unix(0, 1258494203759765625)) startValid, _ := json.Marshal(time.Unix(0, 1258407803759765625)) endValid, _ := json.Marshal(time.Unix(0, 1258494203759765625)) expected := "{\"EphId\":[0,0,0,0,0,0,46,197]," + "\"Source\":[83,140,127,150,177,100,191,27,151,187,159,75,180,114," + "232,159,91,20,132,242,82,9,201,217,52,62,146,186,9,221,157,82,3]," + + "\"AddressSize\":" + strconv.Itoa(int(addressSize)) + "," + "\"End\":" + string(end) + ",\"ExtraChecks\":0," + "\"StartValid\":" + string(startValid) + "," + "\"EndValid\":" + string(endValid) + "," + - "\"RequestMask\":86400000000000,\"Ephemeral\":true," + - "\"StartRequest\":\"0001-01-01T00:00:00Z\"," + - "\"EndRequest\":\"0001-01-01T00:00:00Z\",\"Fake\":true,\"UR\":null,\"ER\":null,\"CR\":null}" + "\"Ephemeral\":true," + + "\"Fake\":true,\"UR\":null,\"ER\":null,\"CR\":null}" timestamp := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) - received, err := generateFakeIdentity(rng, 15, timestamp) + received, err := generateFakeIdentity(rng, addressSize, timestamp) if err != nil { t.Errorf("generateFakeIdentity() returned an error: %+v", err) } @@ -58,7 +60,7 @@ func Test_generateFakeIdentity_GetEphemeralIdError(t *testing.T) { rng := rand.New(rand.NewSource(42)) timestamp := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) - _, err := generateFakeIdentity(rng, math.MaxUint64, timestamp) + _, err := generateFakeIdentity(rng, math.MaxInt8, timestamp) if err == nil || !strings.Contains(err.Error(), "ephemeral ID") { t.Errorf("generateFakeIdentity() did not return the correct error on "+ "failure to generate ephemeral ID: %+v", err) diff --git a/storage/reception/identity.go b/storage/reception/identity.go index 4c65d9696ba4a6d0fd567395a4d86e8bd9145b45..4ae3f9564e3bec5d2ad6441cdc76fa9fbe2e6433 100644 --- a/storage/reception/identity.go +++ b/storage/reception/identity.go @@ -8,6 +8,7 @@ import ( "gitlab.com/xx_network/primitives/id/ephemeral" "gitlab.com/xx_network/primitives/netTime" "strconv" + "strings" "time" ) @@ -16,8 +17,9 @@ const identityStorageVersion = 0 type Identity struct { // Identity - EphId ephemeral.Id - Source *id.ID + EphId ephemeral.Id + Source *id.ID + AddressSize uint8 // Usage variables End time.Time // Timestamp when active polling will stop @@ -25,10 +27,8 @@ type Identity struct { // ID exits active // Polling parameters - StartValid time.Time // Timestamp when the ephID begins being valid - EndValid time.Time // Timestamp when the ephID stops being valid - RequestMask time.Duration // Amount of extra time requested for the poll in - // order to mask the exact valid time for the ID + StartValid time.Time // Timestamp when the ephID begins being valid + EndValid time.Time // Timestamp when the ephID stops being valid // Makes the identity not store on disk Ephemeral bool @@ -76,17 +76,32 @@ func (i Identity) delete(kv *versioned.KV) error { return kv.Delete(identityStorageKey, identityStorageVersion) } -func (i *Identity) String() string { +func (i Identity) String() string { return strconv.FormatInt(i.EphId.Int64(), 16) + " " + i.Source.String() } +func (i Identity) GoString() string { + str := make([]string, 0, 9) + + str = append(str, "EphId:"+strconv.FormatInt(i.EphId.Int64(), 16)) + str = append(str, "Source:"+i.Source.String()) + str = append(str, "AddressSize:"+strconv.FormatUint(uint64(i.AddressSize), 10)) + str = append(str, "End:"+i.End.String()) + str = append(str, "ExtraChecks:"+strconv.FormatUint(uint64(i.ExtraChecks), 10)) + str = append(str, "StartValid:"+i.StartValid.String()) + str = append(str, "EndValid:"+i.EndValid.String()) + str = append(str, "Ephemeral:"+strconv.FormatBool(i.Ephemeral)) + + return "{" + strings.Join(str, ", ") + "}" +} + func (i Identity) Equal(b Identity) bool { return i.EphId == b.EphId && i.Source.Cmp(b.Source) && + i.AddressSize == b.AddressSize && i.End.Equal(b.End) && i.ExtraChecks == b.ExtraChecks && i.StartValid.Equal(b.StartValid) && i.EndValid.Equal(b.EndValid) && - i.RequestMask == b.RequestMask && i.Ephemeral == b.Ephemeral } diff --git a/storage/reception/identityUse_test.go b/storage/reception/identityUse_test.go deleted file mode 100644 index e54d73e723a4daa8365f34e446c60916a43ca3dd..0000000000000000000000000000000000000000 --- a/storage/reception/identityUse_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package reception - -import ( - "math/rand" - "testing" - "time" -) - -func TestIdentityUse_SetSamplingPeriod(t *testing.T) { - rng := rand.New(rand.NewSource(42)) - - const numTests = 1000 - - for i := 0; i < numTests; i++ { - // Generate an identity use - start := randate() - end := start.Add(time.Duration(rand.Uint64() % uint64(92*time.Hour))) - mask := time.Duration(rand.Uint64() % uint64(92*time.Hour)) - iu := IdentityUse{ - Identity: Identity{ - StartValid: start, - EndValid: end, - RequestMask: mask, - }, - } - - // Generate the sampling period - var err error - iu, err = iu.setSamplingPeriod(rng) - if err != nil { - t.Errorf("Errored in generatign sampling "+ - "period on interation %v: %+v", i, err) - } - - // Test that the range between the periods is correct - resultRange := iu.EndRequest.Sub(iu.StartRequest) - expectedRange := iu.EndValid.Sub(iu.StartValid) + iu.RequestMask - - if resultRange != expectedRange { - t.Errorf("The generated sampling period is of the wrong "+ - "size: Expecterd: %s, Received: %s", expectedRange, resultRange) - } - - // Test the sampling range does not exceed a reasonable lower bound - lowerBound := iu.StartValid.Add(-iu.RequestMask) - if !iu.StartRequest.After(lowerBound) { - t.Errorf("Start request exceeds the reasonable lower "+ - "bound: \n\t Bound: %s\n\t Start: %s", lowerBound, iu.StartValid) - } - - // Test the sampling range does not exceed a reasonable upper bound - upperBound := iu.EndValid.Add(iu.RequestMask - time.Millisecond) - if iu.EndRequest.After(upperBound) { - t.Errorf("End request exceeds the reasonable upper bound") - } - } - -} - -func randate() time.Time { - min := time.Date(1970, 1, 0, 0, 0, 0, 0, time.UTC).Unix() - max := time.Date(2070, 1, 0, 0, 0, 0, 0, time.UTC).Unix() - delta := max - min - - sec := rand.Int63n(delta) + min - return time.Unix(sec, 0) -} diff --git a/storage/reception/identity_test.go b/storage/reception/identity_test.go index d80b9501486c94045209da5c99dc0b77a07306f8..0993a0e2af2d37caef6841d8d8b2574faef418b2 100644 --- a/storage/reception/identity_test.go +++ b/storage/reception/identity_test.go @@ -16,11 +16,11 @@ func TestIdentity_EncodeDecode(t *testing.T) { r := Identity{ EphId: ephemeral.Id{}, Source: &id.Permissioning, + AddressSize: 15, End: netTime.Now().Round(0), ExtraChecks: 12, StartValid: netTime.Now().Round(0), EndValid: netTime.Now().Round(0), - RequestMask: 2 * time.Hour, Ephemeral: false, } @@ -45,11 +45,11 @@ func TestIdentity_Delete(t *testing.T) { r := Identity{ EphId: ephemeral.Id{}, Source: &id.Permissioning, + AddressSize: 15, End: netTime.Now().Round(0), ExtraChecks: 12, StartValid: netTime.Now().Round(0), EndValid: netTime.Now().Round(0), - RequestMask: 2 * time.Hour, Ephemeral: false, } @@ -90,11 +90,11 @@ func TestIdentity_Equal(t *testing.T) { if !a.Identity.Equal(b.Identity) { t.Errorf("Equal() found two equal identities as unequal."+ - "\na: %s\nb: %s", a.String(), b.String()) + "\na: %s\nb: %s", a, b) } if a.Identity.Equal(c.Identity) { t.Errorf("Equal() found two unequal identities as equal."+ - "\na: %s\nc: %s", a.String(), c.String()) + "\na: %s\nc: %s", a, c) } } diff --git a/storage/reception/registration.go b/storage/reception/registration.go index 0535f092366ce03f556f21ad61b39b8b65a97b63..611a6c0ba2edce09a00b498e434e9bc7b8b04f7c 100644 --- a/storage/reception/registration.go +++ b/storage/reception/registration.go @@ -8,7 +8,7 @@ import ( "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" - "gitlab.com/xx_network/primitives/netTime" + // "gitlab.com/xx_network/primitives/netTime" "strconv" "time" ) @@ -29,13 +29,13 @@ func newRegistration(reg Identity, kv *versioned.KV) (*registration, error) { reg.EndValid = reg.EndValid.Round(0) reg.End = reg.End.Round(0) - now := netTime.Now() + // now := netTime.Now() // Do edge checks to determine if the identity is valid - if now.After(reg.End) && reg.ExtraChecks < 1 { - return nil, errors.New("Cannot create a registration for an " + - "identity which has expired") - } + // if now.After(reg.End) && reg.ExtraChecks < 1 { + // return nil, errors.New("Cannot create a registration for an " + + // "identity which has expired") + // } // Set the prefix kv = kv.Prefix(regPrefix(reg.EphId, reg.Source, reg.StartValid)) diff --git a/storage/reception/registration_test.go b/storage/reception/registration_test.go index 8e9024c46d9fdb60726498b9fb103cb2b1beddbb..ccf210f5e8aa68ce6835d98ec6a956975f466aae 100644 --- a/storage/reception/registration_test.go +++ b/storage/reception/registration_test.go @@ -5,27 +5,27 @@ import ( "gitlab.com/elixxir/ekv" "gitlab.com/xx_network/primitives/netTime" "math/rand" - "strings" + // "strings" "testing" "time" ) -func TestNewRegistration_Failed(t *testing.T) { - // Generate an identity for use - rng := rand.New(rand.NewSource(42)) - timestamp := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) - idu, _ := generateFakeIdentity(rng, 15, timestamp) - id := idu.Identity - kv := versioned.NewKV(make(ekv.Memstore)) - - id.End = time.Time{} - id.ExtraChecks = 0 - - _, err := newRegistration(id, kv) - if err == nil || !strings.Contains(err.Error(), "Cannot create a registration for an identity which has expired") { - t.Error("Registration creation succeeded with expired identity.") - } -} +// func TestNewRegistration_Failed(t *testing.T) { +// // Generate an identity for use +// rng := rand.New(rand.NewSource(42)) +// timestamp := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) +// idu, _ := generateFakeIdentity(rng, 15, timestamp) +// id := idu.Identity +// kv := versioned.NewKV(make(ekv.Memstore)) + +// id.End = time.Time{} +// id.ExtraChecks = 0 + +// _, err := newRegistration(id, kv) +// if err == nil || !strings.Contains(err.Error(), "Cannot create a registration for an identity which has expired") { +// t.Error("Registration creation succeeded with expired identity.") +// } +// } func TestNewRegistration_Ephemeral(t *testing.T) { // Generate an identity for use diff --git a/storage/reception/store.go b/storage/reception/store.go index bd78f0b7682d3c06c2b47211f518729cb63cbfc1..7a22c05f4ea7c2609ad0c835a69c31983346eefe 100644 --- a/storage/reception/store.go +++ b/storage/reception/store.go @@ -1,8 +1,6 @@ package reception import ( - "bytes" - "crypto/md5" "encoding/json" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -11,8 +9,8 @@ import ( "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" "gitlab.com/xx_network/primitives/netTime" + "golang.org/x/crypto/blake2b" "io" - "strconv" "sync" "time" ) @@ -20,17 +18,11 @@ import ( const receptionPrefix = "reception" const receptionStoreStorageKey = "receptionStoreKey" const receptionStoreStorageVersion = 0 -const receptionIDSizeStorageKey = "receptionIDSizeKey" -const receptionIDSizeStorageVersion = 0 -const defaultIDSize = 12 type Store struct { // Identities which are being actively checked - active []*registration - present map[idHash]interface{} - idSize int - idSizeCond *sync.Cond - isIdSizeSet bool + active []*registration + present map[idHash]struct{} kv *versioned.KV @@ -46,24 +38,20 @@ type storedReference struct { type idHash [16]byte func makeIdHash(ephID ephemeral.Id, source *id.ID) idHash { - h := md5.New() + h, _ := blake2b.New256(nil) h.Write(ephID[:]) h.Write(source.Bytes()) - idHashBytes := h.Sum(nil) idH := idHash{} - copy(idH[:], idHashBytes) + copy(idH[:], h.Sum(nil)) return idH } // NewStore creates a new reception store that starts empty. func NewStore(kv *versioned.KV) *Store { - kv = kv.Prefix(receptionPrefix) s := &Store{ - active: make([]*registration, 0), - present: make(map[idHash]interface{}), - idSize: defaultIDSize * 2, - kv: kv, - idSizeCond: sync.NewCond(&sync.Mutex{}), + active: []*registration{}, + present: make(map[idHash]struct{}), + kv: kv.Prefix(receptionPrefix), } // Store the empty list @@ -71,53 +59,37 @@ func NewStore(kv *versioned.KV) *Store { jww.FATAL.Panicf("Failed to save new reception store: %+v", err) } - // Update the size so queries can be made - s.UpdateIdSize(defaultIDSize) - return s } func LoadStore(kv *versioned.KV) *Store { kv = kv.Prefix(receptionPrefix) - s := &Store{ - kv: kv, - present: make(map[idHash]interface{}), - idSizeCond: sync.NewCond(&sync.Mutex{}), - } // Load the versioned object for the reception list - vo, err := kv.Get(receptionStoreStorageKey, - receptionStoreStorageVersion) + vo, err := kv.Get(receptionStoreStorageKey, receptionStoreStorageVersion) if err != nil { jww.FATAL.Panicf("Failed to get the reception storage list: %+v", err) } - identities := make([]storedReference, len(s.active)) - err = json.Unmarshal(vo.Data, &identities) - if err != nil { - jww.FATAL.Panicf("Failed to unmarshal the reception storage list: %+v", err) + // JSON unmarshal identities list + var identities []storedReference + if err = json.Unmarshal(vo.Data, &identities); err != nil { + jww.FATAL.Panicf("Failed to unmarshal the stored identity list: %+v", err) + } + + s := &Store{ + active: make([]*registration, len(identities)), + present: make(map[idHash]struct{}, len(identities)), + kv: kv, } - s.active = make([]*registration, len(identities)) for i, sr := range identities { s.active[i], err = loadRegistration(sr.Eph, sr.Source, sr.StartValid, s.kv) if err != nil { jww.FATAL.Panicf("Failed to load registration for %s: %+v", regPrefix(sr.Eph, sr.Source, sr.StartValid), err) } - s.present[makeIdHash(sr.Eph, sr.Source)] = nil - } - - // Load the ephemeral ID length - vo, err = kv.Get(receptionIDSizeStorageKey, - receptionIDSizeStorageVersion) - if err != nil { - jww.FATAL.Panicf("Failed to get the reception ID size: %+v", err) - } - - if s.idSize, err = strconv.Atoi(string(vo.Data)); err != nil { - jww.FATAL.Panicf("Failed to unmarshal the reception ID size: %+v", - err) + s.present[makeIdHash(sr.Eph, sr.Source)] = struct{}{} } return s @@ -125,7 +97,6 @@ func LoadStore(kv *versioned.KV) *Store { func (s *Store) save() error { identities := s.makeStoredReferences() - data, err := json.Marshal(&identities) if err != nil { return errors.WithMessage(err, "failed to store reception store") @@ -166,7 +137,7 @@ func (s *Store) makeStoredReferences() []storedReference { return identities[:i] } -func (s *Store) GetIdentity(rng io.Reader) (IdentityUse, error) { +func (s *Store) GetIdentity(rng io.Reader, addressSize uint8) (IdentityUse, error) { s.mux.Lock() defer s.mux.Unlock() @@ -182,7 +153,7 @@ func (s *Store) GetIdentity(rng io.Reader) (IdentityUse, error) { // poll with so we can continue tracking the network and to further // obfuscate network identities. if len(s.active) == 0 { - identity, err = generateFakeIdentity(rng, uint(s.idSize), now) + identity, err = generateFakeIdentity(rng, addressSize, now) if err != nil { jww.FATAL.Panicf("Failed to generate a new ID when none "+ "available: %+v", err) @@ -194,23 +165,15 @@ func (s *Store) GetIdentity(rng io.Reader) (IdentityUse, error) { } } - // Calculate the sampling period - identity, err = identity.setSamplingPeriod(rng) - if err != nil { - jww.FATAL.Panicf("Failed to calculate the sampling period: "+ - "%+v", err) - } - return identity, nil } func (s *Store) AddIdentity(identity Identity) error { - idH := makeIdHash(identity.EphId, identity.Source) s.mux.Lock() defer s.mux.Unlock() - //do not make duplicates of IDs + // Do not make duplicates of IDs if _, ok := s.present[idH]; ok { jww.DEBUG.Printf("Ignoring duplicate identity for %d (%s)", identity.EphId, identity.Source) @@ -219,7 +182,7 @@ func (s *Store) AddIdentity(identity Identity) error { if identity.StartValid.After(identity.EndValid) { return errors.Errorf("Cannot add an identity which start valid "+ - "time (%s) is after its end valid time(%s)", identity.StartValid, + "time (%s) is after its end valid time (%s)", identity.StartValid, identity.EndValid) } @@ -230,11 +193,11 @@ func (s *Store) AddIdentity(identity Identity) error { } s.active = append(s.active, reg) - s.present[idH] = nil + s.present[idH] = struct{}{} if !identity.Ephemeral { if err := s.save(); err != nil { - jww.FATAL.Panicf("Failed to save reception store after identity " + - "addition") + jww.FATAL.Panicf("Failed to save reception store after identity "+ + "addition: %+v", err) } } @@ -245,86 +208,44 @@ func (s *Store) RemoveIdentity(ephID ephemeral.Id) { s.mux.Lock() defer s.mux.Unlock() - for i := 0; i < len(s.active); i++ { - inQuestion := s.active[i] - if bytes.Equal(inQuestion.EphId[:], ephID[:]) { + for i, inQuestion := range s.active { + if inQuestion.EphId == ephID { s.active = append(s.active[:i], s.active[i+1:]...) + err := inQuestion.Delete() if err != nil { jww.FATAL.Panicf("Failed to delete identity: %+v", err) } + if !inQuestion.Ephemeral { if err := s.save(); err != nil { - jww.FATAL.Panicf("Failed to save reception store after " + - "identity removal") + jww.FATAL.Panicf("Failed to save reception store after "+ + "identity removal: %+v", err) } } + return } } } -// Returns whether idSize is set to default -func (s *Store) IsIdSizeDefault() bool { +func (s *Store) SetToExpire(addressSize uint8) { s.mux.Lock() defer s.mux.Unlock() - return s.isIdSizeSet -} -// Updates idSize boolean and broadcasts to any waiting -// idSize readers that id size is now updated with the network -func (s *Store) MarkIdSizeAsSet() { - s.mux.Lock() - s.idSizeCond.L.Lock() - defer s.mux.Unlock() - defer s.idSizeCond.L.Unlock() - s.isIdSizeSet = true - s.idSizeCond.Broadcast() -} + expire := netTime.Now().Add(5 * time.Minute) -// Wrapper function which calls a -// sync.Cond wait. Used on any reader of idSize -// who cannot use the default id size -func (s *Store) WaitForIdSizeUpdate() { - s.idSizeCond.L.Lock() - defer s.idSizeCond.L.Unlock() - for !s.IsIdSizeDefault() { - - s.idSizeCond.Wait() - } -} - -func (s *Store) UpdateIdSize(idSize uint) { - s.mux.Lock() - defer s.mux.Unlock() - - if s.idSize == int(idSize) { - return - } - jww.INFO.Printf("Updating address space size to %v", idSize) - - s.idSize = int(idSize) - - // Store the ID size - obj := &versioned.Object{ - Version: receptionIDSizeStorageVersion, - Timestamp: netTime.Now(), - Data: []byte(strconv.Itoa(s.idSize)), - } - - err := s.kv.Set(receptionIDSizeStorageKey, - receptionIDSizeStorageVersion, obj) - if err != nil { - jww.FATAL.Panicf("Failed to store reception ID size: %+v", err) + for i, active := range s.active { + if active.AddressSize < addressSize && active.EndValid.After(expire) { + s.active[i].EndValid = expire + err := s.active[i].store(s.kv) + if err != nil { + jww.ERROR.Printf("Failed to store identity %d: %+v", i, err) + } + } } } -func (s *Store) GetIDSize() uint { - s.mux.Lock() - defer s.mux.Unlock() - return uint(s.idSize) -} - func (s *Store) prune(now time.Time) { lengthBefore := len(s.active) @@ -333,8 +254,8 @@ func (s *Store) prune(now time.Time) { inQuestion := s.active[i] if now.After(inQuestion.End) && inQuestion.ExtraChecks == 0 { if err := inQuestion.Delete(); err != nil { - jww.ERROR.Printf("Failed to delete Identity for %s: "+ - "%+v", inQuestion, err) + jww.ERROR.Printf("Failed to delete Identity for %s: %+v", + inQuestion, err) } s.active = append(s.active[:i], s.active[i+1:]...) @@ -347,7 +268,7 @@ func (s *Store) prune(now time.Time) { if lengthBefore != len(s.active) { jww.INFO.Printf("Pruned %d identities", lengthBefore-len(s.active)) if err := s.save(); err != nil { - jww.FATAL.Panicf("Failed to store reception storage") + jww.FATAL.Panicf("Failed to store reception storage: %+v", err) } } } @@ -361,11 +282,15 @@ func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error } else { seed := make([]byte, 32) if _, err := rng.Read(seed); err != nil { - return IdentityUse{}, errors.WithMessage(err, "Failed to "+ - "choose ID due to rng failure") + return IdentityUse{}, errors.WithMessage(err, "Failed to choose "+ + "ID due to RNG failure") } - selectedNum := large.NewInt(1).Mod(large.NewIntFromBytes(seed), large.NewInt(int64(len(s.active)))) + selectedNum := large.NewInt(1).Mod( + large.NewIntFromBytes(seed), + large.NewInt(int64(len(s.active))), + ) + selected = s.active[selectedNum.Uint64()] } @@ -373,9 +298,12 @@ func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error selected.ExtraChecks-- } - jww.TRACE.Printf("Selected identity: EphId: %d ID: %s End: %s StartValid: %s EndValid: %s", - selected.EphId.Int64(), selected.Source, selected.End.Format("01/02/06 03:04:05 pm"), - selected.StartValid.Format("01/02/06 03:04:05 pm"), selected.EndValid.Format("01/02/06 03:04:05 pm")) + jww.TRACE.Printf("Selected identity: EphId: %d ID: %s End: %s "+ + "StartValid: %s EndValid: %s", + selected.EphId.Int64(), selected.Source, + selected.End.Format("01/02/06 03:04:05 pm"), + selected.StartValid.Format("01/02/06 03:04:05 pm"), + selected.EndValid.Format("01/02/06 03:04:05 pm")) return IdentityUse{ Identity: selected.Identity, diff --git a/storage/reception/store_test.go b/storage/reception/store_test.go index 8779e8f54cedacd24ae9e4c892fb3b19c279b814..b969c9680a0f4e0885808df8b9144db6ca82d819 100644 --- a/storage/reception/store_test.go +++ b/storage/reception/store_test.go @@ -16,13 +16,12 @@ func TestNewStore(t *testing.T) { kv := versioned.NewKV(make(ekv.Memstore)) expected := &Store{ active: make([]*registration, 0), - idSize: defaultIDSize, kv: kv, } s := NewStore(kv) - if !reflect.DeepEqual([]*registration{}, s.active) || s.idSize != defaultIDSize { + if !reflect.DeepEqual([]*registration{}, s.active) { t.Errorf("NewStore() failed to return the expected Store."+ "\nexpected: %+v\nreceived: %+v", expected, s) } @@ -154,14 +153,14 @@ func TestStore_GetIdentity(t *testing.T) { t.Errorf("AddIdentity() produced an error: %+v", err) } - idu, err := s.GetIdentity(prng) + idu, err := s.GetIdentity(prng, 15) if err != nil { t.Errorf("GetIdentity() produced an error: %+v", err) } if !testID.Equal(idu.Identity) { t.Errorf("GetIdentity() did not return the expected Identity."+ - "\nexpected: %s\nreceived: %s", testID.String(), idu.String()) + "\nexpected: %s\nreceived: %s", testID, idu) } } @@ -181,7 +180,7 @@ func TestStore_AddIdentity(t *testing.T) { if !s.active[0].Identity.Equal(testID.Identity) { t.Errorf("Failed to get expected Identity.\nexpected: %s\nreceived: %s", - testID.Identity.String(), s.active[0]) + testID.Identity, s.active[0]) } } @@ -204,19 +203,6 @@ func TestStore_RemoveIdentity(t *testing.T) { } } -func TestStore_UpdateIdSize(t *testing.T) { - kv := versioned.NewKV(make(ekv.Memstore)) - s := NewStore(kv) - newSize := s.idSize * 2 - - s.UpdateIdSize(uint(newSize)) - - if s.idSize != newSize { - t.Errorf("UpdateIdSize() failed to update the size."+ - "\nexpected: %d\nrecieved: %d", newSize, s.idSize) - } -} - func TestStore_prune(t *testing.T) { kv := versioned.NewKV(make(ekv.Memstore)) s := NewStore(kv) diff --git a/storage/rounds/uncheckedRounds.go b/storage/rounds/uncheckedRounds.go new file mode 100644 index 0000000000000000000000000000000000000000..898222b71556ef86146d996e23c9804bff356f85 --- /dev/null +++ b/storage/rounds/uncheckedRounds.go @@ -0,0 +1,314 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package rounds + +import ( + "bytes" + "encoding/binary" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + "gitlab.com/elixxir/client/storage/versioned" + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "gitlab.com/xx_network/primitives/netTime" + "sync" + "time" +) + +const ( + uncheckedRoundVersion = 0 + uncheckedRoundPrefix = "uncheckedRoundPrefix" + // Key to store rounds + uncheckedRoundKey = "uncheckRounds" + // Key to store individual round + // Housekeeping constant (used for serializing uint64 ie id.Round) + uint64Size = 8 + // Maximum checks that can be performed on a round. Intended so that + // a round is checked no more than 1 week approximately (network/rounds.cappedTries + 7) + maxChecks = 14 +) + +// Round identity information used in message retrieval +// Derived from reception.Identity saving data needed +// for message retrieval +type Identity struct { + EpdId ephemeral.Id + Source *id.ID +} + +// Unchecked round structure is rounds which failed on message retrieval +// These rounds are stored for retry of message retrieval +type UncheckedRound struct { + Info *pb.RoundInfo + Identity + // Timestamp in which round has last been checked + LastCheck time.Time + // Number of times a round has been checked + NumChecks uint64 +} + +// marshal serializes UncheckedRound r into a byte slice +func (r UncheckedRound) marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + // Write the round info + b := make([]byte, uint64Size) + infoBytes, err := proto.Marshal(r.Info) + binary.LittleEndian.PutUint64(b, uint64(len(infoBytes))) + buf.Write(b) + buf.Write(infoBytes) + + b = make([]byte, uint64Size) + + // Write the round identity info + buf.Write(r.Identity.EpdId[:]) + if r.Source != nil { + buf.Write(r.Identity.Source.Marshal()) + } else { + buf.Write(make([]byte, id.ArrIDLen)) + } + + // Write the time stamp bytes + tsBytes, err := r.LastCheck.MarshalBinary() + if err != nil { + return nil, errors.WithMessage(err, "Could not marshal timestamp ") + } + b = make([]byte, uint64Size) + binary.LittleEndian.PutUint64(b, uint64(len(tsBytes))) + buf.Write(b) + buf.Write(tsBytes) + + // Write the number of tries for this round + b = make([]byte, uint64Size) + binary.LittleEndian.PutUint64(b, r.NumChecks) + buf.Write(b) + + return buf.Bytes(), nil +} + +// unmarshal deserializes round data from buff into UncheckedRound r +func (r *UncheckedRound) unmarshal(buff *bytes.Buffer) error { + // Deserialize the roundInfo + roundInfoLen := binary.LittleEndian.Uint64(buff.Next(uint64Size)) + roundInfoBytes := buff.Next(int(roundInfoLen)) + ri := &pb.RoundInfo{} + if err := proto.Unmarshal(roundInfoBytes, ri); err != nil { + return errors.WithMessagef(err, "Failed to unmarshal roundInfo") + } + r.Info = ri + + // Deserialize the round identity information + copy(r.EpdId[:], buff.Next(uint64Size)) + + sourceId, err := id.Unmarshal(buff.Next(id.ArrIDLen)) + if err != nil { + return errors.WithMessage(err, "Failed to unmarshal round identity.source") + } + + r.Source = sourceId + + // Deserialize the timestamp bytes + timestampLen := binary.LittleEndian.Uint64(buff.Next(uint64Size)) + tsByes := buff.Next(int(uint64(timestampLen))) + if err = r.LastCheck.UnmarshalBinary(tsByes); err != nil { + return errors.WithMessage(err, "Failed to unmarshal round timestamp") + } + + r.NumChecks = binary.LittleEndian.Uint64(buff.Next(uint64Size)) + + return nil +} + +// Storage object saving rounds to retry for message retrieval +type UncheckedRoundStore struct { + list map[id.Round]UncheckedRound + mux sync.RWMutex + kv *versioned.KV +} + +// Constructor for a UncheckedRoundStore +func NewUncheckedStore(kv *versioned.KV) (*UncheckedRoundStore, error) { + kv = kv.Prefix(uncheckedRoundPrefix) + + urs := &UncheckedRoundStore{ + list: make(map[id.Round]UncheckedRound, 0), + kv: kv, + } + + return urs, urs.save() + +} + +// Loads an deserializes a UncheckedRoundStore from memory +func LoadUncheckedStore(kv *versioned.KV) (*UncheckedRoundStore, error) { + + kv = kv.Prefix(uncheckedRoundPrefix) + vo, err := kv.Get(uncheckedRoundKey, uncheckedRoundVersion) + if err != nil { + return nil, err + } + + urs := &UncheckedRoundStore{ + list: make(map[id.Round]UncheckedRound), + kv: kv, + } + + err = urs.unmarshal(vo.Data) + if err != nil { + return nil, errors.WithMessage(err, "Failed to load rounds from storage") + } + + return urs, err +} + +// Adds a round to check on the list and saves to memory +func (s *UncheckedRoundStore) AddRound(ri *pb.RoundInfo, ephID ephemeral.Id, source *id.ID) error { + s.mux.Lock() + defer s.mux.Unlock() + rid := id.Round(ri.ID) + + if _, exists := s.list[rid]; !exists { + newUncheckedRound := UncheckedRound{ + Info: ri, + Identity: Identity{ + EpdId: ephID, + Source: source, + }, + LastCheck: netTime.Now(), + NumChecks: 0, + } + + s.list[rid] = newUncheckedRound + + return s.save() + } + + return nil +} + +// Retrieves an UncheckedRound from the map, if it exists +func (s *UncheckedRoundStore) GetRound(rid id.Round) (UncheckedRound, bool) { + s.mux.RLock() + defer s.mux.RUnlock() + rnd, exists := s.list[rid] + return rnd, exists +} + +// Retrieves the list of rounds +func (s *UncheckedRoundStore) GetList() map[id.Round]UncheckedRound { + s.mux.RLock() + defer s.mux.RUnlock() + return s.list +} + +// Increments the amount of checks performed on this stored round +func (s *UncheckedRoundStore) IncrementCheck(rid id.Round) error { + s.mux.Lock() + defer s.mux.Unlock() + rnd, exists := s.list[rid] + if !exists { + return errors.Errorf("round %d could not be found in RAM", rid) + } + + // If a round has been checked the maximum amount of times, + // we bail the round by removing it from store and no longer checking + if rnd.NumChecks >= maxChecks { + if err := s.remove(rid); err != nil { + return errors.WithMessagef(err, "Round %d reached maximum checks "+ + "but could not be removed", rid) + } + return nil + } + + // Update the rounds state + rnd.LastCheck = netTime.Now() + rnd.NumChecks++ + s.list[rid] = rnd + return s.save() +} + +// Remove deletes a round from UncheckedRoundStore's list and from storage +func (s *UncheckedRoundStore) Remove(rid id.Round) error { + s.mux.Lock() + defer s.mux.Unlock() + return s.remove(rid) +} + +// Remove is a helper function which removes the round from UncheckedRoundStore's list +// Note this method is unsafe and should only be used by methods with a lock +func (s *UncheckedRoundStore) remove(rid id.Round) error { + if _, exists := s.list[rid]; !exists { + return errors.Errorf("round %d does not exist in store", rid) + } + delete(s.list, rid) + return s.save() +} + +// save stores the information from the round list into storage +func (s *UncheckedRoundStore) save() error { + // Store list of rounds + data, err := s.marshal() + if err != nil { + return errors.WithMessagef(err, "Could not marshal data for unchecked rounds") + } + + // Create the versioned object + obj := &versioned.Object{ + Version: uncheckedRoundVersion, + Timestamp: netTime.Now(), + Data: data, + } + + // Save to storage + err = s.kv.Set(uncheckedRoundKey, uncheckedRoundVersion, obj) + if err != nil { + return errors.WithMessagef(err, "Could not store data for unchecked rounds") + } + + return nil +} + +// marshal is a helper function which serializes all rounds in list to bytes +func (s *UncheckedRoundStore) marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + // Write number of rounds the buffer + b := make([]byte, 8) + binary.PutVarint(b, int64(len(s.list))) + buf.Write(b) + + for rid, rnd := range s.list { + rndData, err := rnd.marshal() + if err != nil { + return nil, errors.WithMessagef(err, "Failed to marshal round %d", rid) + } + + buf.Write(rndData) + + } + + return buf.Bytes(), nil +} + +// unmarshal deserializes an UncheckedRound from its stored byte data +func (s *UncheckedRoundStore) unmarshal(data []byte) error { + buff := bytes.NewBuffer(data) + // Get number of rounds in list + length, _ := binary.Varint(buff.Next(8)) + + for i := 0; i < int(length); i++ { + rnd := UncheckedRound{} + err := rnd.unmarshal(buff) + if err != nil { + return errors.WithMessage(err, "Failed to unmarshal rounds in storage") + } + + s.list[id.Round(rnd.Info.ID)] = rnd + } + + return nil +} diff --git a/storage/rounds/uncheckedRounds_test.go b/storage/rounds/uncheckedRounds_test.go new file mode 100644 index 0000000000000000000000000000000000000000..63e8c195f4f2ffa752979e5f5780acdc6d2d3f16 --- /dev/null +++ b/storage/rounds/uncheckedRounds_test.go @@ -0,0 +1,371 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package rounds + +import ( + "bytes" + "gitlab.com/elixxir/client/storage/versioned" + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "gitlab.com/xx_network/primitives/netTime" + "reflect" + "testing" +) + +// Unit test +func TestNewUncheckedStore(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + testStore := &UncheckedRoundStore{ + list: make(map[id.Round]UncheckedRound), + kv: kv.Prefix(uncheckedRoundPrefix), + } + + store, err := NewUncheckedStore(kv) + if err != nil { + t.Fatalf("NewUncheckedStore error: "+ + "Could not create unchecked stor: %v", err) + } + + // Compare manually created object with NewUnknownRoundsStore + if !reflect.DeepEqual(testStore, store) { + t.Fatalf("NewUncheckedStore error: "+ + "Returned incorrect Store."+ + "\n\texpected: %+v\n\treceived: %+v", testStore, store) + } + + rid := id.Round(1) + roundInfo := &pb.RoundInfo{ + ID: uint64(rid), + } + uncheckedRound := UncheckedRound{ + Info: roundInfo, + LastCheck: netTime.Now(), + NumChecks: 0, + } + + store.list[rid] = uncheckedRound + if err = store.save(); err != nil { + t.Fatalf("NewUncheckedStore error: "+ + "Could not save store: %v", err) + } + + // Test if round list data matches + expectedRoundData, err := store.marshal() + if err != nil { + t.Fatalf("NewUncheckedStore error: "+ + "Could not marshal data: %v", err) + } + roundData, err := store.kv.Get(uncheckedRoundKey, uncheckedRoundVersion) + if err != nil { + t.Fatalf("NewUncheckedStore error: "+ + "Could not retrieve round list form storage: %v", err) + } + + if !bytes.Equal(expectedRoundData, roundData.Data) { + t.Fatalf("NewUncheckedStore error: "+ + "Data from store was not expected"+ + "\n\tExpected %v\n\tReceived: %v", expectedRoundData, roundData.Data) + } + +} + +// Unit test +func TestLoadUncheckedStore(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + testStore, err := NewUncheckedStore(kv) + if err != nil { + t.Fatalf("LoadUncheckedStore error: "+ + "Could not call constructor NewUncheckedStore: %v", err) + } + + // Add round to store + rid := id.Round(0) + roundInfo := &pb.RoundInfo{ + ID: uint64(rid), + } + + ephId := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} + source := id.NewIdFromBytes([]byte("Sauron"), t) + err = testStore.AddRound(roundInfo, ephId, source) + if err != nil { + t.Fatalf("LoadUncheckedStore error: "+ + "Could not add round to store: %v", err) + } + + // Load store + loadedStore, err := LoadUncheckedStore(kv) + if err != nil { + t.Fatalf("LoadUncheckedStore error: "+ + "Could not call LoadUncheckedStore: %v", err) + } + + // Check if round is in loaded store + rnd, exists := loadedStore.list[rid] + if !exists { + t.Fatalf("LoadUncheckedStore error: "+ + "Added round %d not found in loaded store", rid) + } + + // Check if set values are expected + if !bytes.Equal(rnd.EpdId[:], ephId[:]) || + !source.Cmp(rnd.Source) { + t.Fatalf("LoadUncheckedStore error: "+ + "Values in loaded round %d are not expected."+ + "\n\tExpected ephemeral: %v"+ + "\n\tReceived ephemeral: %v"+ + "\n\tExpected source: %v"+ + "\n\tReceived source: %v", rid, + ephId, rnd.EpdId, + source, rnd.Source) + } + +} + +// Unit test +func TestUncheckedRoundStore_AddRound(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + testStore, err := NewUncheckedStore(kv) + if err != nil { + t.Fatalf("AddRound error: "+ + "Could not call constructor NewUncheckedStore: %v", err) + } + + // Add round to store + rid := id.Round(0) + roundInfo := &pb.RoundInfo{ + ID: uint64(rid), + } + ephId := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} + source := id.NewIdFromBytes([]byte("Sauron"), t) + err = testStore.AddRound(roundInfo, ephId, source) + if err != nil { + t.Fatalf("AddRound error: "+ + "Could not add round to store: %v", err) + } + + if _, exists := testStore.list[rid]; !exists { + t.Errorf("AddRound error: " + + "Could not find added round in list") + } + +} + +// Unit test +func TestUncheckedRoundStore_GetRound(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + testStore, err := NewUncheckedStore(kv) + if err != nil { + t.Fatalf("GetRound error: "+ + "Could not call constructor NewUncheckedStore: %v", err) + } + + // Add round to store + rid := id.Round(0) + roundInfo := &pb.RoundInfo{ + ID: uint64(rid), + } + ephId := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} + source := id.NewIdFromBytes([]byte("Sauron"), t) + err = testStore.AddRound(roundInfo, ephId, source) + if err != nil { + t.Fatalf("GetRound error: "+ + "Could not add round to store: %v", err) + } + + // Retrieve round that was inserted + retrievedRound, exists := testStore.GetRound(rid) + if !exists { + t.Fatalf("GetRound error: " + + "Could not get round from store") + } + + if !bytes.Equal(retrievedRound.EpdId[:], ephId[:]) || + !source.Cmp(retrievedRound.Source) { + t.Fatalf("GetRound error: "+ + "Values in loaded round %d are not expected."+ + "\n\tExpected ephemeral: %v"+ + "\n\tReceived ephemeral: %v"+ + "\n\tExpected source: %v"+ + "\n\tReceived source: %v", rid, + ephId, retrievedRound.EpdId, + source, retrievedRound.Source) + } + + // Try to pull unknown round from store + unknownRound := id.Round(1) + _, exists = testStore.GetRound(unknownRound) + if exists { + t.Fatalf("GetRound error: " + + "Should not find unknown round in store.") + } + +} + +// Unit test +func TestUncheckedRoundStore_GetList(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + testStore, err := NewUncheckedStore(kv) + if err != nil { + t.Fatalf("GetList error: "+ + "Could not call constructor NewUncheckedStore: %v", err) + } + + // Add rounds to store + numRounds := 10 + for i := 0; i < numRounds; i++ { + rid := id.Round(i) + roundInfo := &pb.RoundInfo{ + ID: uint64(rid), + } + ephId := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} + source := id.NewIdFromUInt(uint64(i), id.User, t) + err = testStore.AddRound(roundInfo, ephId, source) + if err != nil { + t.Errorf("GetList error: "+ + "Could not add round to store: %v", err) + } + } + + // Retrieve list + retrievedList := testStore.GetList() + if len(retrievedList) != numRounds { + t.Errorf("GetList error: "+ + "List returned is not of expected size."+ + "\n\tExpected: %v\n\tReceived: %v", numRounds, len(retrievedList)) + } + + for i := 0; i < numRounds; i++ { + rid := id.Round(i) + if _, exists := retrievedList[rid]; !exists { + t.Errorf("GetList error: "+ + "Retrieved list does not contain expected round %d.", rid) + } + } + +} + +// Unit test +func TestUncheckedRoundStore_IncrementCheck(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + testStore, err := NewUncheckedStore(kv) + if err != nil { + t.Fatalf("IncrementCheck error: "+ + "Could not call constructor NewUncheckedStore: %v", err) + } + + // Add rounds to store + numRounds := 10 + for i := 0; i < numRounds; i++ { + rid := id.Round(i) + roundInfo := &pb.RoundInfo{ + ID: uint64(rid), + } + ephId := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} + source := id.NewIdFromUInt(uint64(i), id.User, t) + err = testStore.AddRound(roundInfo, ephId, source) + if err != nil { + t.Errorf("IncrementCheck error: "+ + "Could not add round to store: %v", err) + } + } + + testRound := id.Round(3) + numChecks := 4 + for i := 0; i < numChecks; i++ { + err = testStore.IncrementCheck(testRound) + if err != nil { + t.Errorf("IncrementCheck error: "+ + "Could not increment check for round %d: %v", testRound, err) + } + } + + rnd, _ := testStore.GetRound(testRound) + if rnd.NumChecks != uint64(numChecks) { + t.Errorf("IncrementCheck error: "+ + "Round %d did not have expected number of checks."+ + "\n\tExpected: %v\n\tReceived: %v", testRound, numChecks, rnd.NumChecks) + } + + // Error path: check unknown round can not be incremented + unknownRound := id.Round(numRounds + 5) + err = testStore.IncrementCheck(unknownRound) + if err == nil { + t.Errorf("IncrementCheck error: "+ + "Should not find round %d which was not added to store", unknownRound) + } + + // Reach max checks, ensure that round is removed + maxRound := id.Round(7) + for i := 0; i < maxChecks+1; i++ { + err = testStore.IncrementCheck(maxRound) + if err != nil { + t.Errorf("IncrementCheck error: "+ + "Could not increment check for round %d: %v", maxRound, err) + } + + } + +} + +// Unit test +func TestUncheckedRoundStore_Remove(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + testStore, err := NewUncheckedStore(kv) + if err != nil { + t.Fatalf("Remove error: "+ + "Could not call constructor NewUncheckedStore: %v", err) + } + + // Add rounds to store + numRounds := 10 + for i := 0; i < numRounds; i++ { + rid := id.Round(i) + roundInfo := &pb.RoundInfo{ + ID: uint64(rid), + } + ephId := ephemeral.Id{1, 2, 3, 4, 5, 6, 7, 8} + source := id.NewIdFromUInt(uint64(i), id.User, t) + err = testStore.AddRound(roundInfo, ephId, source) + if err != nil { + t.Errorf("Remove error: "+ + "Could not add round to store: %v", err) + } + } + + // Remove round from storage + removedRound := id.Round(1) + err = testStore.Remove(removedRound) + if err != nil { + t.Errorf("Remove error: "+ + "Could not removed round %d from storage: %v", removedRound, err) + } + + // Check that round was removed + _, exists := testStore.GetRound(removedRound) + if exists { + t.Errorf("Remove error: "+ + "Round %d expected to be removed from storage", removedRound) + } + + // Error path: attempt to remove unknown round + unknownRound := id.Round(numRounds + 5) + err = testStore.Remove(unknownRound) + if err == nil { + t.Errorf("Remove error: "+ + "Should not removed round %d which is not in storage", unknownRound) + } + +} diff --git a/storage/session.go b/storage/session.go index 608068472b49517922d6a84a8a57d90f9dc0054f..fd98a85c22e1d1c3aea073ad3d1d9d6a709c6e1c 100644 --- a/storage/session.go +++ b/storage/session.go @@ -10,8 +10,11 @@ package storage import ( + "gitlab.com/elixxir/client/storage/hostList" + "gitlab.com/elixxir/client/storage/rounds" "sync" "testing" + "time" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -43,12 +46,13 @@ const currentSessionVersion = 0 // Session object, backed by encrypted filestore type Session struct { - kv *versioned.KV + kv *versioned.KV + mux sync.RWMutex //memoized data regStatus RegistrationStatus - baseNdf *ndf.NetworkDefinition + ndf *ndf.NetworkDefinition //sub-stores e2e *e2e.Store @@ -62,6 +66,8 @@ type Session struct { garbledMessages *utility.MeteredCmixMessageBuffer reception *reception.Store clientVersion *clientVersion.Store + uncheckedRounds *rounds.UncheckedRoundStore + hostList *hostList.Store } // Initialize a new Session object @@ -141,6 +147,13 @@ func New(baseDir, password string, u userInterface.User, currentVersion version. return nil, errors.WithMessage(err, "Failed to create client version store.") } + s.uncheckedRounds, err = rounds.NewUncheckedStore(s.kv) + if err != nil { + return nil, errors.WithMessage(err, "Failed to create unchecked round store") + } + + s.hostList = hostList.NewStore(s.kv) + return s, nil } @@ -208,10 +221,17 @@ func Load(baseDir, password string, currentVersion version.Version, } s.conversations = conversation.NewStore(s.kv) - s.partition = partition.New(s.kv) + s.partition = partition.Load(s.kv) s.reception = reception.LoadStore(s.kv) + s.uncheckedRounds, err = rounds.LoadUncheckedStore(s.kv) + if err != nil { + return nil, errors.WithMessage(err, "Failed to load unchecked round store") + } + + s.hostList = hostList.NewStore(s.kv) + return s, nil } @@ -282,6 +302,18 @@ func (s *Session) Partition() *partition.Store { return s.partition } +func (s *Session) UncheckedRounds() *rounds.UncheckedRoundStore { + s.mux.RLock() + defer s.mux.RUnlock() + return s.uncheckedRounds +} + +func (s *Session) HostList() *hostList.Store { + s.mux.RLock() + defer s.mux.RUnlock() + return s.hostList +} + // Get an object from the session func (s *Session) Get(key string) (*versioned.Object, error) { return s.kv.Get(key, currentSessionVersion) @@ -297,6 +329,13 @@ func (s *Session) Delete(key string) error { return s.kv.Delete(key, currentSessionVersion) } +// GetKV returns the Session versioned.KV. +func (s *Session) GetKV() *versioned.KV { + s.mux.RLock() + defer s.mux.RUnlock() + return s.kv +} + // Initializes a Session object wrapped around a MemStore object. // FOR TESTING ONLY func InitTestingSession(i interface{}) *Session { @@ -318,6 +357,13 @@ func InitTestingSession(i interface{}) *Session { } u.SetTransmissionRegistrationValidationSignature([]byte("sig")) u.SetReceptionRegistrationValidationSignature([]byte("sig")) + testTime, err := time.Parse(time.RFC3339, + "2012-12-21T22:08:41+00:00") + if err != nil { + jww.FATAL.Panicf("Could not parse precanned time: %v", err.Error()) + } + u.SetRegistrationTimestamp(testTime.UnixNano()) + s.user = u cmixGrp := cyclic.NewGroup( large.NewIntFromString("9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642F0B5C48"+ @@ -364,5 +410,12 @@ func InitTestingSession(i interface{}) *Session { s.reception = reception.NewStore(s.kv) + s.uncheckedRounds, err = rounds.NewUncheckedStore(s.kv) + if err != nil { + jww.FATAL.Panicf("Failed to create uncheckRound store: %v", err) + } + + s.hostList = hostList.NewStore(s.kv) + return s } diff --git a/storage/user.go b/storage/user.go index df1d9055e3490a3290d9ad378b3857bc16ca7cc4..975b574a2e9678a31eff7f0598b396ab6427a6af 100644 --- a/storage/user.go +++ b/storage/user.go @@ -14,17 +14,18 @@ func (s *Session) GetUser() user.User { defer s.mux.RUnlock() ci := s.user.GetCryptographicIdentity() return user.User{ - TransmissionID: ci.GetTransmissionID().DeepCopy(), - TransmissionSalt: copySlice(ci.GetTransmissionSalt()), - TransmissionRSA: ci.GetReceptionRSA(), - ReceptionID: ci.GetReceptionID().DeepCopy(), - ReceptionSalt: copySlice(ci.GetReceptionSalt()), - ReceptionRSA: ci.GetReceptionRSA(), - Precanned: ci.IsPrecanned(), - CmixDhPrivateKey: s.cmix.GetDHPrivateKey().DeepCopy(), - CmixDhPublicKey: s.cmix.GetDHPublicKey().DeepCopy(), - E2eDhPrivateKey: s.e2e.GetDHPrivateKey().DeepCopy(), - E2eDhPublicKey: s.e2e.GetDHPublicKey().DeepCopy(), + TransmissionID: ci.GetTransmissionID().DeepCopy(), + TransmissionSalt: copySlice(ci.GetTransmissionSalt()), + TransmissionRSA: ci.GetReceptionRSA(), + ReceptionID: ci.GetReceptionID().DeepCopy(), + RegistrationTimestamp: s.user.GetRegistrationTimestamp(), + ReceptionSalt: copySlice(ci.GetReceptionSalt()), + ReceptionRSA: ci.GetReceptionRSA(), + Precanned: ci.IsPrecanned(), + CmixDhPrivateKey: s.cmix.GetDHPrivateKey().DeepCopy(), + CmixDhPublicKey: s.cmix.GetDHPublicKey().DeepCopy(), + E2eDhPrivateKey: s.e2e.GetDHPrivateKey().DeepCopy(), + E2eDhPublicKey: s.e2e.GetDHPublicKey().DeepCopy(), } } diff --git a/storage/user/regValidationSig.go b/storage/user/registation.go similarity index 70% rename from storage/user/regValidationSig.go rename to storage/user/registation.go index 7440500fc680e2aee821ac761f47c8d9b201f40a..4da19b4cd0faf8e83fe22ad2e177afb8d8109897 100644 --- a/storage/user/regValidationSig.go +++ b/storage/user/registation.go @@ -8,14 +8,18 @@ package user import ( + "encoding/binary" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/xx_network/primitives/netTime" + "time" ) const currentRegValidationSigVersion = 0 +const registrationTimestampVersion = 0 const transmissionRegValidationSigKey = "transmissionRegistrationValidationSignature" const receptionRegValidationSigKey = "receptionRegistrationValidationSignature" +const registrationTimestampKey = "registrationTimestamp" // Returns the transmission Identity Validation Signature stored in RAM. May return // nil of no signature is stored @@ -33,6 +37,13 @@ func (u *User) GetReceptionRegistrationValidationSignature() []byte { return u.receptionRegValidationSig } +// Returns the registration timestamp stored in RAM as +func (u *User) GetRegistrationTimestamp() time.Time { + u.rvsMux.RLock() + defer u.rvsMux.RUnlock() + return u.registrationTimestamp +} + // Loads the transmission Identity Validation Signature if it exists in the ekv func (u *User) loadTransmissionRegistrationValidationSignature() { u.rvsMux.Lock() @@ -55,6 +66,18 @@ func (u *User) loadReceptionRegistrationValidationSignature() { u.rvsMux.Unlock() } +// Loads the registration timestamp if it exists in the ekv +func (u *User) loadRegistrationTimestamp() { + u.rvsMux.Lock() + obj, err := u.kv.Get(registrationTimestampKey, + registrationTimestampVersion) + if err == nil { + tsNano := binary.BigEndian.Uint64(obj.Data) + u.registrationTimestamp = time.Unix(0, int64(tsNano)) + } + u.rvsMux.Unlock() +} + // Sets the Identity Validation Signature if it is not set and stores it in // the ekv func (u *User) SetTransmissionRegistrationValidationSignature(b []byte) { @@ -108,3 +131,34 @@ func (u *User) SetReceptionRegistrationValidationSignature(b []byte) { u.receptionRegValidationSig = b } + +// Sets the Registration Timestamp if it is not set and stores it in +// the ekv +func (u *User) SetRegistrationTimestamp(tsNano int64) { + u.rvsMux.Lock() + defer u.rvsMux.Unlock() + + //check if the signature already exists + if !u.registrationTimestamp.IsZero() { + jww.FATAL.Panicf("cannot overwrite existing registration timestamp") + } + + // Serialize the timestamp + tsBytes := make([]byte, 8) + binary.BigEndian.PutUint64(tsBytes, uint64(tsNano)) + + obj := &versioned.Object{ + Version: currentRegValidationSigVersion, + Timestamp: netTime.Now(), + Data: tsBytes, + } + + err := u.kv.Set(registrationTimestampKey, + registrationTimestampVersion, obj) + if err != nil { + jww.FATAL.Panicf("Failed to store the reception timestamp: %s", err) + } + + u.registrationTimestamp = time.Unix(0, tsNano) + +} diff --git a/storage/user/regValidationSig_test.go b/storage/user/registation_test.go similarity index 64% rename from storage/user/regValidationSig_test.go rename to storage/user/registation_test.go index 68a69f60c8a8dca24161a4ec8bc9721d096aa6a9..31c6d3d1ee67f4e81fddd4a5822a18d0ad4d7ccf 100644 --- a/storage/user/regValidationSig_test.go +++ b/storage/user/registation_test.go @@ -9,12 +9,14 @@ package user import ( "bytes" + "encoding/binary" "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/elixxir/ekv" "gitlab.com/xx_network/crypto/signature/rsa" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" "testing" + "time" ) // Test User GetRegistrationValidationSignature function @@ -137,3 +139,88 @@ func TestUser_loadRegistrationValidationSignature(t *testing.T) { t.Errorf("Expected sig did not match loaded. Expected: %+v, Received: %+v", sig, u.receptionRegValidationSig) } } + +// Test User's getter/setter functions for TimeStamp +func TestUser_GetRegistrationTimestamp(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + uid := id.NewIdFromString("test", id.User, t) + salt := []byte("salt") + u, err := NewUser(kv, uid, uid, salt, salt, &rsa.PrivateKey{}, &rsa.PrivateKey{}, false) + if err != nil || u == nil { + t.Errorf("Failed to create new user: %+v", err) + } + + testTime, err := time.Parse(time.RFC3339, + "2012-12-21T22:08:41+00:00") + if err != nil { + t.Fatalf("Could not parse precanned time: %v", err.Error()) + } + + // Test that User has been modified for timestamp + u.SetRegistrationTimestamp(testTime.UnixNano()) + if !testTime.Equal(u.registrationTimestamp) { + t.Errorf("SetRegistrationTimestamp did not set user's timestamp value."+ + "\n\tExpected: %s\n\tReceieved: %s", testTime.String(), u.registrationTimestamp) + } + + // Pull timestamp from kv + obj, err := u.kv.Get(registrationTimestampKey, registrationTimestampVersion) + if err != nil { + t.Errorf("Failed to get reg vaildation signature key: %+v", err) + } + + // Check if kv data is expected + unixNano := binary.BigEndian.Uint64(obj.Data) + if testTime.UnixNano() != int64(unixNano) { + t.Errorf("Timestamp pulled from kv was not expected."+ + "\n\tExpected: %d\n\tReceieved: %d", testTime.UnixNano(), unixNano) + } + + if testTime.UnixNano() != u.GetRegistrationTimestamp().UnixNano() { + t.Errorf("Timestamp from GetRegistrationTimestampNano was not expected."+ + "\n\tExpected: %d\n\tReceieved: %d", testTime.UnixNano(), u.GetRegistrationTimestamp().UnixNano()) + } + + if !testTime.Equal(u.GetRegistrationTimestamp()) { + t.Errorf("Timestamp from GetRegistrationTimestamp was not expected."+ + "\n\tExpected: %s\n\tReceieved: %s", testTime, u.GetRegistrationTimestamp()) + + } + +} + +// Test loading registrationTimestamp from the KV store +func TestUser_loadRegistrationTimestamp(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + uid := id.NewIdFromString("test", id.User, t) + salt := []byte("salt") + u, err := NewUser(kv, uid, uid, salt, salt, &rsa.PrivateKey{}, &rsa.PrivateKey{}, false) + if err != nil || u == nil { + t.Errorf("Failed to create new user: %+v", err) + } + + testTime, err := time.Parse(time.RFC3339, + "2012-12-21T22:08:41+00:00") + if err != nil { + t.Fatalf("Could not parse precanned time: %v", err.Error()) + } + + data := make([]byte, 8) + binary.BigEndian.PutUint64(data, uint64(testTime.UnixNano())) + vo := &versioned.Object{ + Version: currentRegValidationSigVersion, + Timestamp: netTime.Now(), + Data: data, + } + err = kv.Set(registrationTimestampKey, + registrationTimestampVersion, vo) + if err != nil { + t.Errorf("Failed to set reg validation sig key in kv store: %+v", err) + } + + u.loadRegistrationTimestamp() + if !testTime.Equal(u.registrationTimestamp) { + t.Errorf("SetRegistrationTimestamp did not set user's timestamp value."+ + "\n\tExpected: %s\n\tReceieved: %s", testTime.String(), u.registrationTimestamp) + } +} diff --git a/storage/user/user.go b/storage/user/user.go index a55830ff131620f7f9f499b2a6932d616675ccda..c1779524245cd729df731243837d772ddf7b2c03 100644 --- a/storage/user/user.go +++ b/storage/user/user.go @@ -13,6 +13,7 @@ import ( "gitlab.com/xx_network/crypto/signature/rsa" "gitlab.com/xx_network/primitives/id" "sync" + "time" ) type User struct { @@ -20,7 +21,9 @@ type User struct { transmissionRegValidationSig []byte receptionRegValidationSig []byte - rvsMux sync.RWMutex + // Time in which user registered with the network + registrationTimestamp time.Time + rvsMux sync.RWMutex username string usernameMux sync.RWMutex @@ -48,6 +51,7 @@ func LoadUser(kv *versioned.KV) (*User, error) { u.loadTransmissionRegistrationValidationSignature() u.loadReceptionRegistrationValidationSignature() u.loadUsername() + u.loadRegistrationTimestamp() return u, nil } diff --git a/storage/utility/cmixMessageBuffer.go b/storage/utility/cmixMessageBuffer.go index 6091c65bc59ee0836384d4a475a15f8c8f7a940e..8c67658932ab64e75d91e2272f3b94c9c90f6314 100644 --- a/storage/utility/cmixMessageBuffer.go +++ b/storage/utility/cmixMessageBuffer.go @@ -8,7 +8,6 @@ package utility import ( - "crypto/md5" "encoding/json" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -16,6 +15,7 @@ import ( "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" + "golang.org/x/crypto/blake2b" ) const currentCmixMessageVersion = 0 @@ -80,8 +80,14 @@ func (cmh *cmixMessageHandler) DeleteMessage(kv *versioned.KV, key string) error // HashMessage generates a hash of the message. func (cmh *cmixMessageHandler) HashMessage(m interface{}) MessageHash { - sm := m.(storedMessage) - return md5.Sum(sm.Marshal()) + h, _ := blake2b.New256(nil) + + h.Write(m.(storedMessage).Marshal()) + + var messageHash MessageHash + copy(messageHash[:], h.Sum(nil)) + + return messageHash } // CmixMessageBuffer wraps the message buffer to store and load raw cmix diff --git a/storage/utility/dh.go b/storage/utility/dh.go index 9b0280ed1ef4bd96e71d15fcc788b5633c51fe03..6295e446d3563127c4c1262733eb3abf8ac8b91e 100644 --- a/storage/utility/dh.go +++ b/storage/utility/dh.go @@ -42,3 +42,8 @@ func LoadCyclicKey(kv *versioned.KV, key string) (*cyclic.Int, error) { return cy, cy.GobDecode(vo.Data) } + +// DeleteCyclicKey deletes a given cyclic key from storage +func DeleteCyclicKey(kv *versioned.KV, key string) error { + return kv.Delete(key, currentCyclicVersion) +} diff --git a/storage/utility/dh_test.go b/storage/utility/dh_test.go index 4d4a31941faee3467f029ffca4dbc57f8fa9dafe..36fb3c5734af6ea7bd29479279f01b10522b803d 100644 --- a/storage/utility/dh_test.go +++ b/storage/utility/dh_test.go @@ -47,3 +47,27 @@ func TestLoadCyclicKey(t *testing.T) { t.Errorf("Stored int did not match received. Stored: %v, Received: %v", x, loaded) } } + +// Unit test for DeleteCyclicKey +func TestDeleteCyclicKey(t *testing.T) { + kv := make(ekv.Memstore) + vkv := versioned.NewKV(kv) + grp := getTestGroup() + x := grp.NewInt(77) + + intKey := "testKey" + err := StoreCyclicKey(vkv, x, intKey) + if err != nil { + t.Errorf("Failed to store cyclic key: %+v", err) + } + + err = DeleteCyclicKey(vkv, intKey) + if err != nil { + t.Fatalf("DeleteCyclicKey returned an error: %v", err) + } + + _, err = LoadCyclicKey(vkv, intKey) + if err == nil { + t.Errorf("DeleteCyclicKey error: Should not load deleted key: %+v", err) + } +} diff --git a/storage/utility/e2eMessageBuffer.go b/storage/utility/e2eMessageBuffer.go index b8de8f65d3219f662df87680d174aec47743b97f..259c6407a2c08c45b4f2e486182964031669b98c 100644 --- a/storage/utility/e2eMessageBuffer.go +++ b/storage/utility/e2eMessageBuffer.go @@ -8,7 +8,6 @@ package utility import ( - "crypto/md5" "encoding/binary" "encoding/json" jww "github.com/spf13/jwalterweatherman" @@ -17,6 +16,7 @@ import ( "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" + "golang.org/x/crypto/blake2b" ) const currentE2EMessageVersion = 0 @@ -80,17 +80,19 @@ func (emh *e2eMessageHandler) DeleteMessage(kv *versioned.KV, key string) error // Do not include the params in the hash so it is not needed to resubmit the // message into succeeded or failed func (emh *e2eMessageHandler) HashMessage(m interface{}) MessageHash { - msg := m.(e2eMessage) - - var digest []byte - digest = append(digest, msg.Recipient...) - digest = append(digest, msg.Payload...) + h, _ := blake2b.New256(nil) + msg := m.(e2eMessage) + h.Write(msg.Recipient) + h.Write(msg.Payload) mtBytes := make([]byte, 4) binary.BigEndian.PutUint32(mtBytes, msg.MessageType) - digest = append(digest, mtBytes...) + h.Write(mtBytes) + + var messageHash MessageHash + copy(messageHash[:], h.Sum(nil)) - return md5.Sum(digest) + return messageHash } // E2eMessageBuffer wraps the message buffer to store and load raw e2eMessages. diff --git a/storage/utility/messageBuffer_test.go b/storage/utility/messageBuffer_test.go index fc4de8be8dd5573520d02c9a35fae07593bb6a90..5e30960f9ccc3ad7060a876b7924fd3f0d8f3016 100644 --- a/storage/utility/messageBuffer_test.go +++ b/storage/utility/messageBuffer_test.go @@ -9,11 +9,11 @@ package utility import ( "bytes" - "crypto/md5" "encoding/json" "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/elixxir/ekv" "gitlab.com/xx_network/primitives/netTime" + "golang.org/x/crypto/blake2b" "math/rand" "os" "reflect" @@ -48,10 +48,14 @@ func (th *testHandler) DeleteMessage(kv *versioned.KV, key string) error { } func (th *testHandler) HashMessage(m interface{}) MessageHash { - mBytes := m.([]byte) - // Sum returns a array that is the exact same size as the MessageHash and Go - // apparently automatically casts it - return md5.Sum(mBytes) + h, _ := blake2b.New256(nil) + + h.Write(m.([]byte)) + + var messageHash MessageHash + copy(messageHash[:], h.Sum(nil)) + + return messageHash } func newTestHandler() *testHandler { @@ -343,7 +347,13 @@ func makeTestMessages(n int) ([][]byte, map[MessageHash]struct{}) { for i := range msgs { msgs[i] = make([]byte, 256) prng.Read(msgs[i]) - mh[md5.Sum(msgs[i])] = struct{}{} + + h, _ := blake2b.New256(nil) + h.Write(msgs[i]) + var messageHash MessageHash + copy(messageHash[:], h.Sum(nil)) + + mh[messageHash] = struct{}{} } return msgs, mh diff --git a/storage/utility/meteredCmixMessageBuffer.go b/storage/utility/meteredCmixMessageBuffer.go index 719faa3883ebf6d47e5abf86931c3e9e1b8e0143..dd5ade5a31a51469587c4dd8a32d67f07b9c515e 100644 --- a/storage/utility/meteredCmixMessageBuffer.go +++ b/storage/utility/meteredCmixMessageBuffer.go @@ -8,13 +8,13 @@ package utility import ( - "crypto/md5" "encoding/json" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/netTime" + "golang.org/x/crypto/blake2b" "time" ) @@ -77,9 +77,14 @@ func (*meteredCmixMessageHandler) DeleteMessage(kv *versioned.KV, key string) er // HashMessage generates a hash of the message. func (*meteredCmixMessageHandler) HashMessage(m interface{}) MessageHash { - msg := m.(meteredCmixMessage) + h, _ := blake2b.New256(nil) + + h.Write(m.(meteredCmixMessage).M) + + var messageHash MessageHash + copy(messageHash[:], h.Sum(nil)) - return md5.Sum(msg.M) + return messageHash } // CmixMessageBuffer wraps the message buffer to store and load raw cmix diff --git a/storage/utils_test.go b/storage/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6e9cc93343c47ea97587b76bb0951bc9e3e8ac94 --- /dev/null +++ b/storage/utils_test.go @@ -0,0 +1,55 @@ +package storage + +import ( + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/ndf" + "math/rand" +) + +// randID returns a new random ID of the specified type. +func randID(rng *rand.Rand, t id.Type) *id.ID { + newID, _ := id.NewRandomID(rng, t) + return newID +} + +func getNDF() *ndf.NetworkDefinition { + return &ndf.NetworkDefinition{ + E2E: ndf.Group{ + Prime: "E2EE983D031DC1DB6F1A7A67DF0E9A8E5561DB8E8D49413394C049B7A" + + "8ACCEDC298708F121951D9CF920EC5D146727AA4AE535B0922C688B55B3D" + + "D2AEDF6C01C94764DAB937935AA83BE36E67760713AB44A6337C20E78615" + + "75E745D31F8B9E9AD8412118C62A3E2E29DF46B0864D0C951C394A5CBBDC" + + "6ADC718DD2A3E041023DBB5AB23EBB4742DE9C1687B5B34FA48C3521632C" + + "4A530E8FFB1BC51DADDF453B0B2717C2BC6669ED76B4BDD5C9FF558E88F2" + + "6E5785302BEDBCA23EAC5ACE92096EE8A60642FB61E8F3D24990B8CB12EE" + + "448EEF78E184C7242DD161C7738F32BF29A841698978825B4111B4BC3E1E" + + "198455095958333D776D8B2BEEED3A1A1A221A6E37E664A64B83981C46FF" + + "DDC1A45E3D5211AAF8BFBC072768C4F50D7D7803D2D4F278DE8014A47323" + + "631D7E064DE81C0C6BFA43EF0E6998860F1390B5D3FEACAF1696015CB79C" + + "3F9C2D93D961120CD0E5F12CBB687EAB045241F96789C38E89D796138E63" + + "19BE62E35D87B1048CA28BE389B575E994DCA755471584A09EC723742DC3" + + "5873847AEF49F66E43873", + Generator: "2", + }, + CMIX: ndf.Group{ + Prime: "9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642" + + "F0B5C48C8F7A41AADFA187324B87674FA1822B00F1ECF8136943D7C55757" + + "264E5A1A44FFE012E9936E00C1D3E9310B01C7D179805D3058B2A9F4BB6F" + + "9716BFE6117C6B5B3CC4D9BE341104AD4A80AD6C94E005F4B993E14F091E" + + "B51743BF33050C38DE235567E1B34C3D6A5C0CEAA1A0F368213C3D19843D" + + "0B4B09DCB9FC72D39C8DE41F1BF14D4BB4563CA28371621CAD3324B6A2D3" + + "92145BEBFAC748805236F5CA2FE92B871CD8F9C36D3292B5509CA8CAA77A" + + "2ADFC7BFD77DDA6F71125A7456FEA153E433256A2261C6A06ED3693797E7" + + "995FAD5AABBCFBE3EDA2741E375404AE25B", + Generator: "5C7FF6B06F8F143FE8288433493E4769C4D988ACE5BE25A0E2480" + + "9670716C613D7B0CEE6932F8FAA7C44D2CB24523DA53FBE4F6EC3595892D" + + "1AA58C4328A06C46A15662E7EAA703A1DECF8BBB2D05DBE2EB956C142A33" + + "8661D10461C0D135472085057F3494309FFA73C611F78B32ADBB5740C361" + + "C9F35BE90997DB2014E2EF5AA61782F52ABEB8BD6432C4DD097BC5423B28" + + "5DAFB60DC364E8161F4A2A35ACA3A10B1C4D203CC76A470A33AFDCBDD929" + + "59859ABD8B56E1725252D78EAC66E71BA9AE3F1DD2487199874393CD4D83" + + "2186800654760E1E34C09E4D155179F9EC0DC4473F996BDCE6EED1CABED8" + + "B6F116F7AD9CF505DF0F998E34AB27514B0FFE7", + }, + } +} diff --git a/ud/lookup_test.go b/ud/lookup_test.go index 8bdb21a1ae0d347b8561da54a52d4c9dcf7bb33e..02162faaf2e47e555fe54dd8a6fd1d9a3dd36f03 100644 --- a/ud/lookup_test.go +++ b/ud/lookup_test.go @@ -198,6 +198,6 @@ func (s *mockSingleLookup) TransmitSingleUse(_ contact.Contact, payload []byte, return nil } -func (s *mockSingleLookup) StartProcesses() stoppable.Stoppable { - return stoppable.NewSingle("") +func (s *mockSingleLookup) StartProcesses() (stoppable.Stoppable, error) { + return stoppable.NewSingle(""), nil } diff --git a/ud/manager.go b/ud/manager.go index 842d8d353a6d05ab523db2f887c2036d169f976c..9342419bdbbe5b8edb01bb32f858032cc6da8186 100644 --- a/ud/manager.go +++ b/ud/manager.go @@ -15,13 +15,14 @@ import ( "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/crypto/signature/rsa" "gitlab.com/xx_network/primitives/id" + "math" "time" ) type SingleInterface interface { TransmitSingleUse(contact.Contact, []byte, string, uint8, single.ReplyComm, time.Duration) error - StartProcesses() stoppable.Stoppable + StartProcesses() (stoppable.Stoppable, error) } type Manager struct { @@ -50,9 +51,9 @@ type Manager struct { // updated NDF is available and will error if one is not. func NewManager(client *api.Client, single *single.Manager) (*Manager, error) { jww.INFO.Println("ud.NewManager()") - if !client.GetHealth().IsHealthy() { - return nil, errors.New("cannot start UD Manager when network was " + - "never healthy.") + if client.NetworkFollowerStatus() != api.Running { + return nil, errors.New("cannot start UD Manager when network follower is not " + + "running.") } m := &Manager{ @@ -89,6 +90,11 @@ func NewManager(client *api.Client, single *single.Manager) (*Manager, error) { // Create the user discovery host object hp := connect.GetDefaultHostParams() + // Client will not send KeepAlive packets + hp.KaClientOpts.Time = time.Duration(math.MaxInt64) + hp.MaxRetries = 3 + hp.SendTimeout = 3 * time.Second + hp.AuthEnabled = false m.host, err = m.comms.AddHost(&id.UDB, def.UDB.Address, []byte(def.UDB.Cert), hp) if err != nil { return nil, errors.WithMessage(err, "User Discovery host object could "+ diff --git a/ud/register.go b/ud/register.go index 764b8a2b96ee03e75b3eba91e44a2f31f4590f3b..8a25bafc01d150708136945a119de470b6e64e82 100644 --- a/ud/register.go +++ b/ud/register.go @@ -1,6 +1,7 @@ package ud import ( + "fmt" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" pb "gitlab.com/elixxir/comms/mixmessages" @@ -48,7 +49,8 @@ func (m *Manager) register(username string, comm registerUserComms) error { DhPubKey: m.storage.E2e().GetDHPublicKey().Bytes(), Salt: cryptoUser.GetReceptionSalt(), }, - UID: cryptoUser.GetReceptionID().Marshal(), + UID: cryptoUser.GetReceptionID().Marshal(), + Timestamp: user.GetRegistrationTimestamp().UnixNano(), } // Sign the identity data and add to user registration message @@ -84,6 +86,11 @@ func (m *Manager) register(username string, comm registerUserComms) error { if err == nil { err = m.setRegistered() + if m.client != nil { + m.client.ReportEvent(1, "UserDiscovery", "Registration", + fmt.Sprintf("User Registered with UD: %+v", + user)) + } } return err diff --git a/ud/remove.go b/ud/remove.go index 44c4a03dc8de6b432aa828549f8535f9fa620b29..99d9447f330cf0a5c133975abea3a29de1a71b0e 100644 --- a/ud/remove.go +++ b/ud/remove.go @@ -1,23 +1,27 @@ package ud import ( + "crypto/rand" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/crypto/factID" + "gitlab.com/elixxir/crypto/hash" "gitlab.com/elixxir/primitives/fact" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/comms/messages" + "gitlab.com/xx_network/crypto/signature/rsa" ) type removeFactComms interface { - SendDeleteMessage(host *connect.Host, message *mixmessages.FactRemovalRequest) (*messages.Ack, error) + SendRemoveFact(host *connect.Host, message *mixmessages.FactRemovalRequest) (*messages.Ack, error) } // Removes a previously confirmed fact. Will fail if the fact is not // associated with this client. func (m *Manager) RemoveFact(fact fact.Fact) error { jww.INFO.Printf("ud.RemoveFact(%s)", fact.Stringify()) - return m.removeFact(fact, nil) + return m.removeFact(fact, m.comms) } func (m *Manager) removeFact(fact fact.Fact, rFC removeFactComms) error { @@ -33,14 +37,71 @@ func (m *Manager) removeFact(fact fact.Fact, rFC removeFactComms) error { FactType: uint32(fact.T), } + // Create a hash of our fact + fhash := factID.Fingerprint(fact) + + // Sign our inFact for putting into the request + fsig, err := rsa.Sign(rand.Reader, m.privKey, hash.CMixHash, fhash, nil) + if err != nil { + return err + } + + // Create our Fact Removal Request message data + remFactMsg := mixmessages.FactRemovalRequest{ + UID: m.myID.Marshal(), + RemovalData: &mmFact, + FactSig: fsig, + } + + // Send the message + _, err = rFC.SendRemoveFact(m.host, &remFactMsg) + + // Return the error + return err +} + +type removeUserComms interface { + SendRemoveUser(host *connect.Host, message *mixmessages.FactRemovalRequest) (*messages.Ack, error) +} + +// Removes a previously confirmed fact. Will fail if the fact is not +// associated with this client. +func (m *Manager) RemoveUser(fact fact.Fact) error { + jww.INFO.Printf("ud.RemoveUser(%s)", fact.Stringify()) + return m.removeUser(fact, m.comms) +} + +func (m *Manager) removeUser(fact fact.Fact, rFC removeUserComms) error { + if !m.IsRegistered() { + return errors.New("Failed to remove fact: " + + "client is not registered") + } + + // Construct the message to send + // Convert our Fact to a mixmessages Fact for sending + mmFact := mixmessages.Fact{ + Fact: fact.Fact, + FactType: uint32(fact.T), + } + + // Create a hash of our fact + fhash := factID.Fingerprint(fact) + + // Sign our inFact for putting into the request + fsig, err := rsa.Sign(rand.Reader, m.privKey, hash.CMixHash, fhash, nil) + if err != nil { + return err + } + // Create our Fact Removal Request message data remFactMsg := mixmessages.FactRemovalRequest{ UID: m.myID.Marshal(), RemovalData: &mmFact, + FactSig: fsig, } // Send the message - _, err := rFC.SendDeleteMessage(m.host, &remFactMsg) + _, err = rFC.SendRemoveUser(m.host, &remFactMsg) // Return the error return err diff --git a/ud/remove_test.go b/ud/remove_test.go index bfe30a77de244b56c1ebd8dd96bb49abadce8ff1..d65cb0e8ccd888e374e42c2585fe41dfe2305c66 100644 --- a/ud/remove_test.go +++ b/ud/remove_test.go @@ -13,7 +13,7 @@ import ( type testRFC struct{} -func (rFC *testRFC) SendDeleteMessage(host *connect.Host, message *pb.FactRemovalRequest) (*messages.Ack, error) { +func (rFC *testRFC) SendRemoveFact(host *connect.Host, message *pb.FactRemovalRequest) (*messages.Ack, error) { return &messages.Ack{}, nil } @@ -51,3 +51,42 @@ func TestRemoveFact(t *testing.T) { t.Fatal(err) } } + +func (rFC *testRFC) SendRemoveUser(host *connect.Host, message *pb.FactRemovalRequest) (*messages.Ack, error) { + return &messages.Ack{}, nil +} + +func TestRemoveUser(t *testing.T) { + h, err := connect.NewHost(&id.DummyUser, "address", nil, connect.GetDefaultHostParams()) + if err != nil { + t.Fatal(err) + } + + rng := csprng.NewSystemRNG() + cpk, err := rsa.GenerateKey(rng, 2048) + if err != nil { + t.Fatal(err) + } + + isReg := uint32(1) + + m := Manager{ + comms: nil, + host: h, + privKey: cpk, + registered: &isReg, + myID: &id.ID{}, + } + + f := fact.Fact{ + Fact: "testing", + T: 2, + } + + trfc := testRFC{} + + err = m.removeUser(f, &trfc) + if err != nil { + t.Fatal(err) + } +} diff --git a/ud/search.go b/ud/search.go index 8624c778d1be0d6f8443b6fe0ca13153ac82c429..41001234896bba755fd6f032afdb19df8a23b22b 100644 --- a/ud/search.go +++ b/ud/search.go @@ -1,6 +1,7 @@ package ud import ( + "fmt" "github.com/golang/protobuf/proto" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -50,11 +51,17 @@ func (m *Manager) Search(list fact.FactList, callback searchCallback, timeout ti return errors.WithMessage(err, "Failed to transmit search request.") } + if m.client != nil { + m.client.ReportEvent(1, "UserDiscovery", "SearchRequest", + fmt.Sprintf("Sent: %+v", request)) + } + return nil } func (m *Manager) searchResponseHandler(factMap map[string]fact.Fact, callback searchCallback, payload []byte, err error) { + if err != nil { go callback(nil, errors.WithMessage(err, "Failed to search.")) return @@ -66,6 +73,12 @@ func (m *Manager) searchResponseHandler(factMap map[string]fact.Fact, jww.WARN.Printf("Dropped a search response from user discovery due to "+ "failed unmarshal: %s", err) } + + if m.client != nil { + m.client.ReportEvent(1, "UserDiscovery", "SearchResponse", + fmt.Sprintf("Received: %+v", searchResponse)) + } + if searchResponse.Error != "" { err = errors.Errorf("User Discovery returned an error on search: %s", searchResponse.Error) @@ -73,6 +86,11 @@ func (m *Manager) searchResponseHandler(factMap map[string]fact.Fact, return } + //return an error if no facts are found + if len(searchResponse.Contacts) == 0 { + go callback(nil, errors.New("No contacts found in search")) + } + c, err := m.parseContacts(searchResponse.Contacts, factMap) if err != nil { go callback(nil, errors.WithMessage(err, "Failed to parse contacts from "+ @@ -114,12 +132,15 @@ func (m *Manager) parseContacts(response []*Contact, if err != nil { return nil, errors.Errorf("failed to parse Contact user ID: %+v", err) } - + var facts []fact.Fact + if c.Username != "" { + facts = []fact.Fact{{c.Username, fact.Username}} + } // Create new Contact contacts[i] = contact.Contact{ ID: uid, DhPubKey: m.grp.NewIntFromBytes(c.PubKey), - Facts: []fact.Fact{}, + Facts: facts, } // Assign each Fact with a matching hash to the Contact diff --git a/ud/search_test.go b/ud/search_test.go index ac14e1a63243fc4d0296768c23420cf1adc03f16..c333cf08c5beede7a0ea8389b3adcbb2b4e18544 100644 --- a/ud/search_test.go +++ b/ud/search_test.go @@ -448,6 +448,46 @@ func TestManager_parseContacts(t *testing.T) { } } +func TestManager_parseContacts_username(t *testing.T) { + m := &Manager{grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2))} + + // Generate fact list + var factList fact.FactList + for i := 0; i < 10; i++ { + factList = append(factList, fact.Fact{ + Fact: fmt.Sprintf("fact %d", i), + T: fact.FactType(rand.Intn(4)), + }) + } + factHashes, factMap := hashFactList(factList) + + var contacts []*Contact + var expectedContacts []contact.Contact + for i, hash := range factHashes { + contacts = append(contacts, &Contact{ + UserID: id.NewIdFromString("user", id.User, t).Marshal(), + Username: "zezima", + PubKey: []byte{byte(i + 1)}, + TrigFacts: []*HashFact{hash}, + }) + expectedContacts = append(expectedContacts, contact.Contact{ + ID: id.NewIdFromString("user", id.User, t), + DhPubKey: m.grp.NewIntFromBytes([]byte{byte(i + 1)}), + Facts: fact.FactList{{"zezima", fact.Username}, factMap[string(hash.Hash)]}, + }) + } + + testContacts, err := m.parseContacts(contacts, factMap) + if err != nil { + t.Errorf("parseContacts() returned an error: %+v", err) + } + + if !reflect.DeepEqual(expectedContacts, testContacts) { + t.Errorf("parseContacts() did not return the expected contacts."+ + "\nexpected: %+v\nreceived: %+v", expectedContacts, testContacts) + } +} + // Error path: provided contact IDs are malformed and cannot be unmarshaled. func TestManager_parseContacts_IdUnmarshalError(t *testing.T) { m := &Manager{grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2))} @@ -487,6 +527,6 @@ func (s *mockSingleSearch) TransmitSingleUse(partner contact.Contact, payload [] return nil } -func (s *mockSingleSearch) StartProcesses() stoppable.Stoppable { - return stoppable.NewSingle("") +func (s *mockSingleSearch) StartProcesses() (stoppable.Stoppable, error) { + return stoppable.NewSingle(""), nil } diff --git a/ud/udMessages.pb.go b/ud/udMessages.pb.go index 1a245ab0c669f283d9ac5ee522d6e3f763501c4e..82477b3fdd3d3a968997295895f227e1096c7501 100644 --- a/ud/udMessages.pb.go +++ b/ud/udMessages.pb.go @@ -73,7 +73,8 @@ func (m *HashFact) GetType() int32 { type Contact struct { UserID []byte `protobuf:"bytes,1,opt,name=userID,proto3" json:"userID,omitempty"` PubKey []byte `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` - TrigFacts []*HashFact `protobuf:"bytes,3,rep,name=trigFacts,proto3" json:"trigFacts,omitempty"` + Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` + TrigFacts []*HashFact `protobuf:"bytes,4,rep,name=trigFacts,proto3" json:"trigFacts,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -118,6 +119,13 @@ func (m *Contact) GetPubKey() []byte { return nil } +func (m *Contact) GetUsername() string { + if m != nil { + return m.Username + } + return "" +} + func (m *Contact) GetTrigFacts() []*HashFact { if m != nil { return m.TrigFacts @@ -312,25 +320,28 @@ func init() { proto.RegisterType((*LookupResponse)(nil), "parse.LookupResponse") } -func init() { proto.RegisterFile("udMessages.proto", fileDescriptor_9e0cfdc16fb09bb6) } +func init() { + proto.RegisterFile("udMessages.proto", fileDescriptor_9e0cfdc16fb09bb6) +} var fileDescriptor_9e0cfdc16fb09bb6 = []byte{ - // 266 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0xc1, 0x4b, 0xc3, 0x30, - 0x14, 0xc6, 0xc9, 0xda, 0xce, 0xed, 0x39, 0xaa, 0x04, 0x91, 0x1e, 0x4b, 0xf4, 0x50, 0x04, 0x0b, - 0xce, 0xbb, 0x07, 0x15, 0x51, 0xd4, 0x4b, 0x76, 0xf3, 0x96, 0xb5, 0xcf, 0x55, 0x84, 0x26, 0xe4, - 0x25, 0x87, 0xfd, 0xf7, 0xd2, 0x34, 0x6e, 0x08, 0xf3, 0x96, 0xef, 0xbd, 0xf7, 0xe3, 0xfb, 0xde, - 0x0b, 0x9c, 0xfa, 0xf6, 0x1d, 0x89, 0xd4, 0x06, 0xa9, 0x36, 0x56, 0x3b, 0xcd, 0x33, 0xa3, 0x2c, - 0xa1, 0x58, 0xc2, 0xec, 0x59, 0x51, 0xf7, 0xa4, 0x1a, 0xc7, 0x39, 0xa4, 0x9d, 0xa2, 0xae, 0x60, - 0x25, 0xab, 0x16, 0x32, 0xbc, 0x87, 0x9a, 0xdb, 0x1a, 0x2c, 0x26, 0x25, 0xab, 0x32, 0x19, 0xde, - 0xa2, 0x83, 0xa3, 0x07, 0xdd, 0xbb, 0x01, 0x39, 0x87, 0xa9, 0x27, 0xb4, 0x2f, 0x8f, 0x11, 0x8a, - 0x6a, 0xa8, 0x1b, 0xbf, 0x7e, 0xc5, 0x6d, 0x00, 0x17, 0x32, 0x2a, 0x7e, 0x0d, 0x73, 0x67, 0xbf, - 0x36, 0x83, 0x1d, 0x15, 0x49, 0x99, 0x54, 0xc7, 0xcb, 0x93, 0x3a, 0x24, 0xa9, 0x7f, 0x63, 0xc8, - 0xfd, 0x84, 0xb8, 0x01, 0x58, 0xa1, 0xb2, 0x4d, 0xb7, 0xc2, 0xbe, 0xe5, 0x17, 0x90, 0x7e, 0xaa, - 0xc6, 0x15, 0xec, 0x30, 0x17, 0x9a, 0x42, 0x42, 0x3e, 0x22, 0x12, 0xc9, 0xe8, 0x9e, 0x90, 0x5f, - 0xc1, 0xac, 0x19, 0xe3, 0x52, 0x44, 0xf3, 0x88, 0xc6, 0x2d, 0xe4, 0xae, 0xcf, 0xcf, 0x20, 0x43, - 0x6b, 0xb5, 0x2d, 0x92, 0x92, 0x55, 0x73, 0x39, 0x0a, 0x71, 0x09, 0xf0, 0xa6, 0xf5, 0xb7, 0x37, - 0x21, 0xc6, 0x3f, 0x3b, 0x8b, 0x3b, 0xc8, 0xc7, 0xa9, 0x9d, 0xf3, 0xfe, 0x0a, 0xec, 0xcf, 0x15, - 0x0e, 0xba, 0xdc, 0xa7, 0x1f, 0x13, 0xdf, 0xae, 0xa7, 0xe1, 0x7b, 0x6e, 0x7f, 0x02, 0x00, 0x00, - 0xff, 0xff, 0xb4, 0xff, 0x7b, 0xf5, 0xb2, 0x01, 0x00, 0x00, + // 281 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0xc1, 0x4b, 0xf4, 0x30, + 0x10, 0xc5, 0xc9, 0x6e, 0xbb, 0x5f, 0x77, 0xbe, 0xa5, 0x4a, 0x10, 0x29, 0x9e, 0x4a, 0xf4, 0x50, + 0x04, 0x0b, 0xae, 0x77, 0x0f, 0x2a, 0xa2, 0xa8, 0x97, 0xec, 0xcd, 0x5b, 0xb6, 0x1d, 0xb7, 0x22, + 0x36, 0x21, 0x93, 0x1e, 0xf6, 0xee, 0x1f, 0x2e, 0x4d, 0x63, 0x17, 0x61, 0xbd, 0xe5, 0xcd, 0xcc, + 0x8f, 0x79, 0xf3, 0x02, 0x87, 0x5d, 0xfd, 0x82, 0x44, 0x6a, 0x83, 0x54, 0x1a, 0xab, 0x9d, 0xe6, + 0xb1, 0x51, 0x96, 0x50, 0x2c, 0x21, 0x79, 0x50, 0xd4, 0xdc, 0xab, 0xca, 0x71, 0x0e, 0x51, 0xa3, + 0xa8, 0xc9, 0x58, 0xce, 0x8a, 0x85, 0xf4, 0xef, 0xbe, 0xe6, 0xb6, 0x06, 0xb3, 0x49, 0xce, 0x8a, + 0x58, 0xfa, 0xb7, 0xf8, 0x62, 0xf0, 0xef, 0x56, 0xb7, 0xae, 0x67, 0x8e, 0x61, 0xd6, 0x11, 0xda, + 0xc7, 0xbb, 0x40, 0x05, 0xd5, 0xd7, 0x4d, 0xb7, 0x7e, 0xc2, 0xad, 0x27, 0x17, 0x32, 0x28, 0x7e, + 0x02, 0x49, 0x3f, 0xd1, 0xaa, 0x4f, 0xcc, 0xa6, 0x39, 0x2b, 0xe6, 0x72, 0xd4, 0xfc, 0x02, 0xe6, + 0xce, 0xbe, 0x6f, 0x7a, 0x2f, 0x94, 0x45, 0xf9, 0xb4, 0xf8, 0xbf, 0x3c, 0x28, 0xbd, 0xcd, 0xf2, + 0xc7, 0xa3, 0xdc, 0x4d, 0x88, 0x4b, 0x80, 0x15, 0x2a, 0x5b, 0x35, 0x2b, 0x6c, 0x6b, 0x7e, 0x0a, + 0xd1, 0x9b, 0xaa, 0x5c, 0xc6, 0xf6, 0x73, 0xbe, 0x29, 0x24, 0xa4, 0x03, 0x22, 0x91, 0x8c, 0x6e, + 0x09, 0xf9, 0x39, 0x24, 0xd5, 0x70, 0x0a, 0x05, 0x34, 0x0d, 0x68, 0xb8, 0x50, 0x8e, 0x7d, 0x7e, + 0x04, 0x31, 0x5a, 0xab, 0x6d, 0x30, 0x3e, 0x08, 0x71, 0x06, 0xf0, 0xac, 0xf5, 0x47, 0x67, 0xbc, + 0x8d, 0x3f, 0xf2, 0x10, 0xd7, 0x90, 0x0e, 0x53, 0xe3, 0xe6, 0x5d, 0x42, 0xec, 0x57, 0x42, 0x7b, + 0xb7, 0xdc, 0x44, 0xaf, 0x93, 0xae, 0x5e, 0xcf, 0xfc, 0xdf, 0x5d, 0x7d, 0x07, 0x00, 0x00, 0xff, + 0xff, 0x1f, 0xee, 0x62, 0x3e, 0xcf, 0x01, 0x00, 0x00, } diff --git a/ud/udMessages.proto b/ud/udMessages.proto index 0fdc127500d548390c5a8385b445b0ab31130f90..c8f993c8cb08d5fbe40b9c9a782e5ddbc8e4da65 100644 --- a/ud/udMessages.proto +++ b/ud/udMessages.proto @@ -23,7 +23,8 @@ message HashFact { message Contact { bytes userID = 1; bytes pubKey = 2; - repeated HashFact trigFacts = 3; + string username = 3; + repeated HashFact trigFacts = 4; } // Message sent to UDB to search for users @@ -48,4 +49,4 @@ message LookupSend { message LookupResponse { bytes pubKey = 1; string error = 3; -} \ No newline at end of file +}