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
+}