diff --git a/Makefile b/Makefile
index 2e56e3530bb3ef5d03352961f8eef095b4a47c01..bd6383ff69c75244eaf1b62ef3cc9c0fb91c5154 100644
--- a/Makefile
+++ b/Makefile
@@ -22,8 +22,8 @@ build:
 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/elixxir/crypto@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
 
diff --git a/api/authenticatedChannel.go b/api/authenticatedChannel.go
index 0af89ff00d7f87f5590d55f4b2dfcd2788d013d0..88682af983c107345b6415648bec9558f2eb4d6b 100644
--- a/api/authenticatedChannel.go
+++ b/api/authenticatedChannel.go
@@ -20,16 +20,17 @@ import (
 // RequestAuthenticatedChannel sends a request to another party to establish an
 // authenticated channel
 // It will not run if the network status is not healthy
-// An error will be returned if a channel already exists, if a request was
-// already received, or if a request was already sent
+// 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
 // will be called
+// Can be retried.
 func (c *Client) RequestAuthenticatedChannel(recipient, me contact.Contact,
-	message string) error {
+	message string) (id.Round, error) {
 	jww.INFO.Printf("RequestAuthenticatedChannel(%s)", recipient.ID)
 
 	if !c.network.GetHealthTracker().IsHealthy() {
-		return errors.New("Cannot request authenticated channel " +
+		return 0, errors.New("Cannot request authenticated channel " +
 			"creation when the network is not healthy")
 	}
 
@@ -60,11 +61,12 @@ func (c *Client) GetAuthenticatedChannelRequest(partner *id.ID) (contact.Contact
 // 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
-func (c *Client) ConfirmAuthenticatedChannel(recipient contact.Contact) error {
+// Can be retried.
+func (c *Client) ConfirmAuthenticatedChannel(recipient contact.Contact) (id.Round, error) {
 	jww.INFO.Printf("ConfirmAuthenticatedChannel(%s)", recipient.ID)
 
 	if !c.network.GetHealthTracker().IsHealthy() {
-		return errors.New("Cannot request authenticated channel " +
+		return 0, errors.New("Cannot request authenticated channel " +
 			"creation when the network is not healthy")
 	}
 
diff --git a/api/client.go b/api/client.go
index eb12060227a9ae9cbcd8eef9d48ccbcb143cbebf..d3cb829b84b0f87b2743d259cfe884973cb070d4 100644
--- a/api/client.go
+++ b/api/client.go
@@ -84,33 +84,12 @@ func NewClient(ndfJSON, storageDir string, password []byte, registrationCode str
 
 	protoUser := createNewUser(rngStream, cmixGrp, e2eGrp)
 
-	// Get current client version
-	currentVersion, err := version.ParseVersion(SEMVER)
-	if err != nil {
-		return errors.WithMessage(err, "Could not parse version string.")
-	}
-
-	// Create Storage
-	passwordStr := string(password)
-	storageSess, err := storage.New(storageDir, passwordStr, protoUser,
-		currentVersion, cmixGrp, e2eGrp, rngStreamGen)
+	err = checkVersionAndSetupStorage(def, storageDir, password, protoUser,
+		cmixGrp, e2eGrp, rngStreamGen, false, registrationCode)
 	if err != nil {
 		return err
 	}
 
-	// Save NDF to be used in the future
-	storageSess.SetBaseNDF(def)
-
-	//store the registration code for later use
-	storageSess.SetRegCode(registrationCode)
-
-	//move the registration state to keys generated
-	err = storageSess.ForwardRegistrationStatus(storage.KeyGenComplete)
-	if err != nil {
-		return errors.WithMessage(err, "Failed to denote state "+
-			"change in session")
-	}
-
 	//TODO: close the session
 	return nil
 }
@@ -135,29 +114,39 @@ func NewPrecannedClient(precannedID uint, defJSON, storageDir string, password [
 
 	protoUser := createPrecannedUser(precannedID, rngStream, cmixGrp, e2eGrp)
 
-	// Get current client version
-	currentVersion, err := version.ParseVersion(SEMVER)
+	err = checkVersionAndSetupStorage(def, storageDir, password, protoUser,
+		cmixGrp, e2eGrp, rngStreamGen, true, "")
 	if err != nil {
-		return errors.WithMessage(err, "Could not parse version string.")
+		return err
 	}
+	//TODO: close the session
+	return nil
+}
 
-	// Create Storage
-	passwordStr := string(password)
-	storageSess, err := storage.New(storageDir, passwordStr, protoUser,
-		currentVersion, cmixGrp, e2eGrp, rngStreamGen)
+// NewVanityClient creates a user with a receptionID that starts with the supplied prefix
+// It creates client storage, generates keys, connects, and registers
+// with the network. Note that this does not register a username/identity, but
+// merely creates a new cryptographic identity for adding such information
+// at a later date.
+func NewVanityClient(ndfJSON, storageDir string, password []byte, registrationCode string, userIdPrefix string) error {
+	jww.INFO.Printf("NewVanityClient()")
+	// 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)
 
-	// Save NDF to be used in the future
-	storageSess.SetBaseNDF(def)
+	protoUser := createNewVanityUser(rngStream, cmixGrp, e2eGrp, userIdPrefix)
 
-	//move the registration state to indicate registered with permissioning
-	err = storageSess.ForwardRegistrationStatus(
-		storage.PermissioningComplete)
+	err = checkVersionAndSetupStorage(def, storageDir, password, protoUser,
+		cmixGrp, e2eGrp, rngStreamGen, false, registrationCode)
 	if err != nil {
-		return errors.WithMessage(err, "Failed to denote state "+
-			"change in session")
+		return err
 	}
 
 	//TODO: close the session
@@ -199,7 +188,7 @@ func OpenClient(storageDir string, password []byte, parameters params.Network) (
 	return c, nil
 }
 
-// Login initalizes a client object from existing storage.
+// Login initializes a client object from existing storage.
 func Login(storageDir string, password []byte, parameters params.Network) (*Client, error) {
 	jww.INFO.Printf("Login()")
 
@@ -216,7 +205,7 @@ func Login(storageDir string, password []byte, parameters params.Network) (*Clie
 	//Attach the services interface
 	c.services = newServiceProcessiesList(c.runner)
 
-	//initilize comms
+	// initialize comms
 	err = c.initComms()
 	if err != nil {
 		return nil, err
@@ -233,7 +222,7 @@ func Login(storageDir string, password []byte, parameters params.Network) (*Clie
 		}
 	} else {
 		jww.WARN.Printf("Registration with permissioning skipped due to " +
-			"blank permissionign address. Client will not be able to register " +
+			"blank permissioning address. Client will not be able to register " +
 			"or track network.")
 	}
 
@@ -244,13 +233,7 @@ func Login(storageDir string, password []byte, parameters params.Network) (*Clie
 		return nil, err
 	}
 
-	//update gateway connections
-	err = c.network.GetInstance().UpdateGatewayConnections()
-	if err != nil {
-		return nil, err
-	}
-
-	//initilize the auth tracker
+	// initialize the auth tracker
 	c.auth = auth.NewManager(c.switchboard, c.storage, c.network)
 
 	return c, nil
@@ -307,13 +290,7 @@ func LoginWithNewBaseNDF_UNSAFE(storageDir string, password []byte,
 		return nil, err
 	}
 
-	//update gateway connections
-	err = c.network.GetInstance().UpdateGatewayConnections()
-	if err != nil {
-		return nil, err
-	}
-
-	//initilize the auth tracker
+	// initialize the auth tracker
 	c.auth = auth.NewManager(c.switchboard, c.storage, c.network)
 
 	return c, nil
@@ -354,7 +331,7 @@ func (c *Client) initPermissioning(def *ndf.NetworkDefinition) error {
 			jww.ERROR.Printf("Client has failed registration: %s", err)
 			return errors.WithMessage(err, "failed to load client")
 		}
-		jww.INFO.Printf("Client sucsecfully registered with the network")
+		jww.INFO.Printf("Client successfully registered with the network")
 	}
 	return nil
 }
@@ -458,7 +435,7 @@ func (c *Client) StopNetworkFollower(timeout time.Duration) error {
 	return nil
 }
 
-// Gets the state of the network follower. Returns:
+// NetworkFollowerStatus Gets the state of the network follower. Returns:
 // Stopped 	- 0
 // Starting - 1000
 // Running	- 2000
@@ -583,3 +560,43 @@ func decodeGroups(ndf *ndf.NetworkDefinition) (cmixGrp, e2eGrp *cyclic.Group) {
 
 	return cmixGrp, e2eGrp
 }
+
+// checkVersionAndSetupStorage is common code shared by NewClient, NewPrecannedClient and NewVanityClient
+// it checks client version and creates a new storage for user data
+func checkVersionAndSetupStorage(def *ndf.NetworkDefinition, storageDir string, password []byte,
+	protoUser user.User, cmixGrp, e2eGrp *cyclic.Group, rngStreamGen *fastRNG.StreamGenerator,
+	isPrecanned bool, registrationCode string) error {
+	// Get current client version
+	currentVersion, err := version.ParseVersion(SEMVER)
+	if err != nil {
+		return errors.WithMessage(err, "Could not parse version string.")
+	}
+
+	// Create Storage
+	passwordStr := string(password)
+	storageSess, err := storage.New(storageDir, passwordStr, protoUser,
+		currentVersion, cmixGrp, e2eGrp, rngStreamGen)
+	if err != nil {
+		return err
+	}
+
+	// Save NDF to be used in the future
+	storageSess.SetBaseNDF(def)
+
+	if !isPrecanned {
+		//store the registration code for later use
+		storageSess.SetRegCode(registrationCode)
+		//move the registration state to keys generated
+		err = storageSess.ForwardRegistrationStatus(storage.KeyGenComplete)
+	} else {
+		//move the registration state to indicate registered with permissioning
+		err = storageSess.ForwardRegistrationStatus(storage.PermissioningComplete)
+	}
+
+	if err != nil {
+		return errors.WithMessage(err, "Failed to denote state "+
+			"change in session")
+	}
+
+	return nil
+}
diff --git a/api/results.go b/api/results.go
index 885458fe2974d22b16592ba9e3f3ce2f5d4154a2..73d1193ccb9f0d3cb5667d6aa5a9170a2db78a5c 100644
--- a/api/results.go
+++ b/api/results.go
@@ -11,7 +11,6 @@ import (
 	"time"
 
 	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/client/network/gateway"
 	pb "gitlab.com/elixxir/comms/mixmessages"
 	"gitlab.com/elixxir/comms/network"
 	ds "gitlab.com/elixxir/comms/network/dataStructures"
@@ -67,6 +66,8 @@ type historicalRoundsComm interface {
 func (c *Client) GetRoundResults(roundList []id.Round, timeout time.Duration,
 	roundCallback RoundEventCallback) error {
 
+	jww.INFO.Printf("GetRoundResults(%v, %s)", roundList, timeout)
+
 	sendResults := make(chan ds.EventReturn, len(roundList))
 
 	return c.getRoundResults(roundList, timeout, roundCallback,
@@ -91,6 +92,8 @@ func (c *Client) getRoundResults(roundList []id.Round, timeout time.Duration,
 	allRoundsSucceeded := true
 	numResults := 0
 
+	oldestRound := networkInstance.GetOldestRoundID()
+
 	// Parse and adjudicate every round
 	for _, rnd := range roundList {
 		// Every round is timed out by default, until proven to have finished
@@ -111,9 +114,7 @@ func (c *Client) getRoundResults(roundList []id.Round, timeout time.Duration,
 				numResults++
 			}
 		} else {
-			jww.DEBUG.Printf("Failed to ger round [%d] in buffer: %v", rnd, err)
 			// Update oldest round (buffer may have updated externally)
-			oldestRound := networkInstance.GetOldestRoundID()
 			if rnd < oldestRound {
 				// If round is older that oldest round in our buffer
 				// Add it to the historical round request (performed later)
@@ -151,10 +152,12 @@ func (c *Client) getRoundResults(roundList []id.Round, timeout time.Duration,
 				roundCallback(false, true, roundsResults)
 				return
 			case roundReport := <-sendResults:
+
 				numResults--
+
 				// Skip if the round is nil (unknown from historical rounds)
 				// they default to timed out, so correct behavior is preserved
-				if roundReport.RoundInfo == nil || roundReport.TimedOut {
+				if  roundReport.RoundInfo == nil || roundReport.TimedOut {
 					allRoundsSucceeded = false
 				} else {
 					// If available, denote the result
@@ -177,33 +180,36 @@ 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) {
+	instance *network.Instance, sendResults chan ds.EventReturn, comms historicalRoundsComm) {
 
 	var resp *pb.HistoricalRoundsResponse
 
-	for {
+	//retry 5 times
+	for i := 0; i < 5; i++ {
 		// Find a gateway to request about the roundRequests
-		gwHost, err := gateway.Get(instance.GetPartialNdf().Get(), comms, c.rng.GetStream())
-		if err != nil {
-			jww.FATAL.Panicf("Failed to track network, NDF has corrupt "+
-				"data: %s", err)
-		}
+		result, err := c.GetNetworkInterface().GetSender().SendToAny(func(host *connect.Host) (interface{}, error) {
+			return comms.RequestHistoricalRounds(host, msg)
+		})
 
 		// If an error, retry with (potentially) a different gw host.
 		// If no error from received gateway request, exit loop
 		// and process rounds
-		resp, err = comms.RequestHistoricalRounds(gwHost, msg)
 		if err == nil {
+			resp = result.(*pb.HistoricalRoundsResponse)
 			break
+		} else {
+			jww.ERROR.Printf("Failed to lookup historical rounds: %s", err)
 		}
 	}
 
+	if resp == nil{
+		return
+	}
+
 	// Process historical rounds, sending back to the caller thread
 	for _, ri := range resp.Rounds {
 		sendResults <- ds.EventReturn{
-			ri,
-			false,
+			RoundInfo: ri,
 		}
 	}
 }
diff --git a/api/user.go b/api/user.go
index 892d84c63c8fb0e29001774c6c645f351b3d00b8..48d7f992dfa32b48a3905752c55459ddc710e701 100644
--- a/api/user.go
+++ b/api/user.go
@@ -17,6 +17,10 @@ import (
 	"gitlab.com/xx_network/crypto/xx"
 	"gitlab.com/xx_network/primitives/id"
 	"math/rand"
+	"regexp"
+	"runtime"
+	"strings"
+	"sync"
 )
 
 const (
@@ -134,3 +138,126 @@ func createPrecannedUser(precannedID uint, rng csprng.Source, cmix, e2e *cyclic.
 		ReceptionRSA:     rsaKey,
 	}
 }
+
+// createNewVanityUser generates an identity for cMix
+// 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
+	// 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())
+	}
+
+	// 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())
+	}
+
+	// RSA Keygen (4096 bit defaults)
+	transmissionRsaKey, err := rsa.GenerateKey(rng, rsa.DefaultRSABitLen)
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+
+	// Salt, UID, etc gen
+	transmissionSalt := make([]byte, SaltSize)
+	n, err := csprng.NewSystemRNG().Read(transmissionSalt)
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+	if n != SaltSize {
+		jww.FATAL.Panicf("transmissionSalt size too small: %d", n)
+	}
+	transmissionID, err := xx.NewID(transmissionRsaKey.GetPublic(), transmissionSalt, id.User)
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+
+	receptionRsaKey, err := rsa.GenerateKey(rng, rsa.DefaultRSABitLen)
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+
+	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{}
+	cores := runtime.NumCPU()
+
+	var receptionSalt []byte
+	var receptionID *id.ID 
+	
+	pref := prefix
+	ignoreCase := false
+	// check if case-insensitivity is enabled
+	if strings.HasPrefix(prefix, "(?i)") {
+		pref = strings.ToLower(pref[4:])
+		ignoreCase = true
+	}
+	// Check if prefix contains valid Base64 characters
+	match, _ := regexp.MatchString("^[A-Za-z0-9+/]+$", pref)
+	if match == false {
+		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++{
+		wg.Add(1)
+		go func() {
+			rSalt := make([]byte, SaltSize)
+			for {	
+				select {
+				case <- done:
+					defer wg.Done()
+					return
+				default:
+					n, err = csprng.NewSystemRNG().Read(rSalt)
+					if err != nil {
+						jww.FATAL.Panicf(err.Error())
+					}
+					if n != SaltSize {
+						jww.FATAL.Panicf("receptionSalt size too small: %d", n)
+					}
+					rID, err := xx.NewID(receptionRsaKey.GetPublic(), rSalt, id.User)
+					if err != nil {
+						jww.FATAL.Panicf(err.Error())
+					}
+					id := rID.String()
+					if ignoreCase {
+						id = strings.ToLower(id)
+					} 
+					if strings.HasPrefix(id, pref) {
+						mu.Lock()
+						receptionID = rID
+						receptionSalt = rSalt
+						mu.Unlock()
+						found <- true
+						defer wg.Done()
+						return
+					}
+				}	
+			}
+		}()
+	}
+	// wait for a solution then close the done channel to signal the workers to exit
+	<- found
+	close(done)
+	wg.Wait()
+	return user.User{
+		TransmissionID:   transmissionID.DeepCopy(),
+		TransmissionSalt: transmissionSalt,
+		TransmissionRSA:  transmissionRsaKey,
+		ReceptionID:      receptionID.DeepCopy(),
+		ReceptionSalt:    receptionSalt,
+		ReceptionRSA:     receptionRsaKey,
+		Precanned:        false,
+		CmixDhPrivateKey: cmix.NewIntFromBytes(cMixKeyBytes),
+		E2eDhPrivateKey:  e2e.NewIntFromBytes(e2eKeyBytes),
+	}
+}
diff --git a/api/utilsInterfaces_test.go b/api/utilsInterfaces_test.go
index ffa5154795045f361afd70d8cb67d0c45eb6e4c4..62ab16f5231d0bdc81ab30f8e0a1b890fb0931c7 100644
--- a/api/utilsInterfaces_test.go
+++ b/api/utilsInterfaces_test.go
@@ -10,6 +10,7 @@ import (
 	"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"
 	pb "gitlab.com/elixxir/comms/mixmessages"
 	"gitlab.com/elixxir/comms/network"
@@ -79,6 +80,7 @@ func (ht *historicalRounds) GetHost(hostId *id.ID) (*connect.Host, bool) {
 // Contains a test implementation of the networkManager interface.
 type testNetworkManagerGeneric struct {
 	instance *network.Instance
+	sender   *gateway.Sender
 }
 
 /* Below methods built for interface adherence */
@@ -119,3 +121,7 @@ func (t *testNetworkManagerGeneric) GetStoppable() stoppable.Stoppable {
 func (t *testNetworkManagerGeneric) InProgressRegistrations() int {
 	return 0
 }
+
+func (t *testNetworkManagerGeneric) GetSender() *gateway.Sender {
+	return t.sender
+}
diff --git a/api/utils_test.go b/api/utils_test.go
index ab2079ba39782e558cde5ff78ca2cd7f678fa217..a20f1e0bf64a0da34935d467ac483caccd742dce 100644
--- a/api/utils_test.go
+++ b/api/utils_test.go
@@ -8,6 +8,7 @@
 package api
 
 import (
+	"gitlab.com/elixxir/client/network/gateway"
 	"testing"
 
 	"github.com/pkg/errors"
@@ -64,7 +65,10 @@ func newTestingClient(face interface{}) (*Client, error) {
 		return nil, err
 	}
 
-	c.network = &testNetworkManagerGeneric{instance: thisInstance}
+	p := gateway.DefaultPoolParams()
+	p.MaxPoolSize = 1
+	sender, _ := gateway.NewSender(p, c.rng, def, commsManager, c.storage, nil)
+	c.network = &testNetworkManagerGeneric{instance: thisInstance, sender: sender}
 
 	return c, nil
 }
diff --git a/api/version_vars.go b/api/version_vars.go
index 1a4ed3b5336c44eeed1cf474d589c1175808c16a..fa844ec2db29fa3a5d5899abb0adbfaa043a1a03 100644
--- a/api/version_vars.go
+++ b/api/version_vars.go
@@ -1,10 +1,10 @@
 // Code generated by go generate; DO NOT EDIT.
 // This file was generated by robots at
-// 2021-04-02 08:56:27.8657223 -0700 PDT m=+0.054483401
+// 2021-04-29 12:54:02.223688 -0500 CDT m=+0.028208749
 package api
 
-const GITVERSION = `6020ab79 removed an extranious print`
-const SEMVER = "2.3.0"
+const GITVERSION = `9c8c08ea prevent updating with an empty ndf`
+const SEMVER = "2.4.0"
 const DEPENDENCIES = `module gitlab.com/elixxir/client
 
 go 1.13
@@ -13,28 +13,26 @@ require (
 	github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
 	github.com/golang/protobuf v1.4.3
 	github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
-	github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea
-	github.com/magiconair/properties v1.8.4 // indirect
-	github.com/mitchellh/mapstructure v1.4.0 // indirect
-	github.com/pelletier/go-toml v1.8.1 // indirect
+	github.com/magiconair/properties v1.8.5 // indirect
+	github.com/mitchellh/mapstructure v1.4.1 // indirect
+	github.com/pelletier/go-toml v1.9.0 // indirect
 	github.com/pkg/errors v0.9.1
-	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/smartystreets/assertions v1.0.1 // indirect
-	github.com/spf13/afero v1.5.1 // indirect
 	github.com/spf13/cast v1.3.1 // indirect
-	github.com/spf13/cobra v1.1.1
+	github.com/spf13/cobra v1.1.3
 	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.20210401210158-6053ad2e224c
-	gitlab.com/elixxir/crypto v0.0.7-0.20210401210040-b7f1da24ef13
-	gitlab.com/elixxir/ekv v0.1.4
-	gitlab.com/elixxir/primitives v0.0.3-0.20210401175645-9b7b92f74ec4
-	gitlab.com/xx_network/comms v0.0.4-0.20210401160731-7b8890cdd8ad
-	gitlab.com/xx_network/crypto v0.0.5-0.20210401160648-4f06cace9123
-	gitlab.com/xx_network/primitives v0.0.4-0.20210331161816-ed23858bdb93
-	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
-	golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
+	gitlab.com/elixxir/comms v0.0.4-0.20210427005410-7ae183abda2a
+	gitlab.com/elixxir/crypto v0.0.7-0.20210427005255-4fe1bcf69c5a
+	gitlab.com/elixxir/ekv v0.1.5
+	gitlab.com/elixxir/primitives v0.0.3-0.20210427004615-c68ecf15fcf3
+	gitlab.com/xx_network/comms v0.0.4-0.20210426213447-82674e09e402
+	gitlab.com/xx_network/crypto v0.0.5-0.20210420170153-2a6276844076
+	gitlab.com/xx_network/primitives v0.0.4-0.20210402222416-37c1c4d3fac4
+	golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
+	golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 // indirect
+	golang.org/x/text v0.3.6 // indirect
 	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
diff --git a/auth/callback.go b/auth/callback.go
index ef21512f88a06c19322c03518bc262d7d2e94ebe..2c39824cb8631b222d6140fd66d2e9f143873639 100644
--- a/auth/callback.go
+++ b/auth/callback.go
@@ -213,7 +213,7 @@ func (m *Manager) handleConfirm(cmixMsg format.Message, sr *auth.SentRequest,
 	if mgr, err := m.storage.E2e().GetPartner(sr.GetPartner()); mgr != nil || err == nil {
 		jww.WARN.Printf("Cannot confirm auth for %s, channel already "+
 			"exists.", sr.GetPartner())
-		m.storage.Auth().Fail(sr.GetPartner())
+		m.storage.Auth().Done(sr.GetPartner())
 		return
 	}
 
@@ -221,7 +221,7 @@ func (m *Manager) handleConfirm(cmixMsg format.Message, sr *auth.SentRequest,
 	baseFmt, partnerPubKey, err := handleBaseFormat(cmixMsg, grp)
 	if err != nil {
 		jww.WARN.Printf("Failed to handle auth confirm: %s", err)
-		m.storage.Auth().Fail(sr.GetPartner())
+		m.storage.Auth().Done(sr.GetPartner())
 		return
 	}
 
@@ -236,7 +236,7 @@ func (m *Manager) handleConfirm(cmixMsg format.Message, sr *auth.SentRequest,
 	if !success {
 		jww.WARN.Printf("Recieved auth confirmation failed its mac " +
 			"check")
-		m.storage.Auth().Fail(sr.GetPartner())
+		m.storage.Auth().Done(sr.GetPartner())
 		return
 	}
 
@@ -244,7 +244,7 @@ func (m *Manager) handleConfirm(cmixMsg format.Message, sr *auth.SentRequest,
 	if err != nil {
 		jww.WARN.Printf("Failed to unmarshal auth confirmation's "+
 			"encrypted payload: %s", err)
-		m.storage.Auth().Fail(sr.GetPartner())
+		m.storage.Auth().Done(sr.GetPartner())
 		return
 	}
 
@@ -252,7 +252,7 @@ func (m *Manager) handleConfirm(cmixMsg format.Message, sr *auth.SentRequest,
 	if err := m.doConfirm(sr, grp, partnerPubKey, sr.GetMyPrivKey(),
 		sr.GetPartnerHistoricalPubKey(), ecrFmt.GetOwnership()); err != nil {
 		jww.WARN.Printf("Confirmation failed: %s", err)
-		m.storage.Auth().Fail(sr.GetPartner())
+		m.storage.Auth().Done(sr.GetPartner())
 		return
 	}
 }
diff --git a/auth/confirm.go b/auth/confirm.go
index fee1433a2c22b7323cb5c1ce9d1ea57c6108c9a4..40c983c6feb73446495bb9307a2c6793fca7b259 100644
--- a/auth/confirm.go
+++ b/auth/confirm.go
@@ -12,26 +12,23 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/interfaces"
 	"gitlab.com/elixxir/client/interfaces/params"
-	"gitlab.com/elixxir/client/interfaces/utility"
 	"gitlab.com/elixxir/client/storage"
-	ds "gitlab.com/elixxir/comms/network/dataStructures"
+	"gitlab.com/xx_network/primitives/id"
 	"gitlab.com/elixxir/crypto/contact"
 	"gitlab.com/elixxir/crypto/diffieHellman"
 	cAuth "gitlab.com/elixxir/crypto/e2e/auth"
 	"gitlab.com/elixxir/primitives/format"
-	"gitlab.com/elixxir/primitives/states"
 	"io"
-	"time"
 )
 
 func ConfirmRequestAuth(partner contact.Contact, rng io.Reader,
-	storage *storage.Session, net interfaces.NetworkManager) error {
+	storage *storage.Session, net interfaces.NetworkManager) (id.Round, error) {
 
 	/*edge checking*/
 
 	// check that messages can be sent over the network
 	if !net.GetHealthTracker().IsHealthy() {
-		return errors.New("Cannot confirm authenticated message " +
+		return 0, errors.New("Cannot confirm authenticated message " +
 			"when the network is not healthy")
 	}
 
@@ -40,14 +37,15 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader,
 	// the lock
 	storedContact, err := storage.Auth().GetReceivedRequest(partner.ID)
 	if err != nil {
-		return errors.Errorf("failed to find a pending Auth Request: %s",
+		return 0, errors.Errorf("failed to find a pending Auth Request: %s",
 			err)
 	}
+	defer storage.Auth().Done(partner.ID)
 
 	// verify the passed contact matches what is stored
 	if storedContact.DhPubKey.Cmp(partner.DhPubKey) != 0 {
-		storage.Auth().Fail(partner.ID)
-		return errors.WithMessage(err, "Pending Auth Request has different "+
+		storage.Auth().Done(partner.ID)
+		return 0, errors.WithMessage(err, "Pending Auth Request has different "+
 			"pubkey than stored")
 	}
 
@@ -67,8 +65,7 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader,
 	salt := make([]byte, saltSize)
 	_, err = rng.Read(salt)
 	if err != nil {
-		storage.Auth().Fail(partner.ID)
-		return errors.Wrap(err, "Failed to generate salt for "+
+		return 0, errors.Wrap(err, "Failed to generate salt for "+
 			"confirmation")
 	}
 
@@ -107,22 +104,19 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader,
 	p := storage.E2e().GetE2ESessionParams()
 	if err := storage.E2e().AddPartner(partner.ID, partner.DhPubKey, newPrivKey,
 		p, p); err != nil {
-		storage.Auth().Fail(partner.ID)
-		return errors.Errorf("Failed to create channel with partner (%s) "+
-			"on confirmation: %+v",
-			partner.ID, err)
+		jww.WARN.Printf("Failed to create channel with partner (%s) "+
+			"on confirmation, this is likley a replay: %s",
+			partner.ID, err.Error())
 	}
 
 	// delete the in progress negotiation
 	// this unlocks the request lock
-	if err := storage.Auth().Delete(partner.ID); err != nil {
-		return errors.Errorf("UNRECOVERABLE! Failed to delete in "+
+	//fixme - do these deletes at a later date
+	/*if err := storage.Auth().Delete(partner.ID); err != nil {
+		return 0, errors.Errorf("UNRECOVERABLE! Failed to delete in "+
 			"progress negotiation with partner (%s) after creating confirmation: %+v",
 			partner.ID, err)
-	}
-
-	//store the message as a critical message so it will always be sent
-	storage.GetCriticalRawMessages().AddProcessing(cmixMsg, partner.ID)
+	}*/
 
 	jww.INFO.Printf("Confirming Auth with %s, msgDigest: %s",
 		partner.ID, cmixMsg.Digest())
@@ -134,39 +128,11 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader,
 		// retried
 		jww.INFO.Printf("Auth Confirm with %s (msgDigest: %s) failed "+
 			"to transmit: %+v", partner.ID, cmixMsg.Digest(), err)
-		storage.GetCriticalRawMessages().Failed(cmixMsg, partner.ID)
-		return errors.WithMessage(err, "Auth Confirm Failed to transmit")
+		return 0, errors.WithMessage(err, "Auth Confirm Failed to transmit")
 	}
 
 	jww.INFO.Printf("Confirm Request with %s (msgDigest: %s) sent on round %d",
 		partner.ID, cmixMsg.Digest(), round)
 
-	/*check message delivery*/
-	sendResults := make(chan ds.EventReturn, 1)
-	roundEvents := net.GetInstance().GetRoundEvents()
-
-	roundEvents.AddRoundEventChan(round, sendResults, 1*time.Minute,
-		states.COMPLETED, states.FAILED)
-
-	success, numFailed, _ := utility.TrackResults(sendResults, 1)
-	if !success {
-		if numFailed > 0 {
-			jww.INFO.Printf("Auth Confirm with %s (msgDigest: %s) failed "+
-				"delivery due to round failure, will retry on reconnect",
-				partner.ID, cmixMsg.Digest())
-		} else {
-			jww.INFO.Printf("Auth Confirm with %s (msgDigest: %s) failed "+
-				"delivery due to timeout, will retry on reconnect",
-				partner.ID, cmixMsg.Digest())
-		}
-		jww.ERROR.Printf("auth confirm failed to transmit, will be " +
-			"handled on reconnect")
-		storage.GetCriticalRawMessages().Failed(cmixMsg, partner.ID)
-	} else {
-		jww.INFO.Printf("Auth Confirm with %s (msgDigest: %s) delivered "+
-			"sucesfully", partner.ID, cmixMsg.Digest())
-		storage.GetCriticalRawMessages().Succeeded(cmixMsg, partner.ID)
-	}
-
-	return nil
+	return round, nil
 }
diff --git a/auth/request.go b/auth/request.go
index 7c963123b49dd532734871c687277a318887093d..c43707904f835b19d1e64ba53cb4ec9fe550355f 100644
--- a/auth/request.go
+++ b/auth/request.go
@@ -12,59 +12,59 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/interfaces"
 	"gitlab.com/elixxir/client/interfaces/params"
-	"gitlab.com/elixxir/client/interfaces/utility"
 	"gitlab.com/elixxir/client/storage"
 	"gitlab.com/elixxir/client/storage/auth"
 	"gitlab.com/elixxir/client/storage/e2e"
-	ds "gitlab.com/elixxir/comms/network/dataStructures"
 	"gitlab.com/elixxir/crypto/contact"
+	"gitlab.com/elixxir/crypto/cyclic"
 	"gitlab.com/elixxir/crypto/diffieHellman"
 	cAuth "gitlab.com/elixxir/crypto/e2e/auth"
 	"gitlab.com/elixxir/primitives/format"
-	"gitlab.com/elixxir/primitives/states"
+	"gitlab.com/xx_network/primitives/id"
 	"io"
 	"strings"
-	"time"
 )
 
 const terminator = ";"
 
 func RequestAuth(partner, me contact.Contact, message string, rng io.Reader,
-	storage *storage.Session, net interfaces.NetworkManager) error {
+	storage *storage.Session, net interfaces.NetworkManager) (id.Round, error) {
 	/*edge checks generation*/
 
 	// check that an authenticated channel does not already exists
 	if _, err := storage.E2e().GetPartner(partner.ID); err == nil ||
 		!strings.Contains(err.Error(), e2e.NoPartnerErrorStr) {
-		return errors.Errorf("Authenticated channel already " +
+		return 0, errors.Errorf("Authenticated channel already " +
 			"established with partner")
 	}
 
 	// check that the request is being sent from the proper ID
 	if !me.ID.Cmp(storage.GetUser().ReceptionID) {
-		return errors.Errorf("Authenticated channel request " +
+		return 0, errors.Errorf("Authenticated channel request " +
 			"can only be sent from user's identity")
 	}
 
 	// check that the message is properly formed
 	if strings.Contains(message, terminator) {
-		return errors.Errorf("Message cannot contain '%s'", terminator)
+		return 0, errors.Errorf("Message cannot contain '%s'", terminator)
 	}
 
+	//denote if this is a resend of an old request
+	resend := false
+
 	//lookup if an ongoing request is occurring
-	rqType, _, _, err := storage.Auth().GetRequest(partner.ID)
-	if err != nil && strings.Contains(err.Error(), auth.NoRequest) {
-		err = nil
-	}
-	if err != nil {
+	rqType, sr, _, err := storage.Auth().GetRequest(partner.ID)
+
+	if err != nil && !strings.Contains(err.Error(), auth.NoRequest){
 		if rqType == auth.Receive {
-			return errors.WithMessage(err,
-				"Cannot send a request after "+
-					"receiving a request")
+			return 0, errors.WithMessage(err,
+				"Cannot send a request after receiving a request")
 		} else if rqType == auth.Sent {
-			return errors.WithMessage(err,
-				"Cannot send a request after "+
-					"already sending one")
+			resend = true
+		}else{
+			return 0, errors.WithMessage(err,
+				"Cannot send a request after receiving unknown error " +
+				"on requesting contact status")
 		}
 	}
 
@@ -76,7 +76,7 @@ func RequestAuth(partner, me contact.Contact, message string, rng io.Reader,
 	ecrFmt := newEcrFormat(baseFmt.GetEcrPayloadLen())
 	requestFmt, err := newRequestFormat(ecrFmt)
 	if err != nil {
-		return errors.Errorf("failed to make request format: %+v", err)
+		return 0, errors.Errorf("failed to make request format: %+v", err)
 	}
 
 	//check the payload fits
@@ -85,7 +85,7 @@ func RequestAuth(partner, me contact.Contact, message string, rng io.Reader,
 	msgPayloadBytes := []byte(msgPayload)
 
 	if len(msgPayloadBytes) > requestFmt.MsgPayloadLen() {
-		return errors.Errorf("Combined message longer than space "+
+		return 0, errors.Errorf("Combined message longer than space "+
 			"available in payload; available: %v, length: %v",
 			requestFmt.MsgPayloadLen(), len(msgPayloadBytes))
 	}
@@ -95,17 +95,27 @@ func RequestAuth(partner, me contact.Contact, message string, rng io.Reader,
 	salt := make([]byte, saltSize)
 	_, err = rng.Read(salt)
 	if err != nil {
-		return errors.Wrap(err, "Failed to generate salt")
+		return 0, errors.Wrap(err, "Failed to generate salt")
+	}
+
+	var newPrivKey, newPubKey *cyclic.Int
+
+	// in this case we have an ongoing request so we can resend the extant
+	// request
+	if resend{
+		newPrivKey = sr.GetMyPrivKey()
+		newPubKey = sr.GetMyPubKey()
+	//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)
 	}
 
 	//generate ownership proof
 	ownership := cAuth.MakeOwnershipProof(storage.E2e().GetDHPrivateKey(),
 		partner.DhPubKey, storage.E2e().GetGroup())
 
-	//generate new keypair
-	newPrivKey := diffieHellman.GeneratePrivateKey(256, grp, rng)
-	newPubKey := diffieHellman.GeneratePublicKey(newPrivKey, grp)
-
 	jww.TRACE.Printf("RequestAuth MYPUBKEY: %v", newPubKey.Bytes())
 	jww.TRACE.Printf("RequestAuth THEIRPUBKEY: %v", partner.DhPubKey.Bytes())
 
@@ -130,67 +140,30 @@ func RequestAuth(partner, me contact.Contact, message string, rng io.Reader,
 	/*store state*/
 	//fixme: channel is bricked if the first store succedes but the second fails
 	//store the in progress auth
-	err = storage.Auth().AddSent(partner.ID, partner.DhPubKey, newPrivKey,
-		newPrivKey, confirmFp)
-	if err != nil {
-		return errors.Errorf("Failed to store auth request: %s", err)
+	if !resend{
+		err = storage.Auth().AddSent(partner.ID, partner.DhPubKey, newPrivKey,
+			newPrivKey, confirmFp)
+		if err != nil {
+			return 0, errors.Errorf("Failed to store auth request: %s", err)
+		}
 	}
 
-	//store the message as a critical message so it will always be sent
-	storage.GetCriticalRawMessages().AddProcessing(cmixMsg, partner.ID)
+	jww.INFO.Printf("Requesting Auth with %s, msgDigest: %s",
+		partner.ID, cmixMsg.Digest())
 
-	go func() {
-		jww.INFO.Printf("Requesting Auth with %s, msgDigest: %s",
-			partner.ID, cmixMsg.Digest())
-
-		/*send message*/
-		round, _, err := net.SendCMIX(cmixMsg, partner.ID,
-			params.GetDefaultCMIX())
-		if err != nil {
-			// if the send fails just set it to failed, it will
-			// but automatically retried
-			jww.WARN.Printf("Auth Request with %s (msgDigest: %s)"+
-				" failed to transmit: %+v", partner.ID,
-				cmixMsg.Digest(), err)
-			storage.GetCriticalRawMessages().Failed(cmixMsg,
-				partner.ID)
-		}
+	/*send message*/
+	round, _, err := net.SendCMIX(cmixMsg, partner.ID,
+		params.GetDefaultCMIX())
+	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 " +
+			"(msgDigest: %s) failed to transmit: %+v", partner.ID,
+			cmixMsg.Digest(), err)
+	}
 
-		jww.INFO.Printf("Auth Request with %s (msgDigest: %s) sent"+
-			" on round %d", partner.ID, cmixMsg.Digest(), round)
-
-		/*check message delivery*/
-		sendResults := make(chan ds.EventReturn, 1)
-		roundEvents := net.GetInstance().GetRoundEvents()
-
-		roundEvents.AddRoundEventChan(round, sendResults, 1*time.Minute,
-			states.COMPLETED, states.FAILED)
-
-		success, numFailed, _ := utility.TrackResults(sendResults, 1)
-		if !success {
-			if numFailed > 0 {
-				jww.WARN.Printf("Auth Request with %s "+
-					"(msgDigest: %s) failed "+
-					"delivery due to round failure, "+
-					"will retry on reconnect",
-					partner.ID, cmixMsg.Digest())
-			} else {
-				jww.WARN.Printf("Auth Request with %s "+
-					"(msgDigest: %s) failed "+
-					"delivery due to timeout, "+
-					"will retry on reconnect",
-					partner.ID, cmixMsg.Digest())
-			}
-			storage.GetCriticalRawMessages().Failed(cmixMsg,
-				partner.ID)
-		} else {
-			jww.INFO.Printf("Auth Request with %s (msgDigest: %s) "+
-				"delivered sucessfully", partner.ID,
-				cmixMsg.Digest())
-			storage.GetCriticalRawMessages().Succeeded(cmixMsg,
-				partner.ID)
-		}
-	}()
+	jww.INFO.Printf("Auth Request with %s (msgDigest: %s) sent"+
+		" on round %d", partner.ID, cmixMsg.Digest(), round)
 
-	return nil
+	return round, nil
 }
diff --git a/bindings/authenticatedChannels.go b/bindings/authenticatedChannels.go
index 28784c87f944bd3d15e43b873194f968dd513971..30b12f0a1b3039fba5b18eafeff874cc7a438a60 100644
--- a/bindings/authenticatedChannels.go
+++ b/bindings/authenticatedChannels.go
@@ -27,19 +27,20 @@ func (c *Client) MakePrecannedAuthenticatedChannel(precannedID int) (*Contact, e
 // authenticated channel
 // It will not run if the network status is not healthy
 // An error will be returned if a channel already exists, if a request was
-// already received, or if a request was already sent
+// already received.
 // When a confirmation occurs, the channel will be created and the callback
 // will be called
+// This can be called many times and retried.
 //
 // This function takes the marshaled send report to ensure a memory leak does
 // not occur as a result of both sides of the bindings holding a refrence to
 // the same pointer.
 func (c *Client) RequestAuthenticatedChannel(recipientMarshaled,
-	meMarshaled []byte, message string) error {
+	meMarshaled []byte, message string) (int, error) {
 	recipent, err := contact.Unmarshal(recipientMarshaled)
 
 	if err != nil {
-		return errors.New(fmt.Sprintf("Failed to "+
+		return 0, errors.New(fmt.Sprintf("Failed to "+
 			"RequestAuthenticatedChannel: Failed to Unmarshal Recipent: "+
 			"%+v", err))
 	}
@@ -47,11 +48,13 @@ func (c *Client) RequestAuthenticatedChannel(recipientMarshaled,
 	me, err := contact.Unmarshal(meMarshaled)
 
 	if err != nil {
-		return errors.New(fmt.Sprintf("Failed to "+
+		return 0, errors.New(fmt.Sprintf("Failed to "+
 			"RequestAuthenticatedChannel: Failed to Unmarshal Me: %+v", err))
 	}
 
-	return c.api.RequestAuthenticatedChannel(recipent, me, message)
+	rid, err := c.api.RequestAuthenticatedChannel(recipent, me, message)
+
+	return int(rid), err
 }
 
 // RegisterAuthCallbacks registers both callbacks for authenticated channels.
@@ -79,19 +82,26 @@ func (c *Client) RegisterAuthCallbacks(request AuthRequestCallback,
 // 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
-// An error will be returned if a channel already exists, if a request doest
+// An error will be returned if a request doest
 // exist, or if the passed in contact does not exactly match the received
-// request
-func (c *Client) ConfirmAuthenticatedChannel(recipientMarshaled []byte) error {
+// request.
+// This can be called many times and retried.
+//
+// This function takes the marshaled send report to ensure a memory leak does
+// not occur as a result of both sides of the bindings holding a refrence to
+// the same pointer.
+func (c *Client) ConfirmAuthenticatedChannel(recipientMarshaled []byte) (int, error) {
 	recipent, err := contact.Unmarshal(recipientMarshaled)
 
 	if err != nil {
-		return errors.New(fmt.Sprintf("Failed to "+
+		return 0, errors.New(fmt.Sprintf("Failed to "+
 			"ConfirmAuthenticatedChannel: Failed to Unmarshal Recipient: "+
 			"%+v", err))
 	}
 
-	return c.api.ConfirmAuthenticatedChannel(recipent)
+	rid, err := c.api.ConfirmAuthenticatedChannel(recipent)
+
+	return int(rid), err
 }
 
 // VerifyOwnership checks if the ownership proof on a passed contact matches the
diff --git a/bindings/callback.go b/bindings/callback.go
index d0fe898c204188646c74c647f41759b5988cbf69..a6526d24d3ba961db2b79bdc2fbd6a1b950821dc 100644
--- a/bindings/callback.go
+++ b/bindings/callback.go
@@ -31,12 +31,19 @@ type NetworkHealthCallback interface {
 	Callback(bool)
 }
 
-// RoundEventHandler handles round events happening on the cMix network.
+// RoundEventCallback handles waiting on the exact state of a round on
+// the cMix network.
 type RoundEventCallback interface {
 	EventCallback(rid, state int, timedOut bool)
 }
 
-// RoundEventHandler handles round events happening on the cMix network.
+// RoundCompletionCallback is returned when the completion of a round is known.
+type RoundCompletionCallback interface {
+	EventCallback(rid int, success, timedOut bool)
+}
+
+// MessageDeliveryCallback gets called on the determination if all events
+// related to a message send were successful.
 type MessageDeliveryCallback interface {
 	EventCallback(msgID []byte, delivered, timedOut bool, roundResults []byte)
 }
diff --git a/bindings/client.go b/bindings/client.go
index 34fb521076fca455294824ef3104f1c67445669d..d5c40e456de1b263a48184250cbb67fecc9e0c88 100644
--- a/bindings/client.go
+++ b/bindings/client.go
@@ -8,7 +8,6 @@
 package bindings
 
 import (
-	"encoding/json"
 	"errors"
 	"fmt"
 	jww "github.com/spf13/jwalterweatherman"
@@ -157,11 +156,7 @@ func UnmarshalContact(b []byte) (*Contact, error) {
 //Unmarshals a marshaled send report object, returns an error if it fails
 func UnmarshalSendReport(b []byte) (*SendReport, error) {
 	sr := &SendReport{}
-	if err := json.Unmarshal(b, sr); err != nil {
-		return nil, errors.New(fmt.Sprintf("Failed to Unmarshal "+
-			"Send Report: %+v", err))
-	}
-	return sr, nil
+	return sr, sr.Unmarshal(b)
 }
 
 // StartNetworkFollower kicks off the tracking of the network. It starts
@@ -333,29 +328,55 @@ func (c *Client) RegisterRoundEventsHandler(rid int, cb RoundEventCallback,
 	return newRoundUnregister(roundID, ec, c.api.GetRoundEvents())
 }
 
-// RegisterMessageDeliveryCB allows the caller to get notified if the rounds a
-// message was sent in successfully completed. Under the hood, this uses the same
-// interface as RegisterRoundEventsHandler, but provides a convenient way to use
-// the interface in its most common form, looking up the result of message
-// retrieval
+// WaitForRoundCompletion allows the caller to get notified if a round
+// has completed (or failed). Under the hood, this uses an API which uses the internal
+// round data, network historical round lookup, and waiting on network events
+// to determine what has (or will) occur.
+//
+// The callbacks will return at timeoutMS if no state update occurs
+func (c *Client) WaitForRoundCompletion(roundID int,
+	rec RoundCompletionCallback, timeoutMS int) error {
+
+	f := func(allRoundsSucceeded, timedOut bool, rounds map[id.Round]api.RoundResult) {
+		rec.EventCallback(roundID, allRoundsSucceeded, timedOut)
+	}
+
+	timeout := time.Duration(timeoutMS) * time.Millisecond
+
+	return c.api.GetRoundResults([]id.Round{id.Round(roundID)}, timeout, f)
+}
+
+// WaitForMessageDelivery allows the caller to get notified if the rounds a
+// message was sent in successfully completed. Under the hood, this uses an API
+// which uses the internal round data, network historical round lookup, and
+// waiting on network events to determine what has (or will) occur.
 //
 // The callbacks will return at timeoutMS if no state update occurs
 //
 // This function takes the marshaled send report to ensure a memory leak does
 // not occur as a result of both sides of the bindings holding a reference to
 // the same pointer.
-func (c *Client) WaitForRoundCompletion(marshaledSendReport []byte,
+func (c *Client) WaitForMessageDelivery(marshaledSendReport []byte,
 	mdc MessageDeliveryCallback, timeoutMS int) error {
-
+	jww.INFO.Printf("WaitForMessageDelivery(%v, _, %v)",
+		marshaledSendReport, timeoutMS)
 	sr, err := UnmarshalSendReport(marshaledSendReport)
 	if err != nil {
 		return errors.New(fmt.Sprintf("Failed to "+
-			"WaitForRoundCompletion callback due to bad Send Report: %+v", err))
+			"WaitForMessageDelivery callback due to bad Send Report: %+v", err))
+	}
+
+	if sr==nil || sr.rl == nil || len(sr.rl.list) == 0{
+		return errors.New(fmt.Sprintf("Failed to "+
+			"WaitForMessageDelivery callback due to invalid Send Report " +
+			"unmarshal: %s", string(marshaledSendReport)))
 	}
 
 	f := func(allRoundsSucceeded, timedOut bool, rounds map[id.Round]api.RoundResult) {
 		results := make([]byte, len(sr.rl.list))
-
+		jww.INFO.Printf("Processing WaitForMessageDelivery report " +
+			"for %v, success: %v, timedout: %v", sr.mid, allRoundsSucceeded,
+			timedOut)
 		for i, r := range sr.rl.list {
 			if result, exists := rounds[r]; exists {
 				results[i] = byte(result)
@@ -367,7 +388,9 @@ func (c *Client) WaitForRoundCompletion(marshaledSendReport []byte,
 
 	timeout := time.Duration(timeoutMS) * time.Millisecond
 
-	return c.api.GetRoundResults(sr.rl.list, timeout, f)
+	err = c.api.GetRoundResults(sr.rl.list, timeout, f)
+
+	return err
 }
 
 // Returns a user object from which all information about the current user
diff --git a/bindings/message.go b/bindings/message.go
index 978a62743dbd823062f1c4a961e20d74f386c496..6cead4ad09a8be5fc49433bf9e68e5cf0ab87a9f 100644
--- a/bindings/message.go
+++ b/bindings/message.go
@@ -7,7 +7,9 @@
 
 package bindings
 
-import "gitlab.com/elixxir/client/interfaces/message"
+import (
+	"gitlab.com/elixxir/client/interfaces/message"
+)
 
 // Message is a message received from the cMix network in the clear
 // or that has been decrypted using established E2E keys.
@@ -36,10 +38,12 @@ func (m *Message) GetMessageType() int {
 }
 
 // Returns the message's timestamp in ms
-func (m *Message) GetTimestampMS() int {
-	return int(m.r.Timestamp.Unix())
+func (m *Message) GetTimestampMS() int64 {
+	ts := m.r.Timestamp.UnixNano()
+	ts = (ts+999999)/1000000
+	return ts
 }
 
-func (m *Message) GetTimestampNano() int {
-	return int(m.r.Timestamp.UnixNano())
+func (m *Message) GetTimestampNano() int64 {
+	return m.r.Timestamp.UnixNano()
 }
diff --git a/bindings/send.go b/bindings/send.go
index a7e35873447758c22cb9ddfc8eddeaf86fe1bb73..673fad0003ac009b707de67be8f3edc9804f639f 100644
--- a/bindings/send.go
+++ b/bindings/send.go
@@ -135,6 +135,11 @@ type SendReport struct {
 	mid e2e.MessageID
 }
 
+type SendReportDisk struct{
+	List []id.Round
+	Mid []byte
+}
+
 func (sr *SendReport) GetRoundList() *RoundList {
 	return sr.rl
 }
@@ -144,5 +149,22 @@ func (sr *SendReport) GetMessageID() []byte {
 }
 
 func (sr *SendReport) Marshal() ([]byte, error) {
-	return json.Marshal(sr)
+	srd := SendReportDisk{
+		List: sr.rl.list,
+		Mid:  sr.mid[:],
+	}
+	return json.Marshal(&srd)
+}
+
+func (sr *SendReport) Unmarshal(b []byte) error {
+	srd := SendReportDisk{
+	}
+	if err := json.Unmarshal(b, &srd); err!=nil{
+		return errors.New(fmt.Sprintf("Failed to unmarshal send " +
+			"report: %s", err.Error()))
+	}
+
+	copy(sr.mid[:],srd.Mid)
+	sr.rl = &RoundList{list:srd.List}
+	return nil
 }
diff --git a/cmd/getndf.go b/cmd/getndf.go
index 914412093c8f1247953473c29a02edcd68346235..7ac150020c911d695f646993e3a409c18d6cb8da 100644
--- a/cmd/getndf.go
+++ b/cmd/getndf.go
@@ -18,11 +18,13 @@ import (
 	// "gitlab.com/elixxir/client/switchboard"
 	// "gitlab.com/elixxir/client/ud"
 	// "gitlab.com/elixxir/primitives/fact"
+	"gitlab.com/elixxir/client/api"
 	"gitlab.com/elixxir/comms/client"
 	"gitlab.com/xx_network/comms/connect"
 	//"time"
 	pb "gitlab.com/elixxir/comms/mixmessages"
 	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
 	"gitlab.com/xx_network/primitives/utils"
 )
 
@@ -63,12 +65,14 @@ var getNDFCmd = &cobra.Command{
 		if gwHost != "" {
 			host, _ := connect.NewHost(&id.TempGateway, gwHost,
 				cert, params)
+			dummyID := ephemeral.ReservedIDs[0]
 			pollMsg := &pb.GatewayPoll{
 				Partial: &pb.NDFHash{
 					Hash: nil,
 				},
 				LastUpdate:  uint64(0),
-				ReceptionID: id.DummyUser.Marshal(),
+				ReceptionID: dummyID[:],
+				ClientVersion: []byte(api.SEMVER),
 			}
 			resp, err := comms.SendPoll(host, pollMsg)
 			if err != nil {
diff --git a/cmd/init.go b/cmd/init.go
index cf19521f066d1e9e878524a7ee75fd3471cf6b7c..b00f215475bfba0c513752b1d2fd01fd13cea5c8 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -11,6 +11,7 @@ package cmd
 import (
 	"fmt"
 	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
 	jww "github.com/spf13/jwalterweatherman"
 )
 
@@ -29,5 +30,9 @@ 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.")
+	_ = viper.BindPFlag("userid-prefix", initCmd.Flags().Lookup("userid-prefix"))
+
 	rootCmd.AddCommand(initCmd)
 }
diff --git a/cmd/root.go b/cmd/root.go
index 1c7c2166ef55eac800f384c35fdffe710c288aed..fb14badf09039bbfd31e08c341e7352a5fb2d68d 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -78,7 +78,7 @@ var rootCmd = &cobra.Command{
 		jww.INFO.Printf("Message ListenerID: %v", listenerID)
 
 		// Set up auth request handler, which simply prints the
-		// user id of the requestor.
+		// user id of the requester.
 		authMgr := client.GetAuthRegistrar()
 		authMgr.AddGeneralRequestCallback(printChanRequest)
 
@@ -95,7 +95,7 @@ var rootCmd = &cobra.Command{
 				requestor contact.Contact, message string) {
 				jww.INFO.Printf("Channel Request: %s",
 					requestor.ID)
-				err := client.ConfirmAuthenticatedChannel(
+				_, err := client.ConfirmAuthenticatedChannel(
 					requestor)
 				if err != nil {
 					jww.FATAL.Panicf("%+v", err)
@@ -163,7 +163,7 @@ var rootCmd = &cobra.Command{
 		}
 
 		if !unsafe && !authConfirmed {
-			jww.INFO.Printf("Waiting for authentication channel "+
+			jww.INFO.Printf("Waiting for authentication channel"+
 				" confirmation with partner %s", recipientID)
 			scnt := uint(0)
 			waitSecs := viper.GetUint("auth-timeout")
@@ -306,7 +306,7 @@ func createClient() *api.Client {
 	storeDir := viper.GetString("session")
 	regCode := viper.GetString("regcode")
 	precannedID := viper.GetUint("sendid")
-
+	userIDprefix := viper.GetString("userid-prefix")
 	//create a new client if none exist
 	if _, err := os.Stat(storeDir); os.IsNotExist(err) {
 		// Load NDF
@@ -320,8 +320,14 @@ func createClient() *api.Client {
 			err = api.NewPrecannedClient(precannedID,
 				string(ndfJSON), storeDir, []byte(pass))
 		} else {
-			err = api.NewClient(string(ndfJSON), storeDir,
+			if userIDprefix != "" {
+				err = api.NewVanityClient(string(ndfJSON), storeDir,
+				[]byte(pass), regCode, userIDprefix)
+			} else {
+				err = api.NewClient(string(ndfJSON), storeDir,
 				[]byte(pass), regCode)
+			}
+			
 		}
 
 		if err != nil {
@@ -401,7 +407,7 @@ func acceptChannel(client *api.Client, recipientID *id.ID) {
 	if err != nil {
 		jww.FATAL.Panicf("%+v", err)
 	}
-	err = client.ConfirmAuthenticatedChannel(
+	_, err = client.ConfirmAuthenticatedChannel(
 		recipientContact)
 	if err != nil {
 		jww.FATAL.Panicf("%+v", err)
@@ -463,7 +469,7 @@ func addAuthenticatedChannel(client *api.Client, recipientID *id.ID,
 		me := client.GetUser().GetContact()
 		jww.INFO.Printf("Requesting auth channel from: %s",
 			recipientID)
-		err := client.RequestAuthenticatedChannel(recipientContact,
+		_, err := client.RequestAuthenticatedChannel(recipientContact,
 			me, msg)
 		if err != nil {
 			jww.FATAL.Panicf("%+v", err)
diff --git a/cmd/single.go b/cmd/single.go
index 7fe8e2038ef34240bd2ee363e6f38d8d94d26ed9..15f803b11bec0f775845fdfdfb9f8292c75ea2a7 100644
--- a/cmd/single.go
+++ b/cmd/single.go
@@ -55,7 +55,7 @@ var singleCmd = &cobra.Command{
 			authMgr.AddGeneralRequestCallback(func(
 				requester contact.Contact, message string) {
 				jww.INFO.Printf("Got request: %s", requester.ID)
-				err := client.ConfirmAuthenticatedChannel(requester)
+				_, err := client.ConfirmAuthenticatedChannel(requester)
 				if err != nil {
 					jww.FATAL.Panicf("%+v", err)
 				}
diff --git a/cmd/ud.go b/cmd/ud.go
index ccefe891a4e87c2053ec62fd95e835115aa21a45..38cbecb2cd1c6572cd140b158f435fe4291500cd 100644
--- a/cmd/ud.go
+++ b/cmd/ud.go
@@ -55,7 +55,7 @@ var udCmd = &cobra.Command{
 			authMgr.AddGeneralRequestCallback(func(
 				requester contact.Contact, message string) {
 				jww.INFO.Printf("Got Request: %s", requester.ID)
-				err := client.ConfirmAuthenticatedChannel(requester)
+				_, err := client.ConfirmAuthenticatedChannel(requester)
 				if err != nil {
 					jww.FATAL.Panicf("%+v", err)
 				}
diff --git a/cmd/version.go b/cmd/version.go
index 2a7eca16517f8e7f3d5c547ea35dbc472d4607c5..b781e502cb97afc8edd8c90300c8a9f1e5816969 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.3.0"
+const currentVersion = "2.4.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 47671e792ba3599c48275a617a133c3a9005fd28..20c85a77446311ad7223fb314e0e7bbfa0aa5108 100644
--- a/go.mod
+++ b/go.mod
@@ -6,28 +6,26 @@ require (
 	github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
 	github.com/golang/protobuf v1.4.3
 	github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
-	github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea // indirect
-	github.com/magiconair/properties v1.8.4 // indirect
-	github.com/mitchellh/mapstructure v1.4.0 // indirect
-	github.com/pelletier/go-toml v1.8.1 // indirect
+	github.com/magiconair/properties v1.8.5 // indirect
+	github.com/mitchellh/mapstructure v1.4.1 // indirect
+	github.com/pelletier/go-toml v1.9.0 // indirect
 	github.com/pkg/errors v0.9.1
-	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
 	github.com/smartystreets/assertions v1.0.1 // indirect
-	github.com/spf13/afero v1.5.1 // indirect
 	github.com/spf13/cast v1.3.1 // indirect
-	github.com/spf13/cobra v1.1.1
+	github.com/spf13/cobra v1.1.3
 	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.20210414225623-fb1a8a82f210
-	gitlab.com/elixxir/crypto v0.0.7-0.20210412231025-6f75c577f803
-	gitlab.com/elixxir/ekv v0.1.4
-	gitlab.com/elixxir/primitives v0.0.3-0.20210409190923-7bf3cd8d97e7
-	gitlab.com/xx_network/comms v0.0.4-0.20210414225551-37262e764468
-	gitlab.com/xx_network/crypto v0.0.5-0.20210413200952-56bd15ec9d99
-	gitlab.com/xx_network/primitives v0.0.4-0.20210412170941-7ef69bce5a5c
-	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
-	golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
+	gitlab.com/elixxir/comms v0.0.4-0.20210429182303-0edbda5e1b2c
+	gitlab.com/elixxir/crypto v0.0.7-0.20210429182057-898c75770293
+	gitlab.com/elixxir/ekv v0.1.5
+	gitlab.com/elixxir/primitives v0.0.3-0.20210429180244-cdbb97da0c16
+	gitlab.com/xx_network/comms v0.0.4-0.20210426213447-82674e09e402
+	gitlab.com/xx_network/crypto v0.0.5-0.20210420170153-2a6276844076
+	gitlab.com/xx_network/primitives v0.0.4-0.20210402222416-37c1c4d3fac4
+	golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
+	golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 // indirect
+	golang.org/x/text v0.3.6 // indirect
 	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
diff --git a/go.sum b/go.sum
index 3e66ea636bbb55882fbbcca02e6b7f6f838bff2b..9c4b366bf5651eff852a775187540585485b0aca 100644
--- a/go.sum
+++ b/go.sum
@@ -10,8 +10,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=
-git.schwanenlied.me/yawning/aez.git v0.0.0-20180408160647-ec7426b44926/go.mod h1:sXNmB4ljkeRBT0xvmbMGP7ldvC2C2CIUXBlqIO8XGc4=
-git.schwanenlied.me/yawning/bsaes.git v0.0.0-20190320102049-26d1add596b6/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo=
 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=
@@ -21,7 +19,6 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/aws/aws-lambda-go v1.8.1/go.mod h1:zUsUQhAUjYzR8AuduJPCfhBuKWUaDbQiPOG+ouzmE1A=
 github.com/badoux/checkmail v1.2.1 h1:TzwYx5pnsV6anJweMx2auXdekBwGr/yt1GgalIx9nBQ=
 github.com/badoux/checkmail v1.2.1/go.mod h1:XroCOBU5zzZJcLvgwU15I+2xXyCdTWXyR9MGfRhBYy0=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -30,7 +27,6 @@ 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/cloudflare/circl v1.0.1-0.20210104183656-96a0695de3c3/go.mod h1:l2CvGr3DNS9Egif8pwQqJ45Ci9Y/PPs0XJHTcRKbGBQ=
 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=
@@ -46,12 +42,10 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 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/flynn/noise v0.0.0-20180327030543-2492fe189ae6/go.mod h1:1i71OnUq3iUe1ma7Lr6yG6/rjvM3emb6yoL7xLFzcVQ=
 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=
-github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -136,17 +130,9 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/katzenpost/chacha20 v0.0.0-20190907175341-fad9676fa4d8/go.mod h1:d9kxwmGOcutgP6bQwr2xaLInaW5yJsxsoPRyUIG0J/E=
-github.com/katzenpost/chacha20 v0.0.0-20190910113340-7ce890d6a556/go.mod h1:d9kxwmGOcutgP6bQwr2xaLInaW5yJsxsoPRyUIG0J/E=
-github.com/katzenpost/core v0.0.13/go.mod h1:RjyD4DLQr+P5FnP6iiZUTgvalZwT7NyawXwYzn32eYQ=
-github.com/katzenpost/core v0.0.14 h1:8n30pGlm6cXa1PjU+HBv8nARFh67UmX/eYkHnbcBfgs=
-github.com/katzenpost/core v0.0.14/go.mod h1:RjyD4DLQr+P5FnP6iiZUTgvalZwT7NyawXwYzn32eYQ=
-github.com/katzenpost/newhope v0.0.0-20190907181500-0c77ddcb510f/go.mod h1:xA8+73aJ0TU2W701F6Ac7LL8jiUuv6me5pO+/nIuaCM=
-github.com/katzenpost/noise v0.0.2/go.mod h1:L6ioEZo4vpnAgdh4x8qenV7T0/k8mltat1EjxQO0TNA=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -159,8 +145,8 @@ github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3Hn
 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=
+github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
+github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -175,8 +161,8 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
 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/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
+github.com/mitchellh/mapstructure v1.4.1/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=
@@ -187,13 +173,12 @@ github.com/nyaruka/phonenumbers v1.0.60/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJr
 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=
-github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
-github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
+github.com/pelletier/go-toml v1.9.0 h1:NOd0BRdOKpPf0SxkL3HxSQOG7rNh+4kl6PHcBPFs7Q0=
+github.com/pelletier/go-toml v1.9.0/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -223,14 +208,13 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg=
-github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
-github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
+github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
+github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
@@ -252,9 +236,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 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/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
-github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 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=
@@ -269,84 +250,44 @@ 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.20210402183711-0350710740e7 h1:JfR2UVZDqAIQeicHq7ZIPuJyPzkcr59OT5uGz4XyFiI=
-gitlab.com/elixxir/comms v0.0.4-0.20210402183711-0350710740e7/go.mod h1:3ikLStVfz4wUMgzL/WkDE9umkeizAKMlO5GQcnDmH5c=
-gitlab.com/elixxir/comms v0.0.4-0.20210402205438-eca60a717c47 h1:w0SarO3yraN81dZJan7lDz6OjexoqP52kRAdlnfCr4o=
-gitlab.com/elixxir/comms v0.0.4-0.20210402205438-eca60a717c47/go.mod h1:Hd9NbUwdRXFcs5ZgukPYsChmM8AqxJRjzMNvQuUXqh4=
-gitlab.com/elixxir/comms v0.0.4-0.20210402205854-0ea056917a98 h1:6GF+txzdbf5EKZKXcwfHagH7G+vmDmMjQZAoMSsHXm4=
-gitlab.com/elixxir/comms v0.0.4-0.20210402205854-0ea056917a98/go.mod h1:Hd9NbUwdRXFcs5ZgukPYsChmM8AqxJRjzMNvQuUXqh4=
-gitlab.com/elixxir/comms v0.0.4-0.20210402222700-7fac5f85c596 h1:jX6H4vu//StDLKZn5lcmxp9S3IhzS/8Uttg0K5r7Iqo=
-gitlab.com/elixxir/comms v0.0.4-0.20210402222700-7fac5f85c596/go.mod h1:jqqUYnsftpfQXJ57BPYp5A+i7qfA5IXhKUE9ZOSrqaE=
-gitlab.com/elixxir/comms v0.0.4-0.20210405170210-0aa75aa7fefd h1:0wkkIeKyZ9SW+0MpxpZnypTOpePze8/r4USw1hooAJs=
-gitlab.com/elixxir/comms v0.0.4-0.20210405170210-0aa75aa7fefd/go.mod h1:jqqUYnsftpfQXJ57BPYp5A+i7qfA5IXhKUE9ZOSrqaE=
-gitlab.com/elixxir/comms v0.0.4-0.20210405183148-930ea17a1b5f h1:ZUY46FcnA7BOd9pCggproSRBqycV0X+zKyai3eTL2ys=
-gitlab.com/elixxir/comms v0.0.4-0.20210405183148-930ea17a1b5f/go.mod h1:jqqUYnsftpfQXJ57BPYp5A+i7qfA5IXhKUE9ZOSrqaE=
-gitlab.com/elixxir/comms v0.0.4-0.20210413194022-f5422be88efb h1:mPwoG9nAhihK4Rfl6Hph/DzMmkePv4zJ4nzTC+ewnEc=
-gitlab.com/elixxir/comms v0.0.4-0.20210413194022-f5422be88efb/go.mod h1:2meX6DKpVJAUcgt1d727t1I28C8qsZNSn9Vm8dlkgmA=
-gitlab.com/elixxir/comms v0.0.4-0.20210414195240-0a29afe5c282 h1:sv1Yiv18o8Q/tpBSP3Bpm2GrbQYdWcGaXELIU6OGxVc=
-gitlab.com/elixxir/comms v0.0.4-0.20210414195240-0a29afe5c282/go.mod h1:K0sExgT6vTjMY/agP7hRSutveJzGSNOuFjSsAl2vmz8=
-gitlab.com/elixxir/comms v0.0.4-0.20210414200820-10e888270d4d h1:bHhOyckCTs0mRdgasEWgyxSz3JgK1J0ZBaV5cp2IjLk=
-gitlab.com/elixxir/comms v0.0.4-0.20210414200820-10e888270d4d/go.mod h1:K0sExgT6vTjMY/agP7hRSutveJzGSNOuFjSsAl2vmz8=
-gitlab.com/elixxir/comms v0.0.4-0.20210414225623-fb1a8a82f210 h1:6r9esD2qZUXqll0I3KW50RQQiuN66jpDFAeqRPy1ZVs=
-gitlab.com/elixxir/comms v0.0.4-0.20210414225623-fb1a8a82f210/go.mod h1:k93pPqBAdnHvoRlIpj6KGqx6HpJGbKMUI+v+PuBcCsM=
+gitlab.com/elixxir/comms v0.0.4-0.20210427005410-7ae183abda2a h1:u3bGNs0IRcFw+OtRNBR87UCRWHLA57TkSF80HdReg4U=
+gitlab.com/elixxir/comms v0.0.4-0.20210427005410-7ae183abda2a/go.mod h1:r9zz08vO5MjzAsfA6yd978PIkm0dVYAo4EooXc/ES1M=
+gitlab.com/elixxir/comms v0.0.4-0.20210429182303-0edbda5e1b2c h1:+6hshgCT43Z4f+nNQUeIZGdIYhch5dGgJLcMzyUPujo=
+gitlab.com/elixxir/comms v0.0.4-0.20210429182303-0edbda5e1b2c/go.mod h1:eRkJ3+6afFAqTyHneUN04ix4MIV16F52/GMHaNbGXZU=
 gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4 h1:28ftZDeYEko7xptCZzeFWS1Iam95dj46TWFVVlKmw6A=
 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.6 h1:c94CGzBTV7LgInGHfmeJHrqq9nIc/WEOLUd9OeQBN74=
-gitlab.com/elixxir/crypto v0.0.6/go.mod h1:V8lricBRpa8v1ySymXQ1/lsb+8/lSak5S7ZWRT6OACY=
-gitlab.com/elixxir/crypto v0.0.7-0.20210401210040-b7f1da24ef13 h1:x6oSLhgzhBcXeItHQ7OlDNoyvebgyNdGCaywAP/IkMc=
-gitlab.com/elixxir/crypto v0.0.7-0.20210401210040-b7f1da24ef13/go.mod h1:5k+LGynIQa42jZ/UbNVwBiZGHKvLXbXXkqyuTY6uHs0=
-gitlab.com/elixxir/crypto v0.0.7-0.20210412231025-6f75c577f803 h1:8sLODlAYRT0Y9NA+uoMoF1qBrBRrW5TikyKAOvyCd+E=
-gitlab.com/elixxir/crypto v0.0.7-0.20210412231025-6f75c577f803/go.mod h1:HMMRBuv/yMqB5c31G9OPlOAifOOqGypCyD5v6py+4vo=
-gitlab.com/elixxir/ekv v0.1.4 h1:NLVMwsFEKArWcsDHu2DbXlm9374iSgn7oIA3rVSsvjc=
-gitlab.com/elixxir/ekv v0.1.4/go.mod h1:e6WPUt97taFZe5PFLPb1Dupk7tqmDCTQu1kkstqJvw4=
+gitlab.com/elixxir/crypto v0.0.7-0.20210427005255-4fe1bcf69c5a h1:DX+PiQ/koxI/uNNqAxUiFJXP+nbl5qN1u3lBElpauzQ=
+gitlab.com/elixxir/crypto v0.0.7-0.20210427005255-4fe1bcf69c5a/go.mod h1:XuDhkhjI/h1w8OQW8neyP9hvt2KMH/GSCjRQTWg2AEk=
+gitlab.com/elixxir/crypto v0.0.7-0.20210429182057-898c75770293 h1:7f7rYmDI1HvSgT0+1xhSb7rM5zxQjG005byDtr4BqX0=
+gitlab.com/elixxir/crypto v0.0.7-0.20210429182057-898c75770293/go.mod h1:e2tQyVZR4Tbj4ashHIDF2k55Gj2MT+J6qhZxrszPA74=
+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.20201116174806-97f190989704/go.mod h1:3fxFHSlQhkV4vs+S0dZEz3Om3m+40WX8L806yvSnNFc=
-gitlab.com/elixxir/primitives v0.0.3-0.20210401175645-9b7b92f74ec4 h1:PFrOIpax1IMXS7jVGFhOF3bSOWh3IWhNUD18n1DzSZM=
-gitlab.com/elixxir/primitives v0.0.3-0.20210401175645-9b7b92f74ec4/go.mod h1:9qqDucNbLP9ArL1VKCXQuqYrcAbJIUcI8uzbP7NmKDw=
-gitlab.com/elixxir/primitives v0.0.3-0.20210409190923-7bf3cd8d97e7 h1:q3cw7WVtD6hDqTi8ydky+yiqJ4RkWp/hkTSNirr9Z6Y=
-gitlab.com/elixxir/primitives v0.0.3-0.20210409190923-7bf3cd8d97e7/go.mod h1:h0QHrjrixLNaP24ZXAgDOZXP4eegrQ24BCZPGitg8Jg=
+gitlab.com/elixxir/primitives v0.0.3-0.20210427004615-c68ecf15fcf3 h1:fQZzwSDzFymbXlTPVCDTrnBkOhFAx4fACwZQHpmTVOY=
+gitlab.com/elixxir/primitives v0.0.3-0.20210427004615-c68ecf15fcf3/go.mod h1:h0QHrjrixLNaP24ZXAgDOZXP4eegrQ24BCZPGitg8Jg=
+gitlab.com/elixxir/primitives v0.0.3-0.20210429180244-cdbb97da0c16 h1:ZIXWpEbSWUrVvqtrmo5/MVvDJm94eX5j2dgtGptkt6U=
+gitlab.com/elixxir/primitives v0.0.3-0.20210429180244-cdbb97da0c16/go.mod h1:h0QHrjrixLNaP24ZXAgDOZXP4eegrQ24BCZPGitg8Jg=
 gitlab.com/xx_network/comms v0.0.0-20200805174823-841427dd5023/go.mod h1:owEcxTRl7gsoM8c3RQ5KAm5GstxrJp5tn+6JfQ4z5Hw=
-gitlab.com/xx_network/comms v0.0.4-0.20210401160731-7b8890cdd8ad h1:0E4wnLoOqODo6K2SwVG18y63sns4WLN3x+nSM9SWfiM=
-gitlab.com/xx_network/comms v0.0.4-0.20210401160731-7b8890cdd8ad/go.mod h1:inre/Ot0UJkxcfF4Oy4jk2A1MXyicRkPZB9FfnCfKQY=
-gitlab.com/xx_network/comms v0.0.4-0.20210407173545-dafd47029306/go.mod h1:PXbUpBMUcDygpEV1ptEc/pych07YkYQ/tv0AQPw+BRk=
-gitlab.com/xx_network/comms v0.0.4-0.20210413200413-41a493c32b06 h1:ejoRHig0h5oEXLbEqQbE3wjcJtEoK0q7mHkfZjphtRo=
-gitlab.com/xx_network/comms v0.0.4-0.20210413200413-41a493c32b06/go.mod h1:PXbUpBMUcDygpEV1ptEc/pych07YkYQ/tv0AQPw+BRk=
-gitlab.com/xx_network/comms v0.0.4-0.20210414191603-0904bc6eeda2 h1:4Xhw5zO9ggeD66z5SajzRcgkQ0RNdAENETwPlawo5aU=
-gitlab.com/xx_network/comms v0.0.4-0.20210414191603-0904bc6eeda2/go.mod h1:PXbUpBMUcDygpEV1ptEc/pych07YkYQ/tv0AQPw+BRk=
-gitlab.com/xx_network/comms v0.0.4-0.20210414225551-37262e764468 h1:09cm7A2rSbNZQdYH+95zPj5vu3FyM1Vy2H1REU3YgpA=
-gitlab.com/xx_network/comms v0.0.4-0.20210414225551-37262e764468/go.mod h1:fUw26IhQ2MtjUnQVLO7TQ4tdr+g6qJPGrKfhXCb4R1E=
+gitlab.com/xx_network/comms v0.0.4-0.20210426213447-82674e09e402 h1:owWfI0mh31/nUV8Dh70z+inpFhU4GItlpsjsqYnWn7c=
+gitlab.com/xx_network/comms v0.0.4-0.20210426213447-82674e09e402/go.mod h1:PIgq/b4ucczEqCWAmPEnht/4QJw57+mPSICHiyMEstU=
 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.20201124194022-366c10b1bce0/go.mod h1:+lcQEy+Th4eswFgQDwT0EXKp4AXrlubxalwQFH5O0Mk=
-gitlab.com/xx_network/crypto v0.0.5-0.20210401160648-4f06cace9123 h1:i2PajAamYlacUpAFWqE7g5qtM6Vt/xG9iDfoK1nc2l4=
-gitlab.com/xx_network/crypto v0.0.5-0.20210401160648-4f06cace9123/go.mod h1:CWV349I9Nv1zPCIY/f8Ej6yWs7NG0HLTWnm+Jlz7jKc=
-gitlab.com/xx_network/crypto v0.0.5-0.20210405224157-2b1f387b42c1/go.mod h1:CUhRpioyLaKIylg+LIyZX1rhOmFaEXQQ6esNycx9dcA=
-gitlab.com/xx_network/crypto v0.0.5-0.20210413184628-dbc1dd97ed5e/go.mod h1:Rz97srKNbUOnMk+gKyRnJYIVIA6bGBj+BB5Of6Pfyng=
-gitlab.com/xx_network/crypto v0.0.5-0.20210413200952-56bd15ec9d99 h1:2WYJ+gkFAzlcdvmBzmNxx5ANNEf+p08EoT+Qeiptzw8=
-gitlab.com/xx_network/crypto v0.0.5-0.20210413200952-56bd15ec9d99/go.mod h1:Rz97srKNbUOnMk+gKyRnJYIVIA6bGBj+BB5Of6Pfyng=
+gitlab.com/xx_network/crypto v0.0.5-0.20210420170153-2a6276844076 h1:bLR43541fUUhPTMGuCWKz7IEQevAm4OeTBVtsDt8tgg=
+gitlab.com/xx_network/crypto v0.0.5-0.20210420170153-2a6276844076/go.mod h1:CUhRpioyLaKIylg+LIyZX1rhOmFaEXQQ6esNycx9dcA=
 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.20210331161816-ed23858bdb93 h1:ZV5ZfSBX7+7moL8pywpCB3HTNXd03tSwMgDA7SDYaFA=
-gitlab.com/xx_network/primitives v0.0.4-0.20210331161816-ed23858bdb93/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE=
-gitlab.com/xx_network/primitives v0.0.4-0.20210402183235-e04f174cf8c4 h1:uPTABEykN9moPQjb427tqVFPcxWbYbiqLZ26iwI8Cws=
-gitlab.com/xx_network/primitives v0.0.4-0.20210402183235-e04f174cf8c4/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE=
-gitlab.com/xx_network/primitives v0.0.4-0.20210402205313-e9b80f75e701 h1:svcqDo2heNLjmUncmgymwRH3lkV5Jy3PdhvLuqmI39o=
-gitlab.com/xx_network/primitives v0.0.4-0.20210402205313-e9b80f75e701/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE=
 gitlab.com/xx_network/primitives v0.0.4-0.20210402222416-37c1c4d3fac4 h1:YPYTKF0zQf08y0eQrjQP01C/EWQTypdqawjZPr5c6rc=
 gitlab.com/xx_network/primitives v0.0.4-0.20210402222416-37c1c4d3fac4/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE=
-gitlab.com/xx_network/primitives v0.0.4-0.20210406200245-e5a84aeae64d/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE=
-gitlab.com/xx_network/primitives v0.0.4-0.20210412170941-7ef69bce5a5c h1:WpAkvPhg5NYkLkJ8M0/jD4VMuNpqQTahlwkVbbV8SHk=
-gitlab.com/xx_network/primitives v0.0.4-0.20210412170941-7ef69bce5a5c/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=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -360,8 +301,6 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 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-20190829043050-9756ffdc2472/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=
@@ -371,9 +310,10 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt
 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-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 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=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -407,8 +347,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 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-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
-golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+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/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=
@@ -428,7 +368,6 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190902133755-9109b7679e13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 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=
@@ -439,9 +378,10 @@ golang.org/x/sys v0.0.0-20200917073148-efd3b9a0ff20/go.mod h1:h1NjWce9XRLGQEsW7w
 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-20201211090839-8ad439b19e0f/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-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs=
+golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/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=
@@ -451,6 +391,8 @@ 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=
@@ -520,13 +462,11 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 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=
diff --git a/interfaces/networkManager.go b/interfaces/networkManager.go
index da548c855e48a30a5fff0895842f04975e00c88b..1daa7d38104731870459b1323c8d918401a398fd 100644
--- a/interfaces/networkManager.go
+++ b/interfaces/networkManager.go
@@ -10,6 +10,7 @@ package interfaces
 import (
 	"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/comms/network"
 	"gitlab.com/elixxir/crypto/e2e"
@@ -24,6 +25,7 @@ type NetworkManager interface {
 	SendCMIX(message format.Message, recipient *id.ID, p params.CMIX) (id.Round, ephemeral.Id, error)
 	GetInstance() *network.Instance
 	GetHealthTracker() HealthTracker
+	GetSender() *gateway.Sender
 	Follow(report ClientErrorReport) (stoppable.Stoppable, error)
 	CheckGarbledMessages()
 	InProgressRegistrations() int
diff --git a/keyExchange/utils_test.go b/keyExchange/utils_test.go
index f02e056657b6d3e4fdfcd102cfc3093ddabb45e9..a1d9d83f5550c47876a312c236f031afdbdd65ad 100644
--- a/keyExchange/utils_test.go
+++ b/keyExchange/utils_test.go
@@ -13,6 +13,7 @@ import (
 	"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/e2e"
@@ -103,7 +104,11 @@ func (t *testNetworkManagerGeneric) InProgressRegistrations() int {
 	return 0
 }
 
-func InitTestingContextGeneric(i interface{}) (*storage.Session, interfaces.NetworkManager, error) {
+func (t *testNetworkManagerGeneric) GetSender() *gateway.Sender {
+	return nil
+}
+
+func InitTestingContextGeneric(i interface{}) (*storage.Session, interfaces.NetworkManager) {
 	switch i.(type) {
 	case *testing.T, *testing.M, *testing.B, *testing.PB:
 		break
@@ -120,12 +125,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, err
+		return nil, nil
 	}
 
 	thisManager := &testNetworkManagerGeneric{instance: thisInstance}
 
-	return thisSession, thisManager, nil
+	return thisSession, thisManager
 
 }
 
@@ -210,6 +215,10 @@ func (t *testNetworkManagerFullExchange) InProgressRegistrations() int {
 	return 0
 }
 
+func (t *testNetworkManagerFullExchange) GetSender() *gateway.Sender {
+	return nil
+}
+
 func InitTestingContextFullExchange(i interface{}) (*storage.Session, *switchboard.Switchboard, interfaces.NetworkManager) {
 	switch i.(type) {
 	case *testing.T, *testing.M, *testing.B, *testing.PB:
diff --git a/network/checkedRounds.go b/network/checkedRounds.go
index d104ac9589bfb9d7b7bce41d967b7013fdfb9141..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/network/checkedRounds.go
+++ b/network/checkedRounds.go
@@ -1,74 +0,0 @@
-package network
-
-import (
-	"container/list"
-	"crypto/md5"
-	"gitlab.com/elixxir/client/storage/reception"
-	"gitlab.com/xx_network/primitives/id"
-)
-
-type idFingerprint [16]byte
-
-type checkedRounds struct {
-	lookup map[idFingerprint]*checklist
-}
-
-type checklist struct {
-	m map[id.Round]interface{}
-	l *list.List
-}
-
-func newCheckedRounds() *checkedRounds {
-	return &checkedRounds{
-		lookup: make(map[idFingerprint]*checklist),
-	}
-}
-
-func (cr *checkedRounds) Check(identity reception.IdentityUse, rid id.Round) bool {
-	idFp := getIdFingerprint(identity)
-	cl, exists := cr.lookup[idFp]
-	if !exists {
-		cl = &checklist{
-			m: make(map[id.Round]interface{}),
-			l: list.New().Init(),
-		}
-		cr.lookup[idFp] = cl
-	}
-
-	if _, exists := cl.m[rid]; !exists {
-		cl.m[rid] = nil
-		cl.l.PushBack(rid)
-		return true
-	}
-	return false
-}
-
-func (cr *checkedRounds) Prune(identity reception.IdentityUse, earliestAllowed id.Round) {
-	idFp := getIdFingerprint(identity)
-	cl, exists := cr.lookup[idFp]
-	if !exists {
-		return
-	}
-
-	e := cl.l.Front()
-	for e != nil {
-		if e.Value.(id.Round) < earliestAllowed {
-			delete(cl.m, e.Value.(id.Round))
-			lastE := e
-			e = e.Next()
-			cl.l.Remove(lastE)
-		} else {
-			break
-		}
-	}
-}
-
-func getIdFingerprint(identity reception.IdentityUse) idFingerprint {
-	h := md5.New()
-	h.Write(identity.EphId[:])
-	h.Write(identity.Source[:])
-
-	fp := idFingerprint{}
-	copy(fp[:], h.Sum(nil))
-	return fp
-}
diff --git a/network/ephemeral/testutil.go b/network/ephemeral/testutil.go
index fbcc4e3e8d031097f4d9447c0904f537451b3d40..d2708b0c71c7d112151e4494d3ef631a78adaf62 100644
--- a/network/ephemeral/testutil.go
+++ b/network/ephemeral/testutil.go
@@ -8,6 +8,7 @@
 package ephemeral
 
 import (
+	"gitlab.com/elixxir/client/network/gateway"
 	"testing"
 
 	jww "github.com/spf13/jwalterweatherman"
@@ -79,6 +80,10 @@ func (t *testNetworkManager) InProgressRegistrations() int {
 	return 0
 }
 
+func (t *testNetworkManager) GetSender() *gateway.Sender {
+	return nil
+}
+
 func NewTestNetworkManager(i interface{}) interfaces.NetworkManager {
 	switch i.(type) {
 	case *testing.T, *testing.M, *testing.B:
diff --git a/network/follow.go b/network/follow.go
index ebf06e5db5060b5fe69b8e032329ef660a79fdcb..dc9f0196a0e201c2c6e5fc3696ad537c776f5f12 100644
--- a/network/follow.go
+++ b/network/follow.go
@@ -23,18 +23,14 @@ package network
 //		instance
 
 import (
-	"bytes"
 	"fmt"
-	"math"
+	"sync/atomic"
 	"time"
-
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/interfaces"
-	"gitlab.com/elixxir/client/network/gateway"
 	"gitlab.com/elixxir/client/network/rounds"
 	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"
@@ -64,8 +60,9 @@ func (m *manager) followNetwork(report interfaces.ClientErrorReport, quitCh <-ch
 		case <-ticker.C:
 			m.follow(report, rng, m.Comms)
 		case <-TrackTicker.C:
-			jww.INFO.Println(m.tracker.Report())
-			m.tracker = newPollTracker()
+			numPolls := atomic.SwapUint64(m.tracker, 0)
+			jww.INFO.Printf("Polled the network %d times in the "+
+				"last %s", numPolls, debugTrackPeriod)
 		}
 	}
 }
@@ -80,15 +77,7 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 			"impossible: %+v", err)
 	}
 
-	m.tracker.Track(identity.EphId, identity.Source)
-
-	//randomly select a gateway to poll
-	//TODO: make this more intelligent
-	gwHost, err := gateway.Get(m.Instance.GetPartialNdf().Get(), comms, rng)
-	if err != nil {
-		jww.FATAL.Panicf("Failed to follow network, NDF has corrupt "+
-			"data: %s", err)
-	}
+	atomic.AddUint64(m.tracker, 1)
 
 	// Get client version for poll
 	version := m.Session.GetClientVersion()
@@ -105,20 +94,28 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 		ClientVersion:  []byte(version.String()),
 		FastPolling:    m.param.FastPolling,
 	}
-	jww.TRACE.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), gwHost.GetId())
 
-	pollResp, err := comms.SendPoll(gwHost, &pollReq)
+	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())
+		result, err := comms.SendPoll(host, &pollReq)
+		if err != nil {
+			if report != nil {
+				report(
+					"NetworkFollower",
+					fmt.Sprintf("Failed to poll network, \"%s\", Gateway: %s", err.Error(), host.String()),
+					fmt.Sprintf("%+v", err),
+				)
+			}
+			jww.ERROR.Printf("Unable to poll %s for NDF: %+v", host, err)
+		}
+		return result, err
+	})
 	if err != nil {
-		report(
-			"NetworkFollower",
-			fmt.Sprintf("Failed to poll network, \"%s\", Gateway: %s", err.Error(), gwHost.String()),
-			fmt.Sprintf("%+v", err),
-		)
-		jww.ERROR.Printf("Unable to poll %s for NDF: %+v", gwHost, err)
 		return
 	}
+	pollResp := result.(*pb.GatewayPollResponse)
 
 	// ---- Process Network State Update Data ----
 	gwRoundsState := &knownRounds.KnownRounds{}
@@ -138,11 +135,8 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 			return
 		}
 
-		err = m.Instance.UpdateGatewayConnections()
-		if err != nil {
-			jww.ERROR.Printf("Unable to update gateway connections: %+v", err)
-			return
-		}
+		// update gateway connections
+		m.GetSender().UpdateNdf(m.GetInstance().GetPartialNdf().Get())
 	}
 
 	//check that the stored address space is correct
@@ -158,51 +152,52 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 			return
 		}
 
+		// 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
-					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
+		//			// 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
+		//		}
+		//	}
+		//}
 	}
 
 	// ---- Identity Specific Round Processing -----
@@ -225,20 +220,12 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 		return
 	}
 
-	firstRound := id.Round(math.MaxUint64)
-	lastRound := id.Round(0)
 
 	//prepare the filter objects for processing
-	filterList := make([]*rounds.RemoteFilter, filtersEnd-filtersStart)
+	filterList := make([]*rounds.RemoteFilter, 0, filtersEnd-filtersStart)
 	for i := filtersStart; i < filtersEnd; i++ {
 		if len(pollResp.Filters.Filters[i].Filter) != 0 {
-			filterList[i-filtersStart] = rounds.NewRemoteFilter(pollResp.Filters.Filters[i])
-			if filterList[i-filtersStart].FirstRound() < firstRound {
-				firstRound = filterList[i-filtersStart].FirstRound()
-			}
-			if filterList[i-filtersStart].LastRound() > lastRound {
-				lastRound = filterList[i-filtersStart].LastRound()
-			}
+			filterList= append(filterList,rounds.NewRemoteFilter(pollResp.Filters.Filters[i]))
 		}
 	}
 
@@ -246,7 +233,7 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 	// are messages waiting in rounds and then sends signals to the appropriate
 	// handling threads
 	roundChecker := func(rid id.Round) bool {
-		return rounds.Checker(rid, filterList)
+		return rounds.Checker(rid, filterList, identity.CR)
 	}
 
 	// move the earliest unknown round tracker forward to the earliest
@@ -268,30 +255,25 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 
 	roundsWithMessages2 := identity.UR.Iterate(func(rid id.Round) bool {
 		if gwRoundsState.Checked(rid) {
-			return rounds.Checker(rid, filterList)
+			return rounds.Checker(rid, filterList, identity.CR)
 		}
 		return false
 	}, roundsUnknown)
 
 	for _, rid := range roundsWithMessages {
-		if m.checked.Check(identity, rid) {
+		if identity.CR.Check(rid) {
 			m.round.GetMessagesFromRound(rid, identity)
 		}
 	}
-	for _, rid := range roundsWithMessages2 {
-		m.round.GetMessagesFromRound(rid, identity)
-	}
 
-	earliestToKeep := getEarliestToKeep(m.param.KnownRoundsThreshold,
-		gwRoundsState.GetLastChecked())
-
-	m.checked.Prune(identity, earliestToKeep)
-
-}
+	identity.CR.Prune()
+	err = identity.CR.SaveCheckedRounds()
+	if err != nil {
+		jww.ERROR.Printf("Could not save rounds for identity %d (%s): %+v",
+			identity.EphId.Int64(), identity.Source, err)
+	}
 
-func getEarliestToKeep(delta uint, lastchecked id.Round) id.Round {
-	if uint(lastchecked) < delta {
-		return 0
+	for _, rid := range roundsWithMessages2 {
+		m.round.GetMessagesFromRound(rid, identity)
 	}
-	return lastchecked - id.Round(delta)
 }
diff --git a/network/gateway/gateway.go b/network/gateway/gateway.go
deleted file mode 100644
index f4a16cf2e78a5be6fe9cadbcd496b2cd7bb95b79..0000000000000000000000000000000000000000
--- a/network/gateway/gateway.go
+++ /dev/null
@@ -1,112 +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 gateway
-
-import (
-	"encoding/binary"
-	"fmt"
-	"github.com/pkg/errors"
-	"gitlab.com/elixxir/comms/mixmessages"
-	"gitlab.com/elixxir/crypto/shuffle"
-	"gitlab.com/xx_network/comms/connect"
-	"gitlab.com/xx_network/primitives/id"
-	"gitlab.com/xx_network/primitives/ndf"
-	"io"
-	"math"
-)
-
-type HostGetter interface {
-	GetHost(hostId *id.ID) (*connect.Host, bool)
-}
-
-// Get the Host of a random gateway in the NDF
-func Get(ndf *ndf.NetworkDefinition, hg HostGetter, rng io.Reader) (*connect.Host, error) {
-	gwLen := uint32(len(ndf.Gateways))
-	if gwLen == 0 {
-		return nil, errors.Errorf("no gateways available")
-	}
-
-	gwIdx := ReadRangeUint32(0, gwLen, rng)
-	gwID, err := id.Unmarshal(ndf.Nodes[gwIdx].ID)
-	if err != nil {
-		return nil, errors.WithMessage(err, "failed to get Gateway")
-	}
-	gwID.SetType(id.Gateway)
-	gwHost, ok := hg.GetHost(gwID)
-	if !ok {
-		return nil, errors.Errorf("host for gateway %s could not be "+
-			"retrieved", gwID)
-	}
-	return gwHost, nil
-}
-
-// GetAllShuffled returns a shufled list of gateway hosts from the specified round
-func GetAllShuffled(hg HostGetter, ri *mixmessages.RoundInfo) ([]*connect.Host, error) {
-	roundTop := ri.GetTopology()
-	hosts := make([]*connect.Host, 0)
-	shuffledList := make([]uint64, 0)
-
-	// Collect all host information from the round
-	for index, _ := range roundTop {
-		selectedId, err := id.Unmarshal(roundTop[index])
-		if err != nil {
-			return nil, err
-		}
-
-		selectedId.SetType(id.Gateway)
-
-		gwHost, ok := hg.GetHost(selectedId)
-		if !ok {
-			return nil, errors.Errorf("Could not find host for gateway %s", selectedId)
-		}
-		hosts = append(hosts, gwHost)
-		shuffledList = append(shuffledList, uint64(index))
-	}
-
-	returnHosts := make([]*connect.Host, len(hosts))
-
-	// Shuffle a list corresponding to the valid gateway hosts
-	shuffle.Shuffle(&shuffledList)
-
-	// Index through the shuffled list, building a list
-	// of shuffled gateways from the round
-	for index, shuffledIndex := range shuffledList {
-		returnHosts[index] = hosts[shuffledIndex]
-	}
-
-	return returnHosts, nil
-
-}
-
-// ReadUint32 reads an integer from an io.Reader (which should be a CSPRNG)
-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))
-	}
-	return binary.BigEndian.Uint32(rndBytes[:])
-}
-
-// ReadRangeUint32 reduces an integer from 0, MaxUint32 to the range start, end
-func ReadRangeUint32(start, end uint32, rng io.Reader) uint32 {
-	size := end - start
-	// note we could just do the part inside the () here, but then extra
-	// can == size which means a little bit of range is wastes, either
-	// choice seems negligible so we went with the "more correct"
-	extra := (math.MaxUint32%size + 1) % size
-	limit := math.MaxUint32 - extra
-	// Loop until we read something inside the limit
-	for {
-		res := ReadUint32(rng)
-		if res > limit {
-			continue
-		}
-		return (res % size) + start
-	}
-}
diff --git a/network/gateway/hostPool.go b/network/gateway/hostPool.go
new file mode 100644
index 0000000000000000000000000000000000000000..f165ce5a9424a37e08e2442bdafc884959a2472b
--- /dev/null
+++ b/network/gateway/hostPool.go
@@ -0,0 +1,465 @@
+///////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                          //
+//                                                                           //
+// Use of this source code is governed by a license that can be found in the //
+// LICENSE file                                                              //
+///////////////////////////////////////////////////////////////////////////////
+
+// Package gateway Handles functionality related to providing Gateway connect.Host objects
+// for message sending to the rest of the client repo
+// Used to minimize # of open connections on mobile clients
+
+package gateway
+
+import (
+	"encoding/binary"
+	"fmt"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/comms/network"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/xx_network/comms/connect"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/ndf"
+	"io"
+	"math"
+	"strings"
+	"sync"
+	"time"
+)
+
+// List of errors that initiate a Host replacement
+var errorsList = []string{"context deadline exceeded", "connection refused", "host disconnected",
+	"transport is closing", "all SubConns are in TransientFailure", "Last try to connect",
+	ndf.NO_NDF, "Host is in cool down"}
+
+// HostManager Interface allowing storage and retrieval of Host objects
+type HostManager interface {
+	GetHost(hostId *id.ID) (*connect.Host, bool)
+	AddHost(hid *id.ID, address string, cert []byte, params connect.HostParams) (host *connect.Host, err error)
+	RemoveHost(hid *id.ID)
+}
+
+// HostPool Handles providing hosts to the Client
+type HostPool struct {
+	hostMap  map[id.ID]uint32 // map key to its index in the slice
+	hostList []*connect.Host  // each index in the slice contains the value
+	hostMux  sync.RWMutex     // Mutex for the above map/list combination
+
+	ndfMap map[id.ID]int // map gateway ID to its index in the ndf
+	ndf    *ndf.NetworkDefinition
+	ndfMux sync.RWMutex
+
+	poolParams     PoolParams
+	rng            *fastRNG.StreamGenerator
+	storage        *storage.Session
+	manager        HostManager
+	addGatewayChan chan network.NodeGateway
+}
+
+// PoolParams Allows configuration of HostPool parameters
+type PoolParams struct {
+	MaxPoolSize uint32 // Maximum number of Hosts in the HostPool
+	PoolSize    uint32 // Allows override of HostPool size. Set to zero for dynamic size calculation
+	// TODO: Move up a layer
+	ProxyAttempts uint32             // How many proxies will be used in event of send failure
+	HostParams    connect.HostParams // Parameters for the creation of new Host objects
+}
+
+// DefaultPoolParams Returns a default set of PoolParams
+func DefaultPoolParams() PoolParams {
+	p := PoolParams{
+		MaxPoolSize:   30,
+		ProxyAttempts: 5,
+		PoolSize:      0,
+		HostParams:    connect.GetDefaultHostParams(),
+	}
+	p.HostParams.MaxRetries = 1
+	p.HostParams.AuthEnabled = false
+	p.HostParams.EnableCoolOff = true
+	p.HostParams.NumSendsBeforeCoolOff = 1
+	p.HostParams.CoolOffTimeout = 5 * time.Minute
+	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) {
+	var err error
+
+	// Determine size of HostPool
+	if poolParams.PoolSize == 0 {
+		poolParams.PoolSize, err = getPoolSize(uint32(len(ndf.Gateways)), poolParams.MaxPoolSize)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	result := &HostPool{
+		manager:        getter,
+		hostMap:        make(map[id.ID]uint32),
+		hostList:       make([]*connect.Host, poolParams.PoolSize),
+		poolParams:     poolParams,
+		ndf:            ndf,
+		rng:            rng,
+		storage:        storage,
+		addGatewayChan: addGateway,
+	}
+
+	// Propagate the NDF
+	err = result.updateConns()
+	if err != nil {
+		return nil, err
+	}
+
+	// Build the initial HostPool and return
+	for i := 0; 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))
+	return result, nil
+}
+
+// UpdateNdf Mutates internal ndf to the given ndf
+func (h *HostPool) UpdateNdf(ndf *ndf.NetworkDefinition) {
+	if len(ndf.Gateways) == 0 {
+		jww.WARN.Printf("Unable to UpdateNdf: no gateways available")
+		return
+	}
+
+	h.ndfMux.Lock()
+	h.ndf = ndf
+
+	h.hostMux.Lock()
+	err := h.updateConns()
+	h.hostMux.Unlock()
+	if err != nil {
+		jww.ERROR.Printf("Unable to updateConns: %+v", err)
+	}
+	h.ndfMux.Unlock()
+}
+
+// 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 {
+		length = h.poolParams.PoolSize
+	}
+
+	checked := make(map[uint32]interface{}) // Keep track of Hosts already selected to avoid duplicates
+	if excluded != nil {
+		// Add excluded Hosts to already-checked list
+		for i := range excluded {
+			gwId := excluded[i]
+			if idx, ok := h.hostMap[*gwId]; ok {
+				checked[idx] = nil
+			}
+		}
+	}
+
+	result := make([]*connect.Host, 0, length)
+	rng := h.rng.GetStream()
+	h.hostMux.RLock()
+	for i := uint32(0); i < length; {
+		// If we've checked the entire HostPool, bail
+		if uint32(len(checked)) >= h.poolParams.PoolSize {
+			break
+		}
+
+		// Check the next HostPool index
+		gwIdx := readRangeUint32(0, h.poolParams.PoolSize, rng)
+		if _, ok := checked[gwIdx]; !ok {
+			result = append(result, h.hostList[gwIdx])
+			checked[gwIdx] = nil
+			i++
+		}
+	}
+	h.hostMux.RUnlock()
+	rng.Close()
+
+	return result
+}
+
+// Obtain a specific connect.Host from the manager, irrespective of the HostPool
+func (h *HostPool) getSpecific(target *id.ID) (*connect.Host, bool) {
+	return h.manager.GetHost(target)
+}
+
+// Try to obtain the given targets from the HostPool
+// If each is not present, obtain a random replacement from the HostPool
+func (h *HostPool) getPreferred(targets []*id.ID) []*connect.Host {
+	checked := make(map[uint32]interface{}) // Keep track of Hosts already selected to avoid duplicates
+	length := len(targets)
+	if length > int(h.poolParams.PoolSize) {
+		length = int(h.poolParams.PoolSize)
+	}
+	result := make([]*connect.Host, length)
+
+	rng := h.rng.GetStream()
+	h.hostMux.RLock()
+	for i := 0; i < length; {
+		if hostIdx, ok := h.hostMap[*targets[i]]; ok {
+			result[i] = h.hostList[hostIdx]
+			checked[hostIdx] = nil
+			i++
+			continue
+		}
+
+		gwIdx := readRangeUint32(0, h.poolParams.PoolSize, rng)
+		if _, ok := checked[gwIdx]; !ok {
+			result[i] = h.hostList[gwIdx]
+			checked[gwIdx] = nil
+			i++
+		}
+	}
+	h.hostMux.RUnlock()
+	rng.Close()
+
+	return result
+}
+
+// Replaces the given hostId in the HostPool if the given hostErr is in errorList
+func (h *HostPool) checkReplace(hostId *id.ID, hostErr error) error {
+	// Check if Host should be replaced
+	doReplace := false
+	if hostErr != nil {
+		for _, errString := range errorsList {
+			if strings.Contains(hostErr.Error(), errString) {
+				// Host needs replaced, flag and continue
+				doReplace = true
+				break
+			}
+		}
+	}
+
+	if doReplace {
+		h.hostMux.Lock()
+		defer h.hostMux.Unlock()
+
+		// If the Host is still in the pool
+		if oldPoolIndex, ok := h.hostMap[*hostId]; ok {
+			// Replace it
+			h.ndfMux.RLock()
+			err := h.forceReplace(oldPoolIndex)
+			h.ndfMux.RUnlock()
+			return err
+		}
+	}
+	return nil
+}
+
+// Replace given Host index with a new, randomly-selected Host from the NDF
+func (h *HostPool) forceReplace(oldPoolIndex uint32) error {
+	rng := h.rng.GetStream()
+	defer rng.Close()
+
+	// Loop until a replacement Host is found
+	for {
+		// Randomly select a new Gw by index in the NDF
+		ndfIdx := readRangeUint32(0, uint32(len(h.ndf.Gateways)), rng)
+		jww.DEBUG.Printf("Attempting to replace Host at HostPool %d with Host at NDF %d...", oldPoolIndex, ndfIdx)
+
+		// Use the random ndfIdx to obtain a GwId from the NDF
+		gwId, err := id.Unmarshal(h.ndf.Gateways[ndfIdx].ID)
+		if err != nil {
+			return errors.WithMessage(err, "failed to get Gateway for pruning")
+		}
+
+		// Verify the new GwId is not already in the hostMap
+		if _, ok := h.hostMap[*gwId]; !ok {
+			// If it is a new GwId, replace the old Host with the new Host
+			return h.replaceHost(gwId, oldPoolIndex)
+		}
+	}
+}
+
+// Replace the given slot in the HostPool with a new Gateway with the specified ID
+func (h *HostPool) replaceHost(newId *id.ID, oldPoolIndex uint32) error {
+	// Obtain that GwId's Host object
+	newHost, ok := h.manager.GetHost(newId)
+	if !ok {
+		return errors.Errorf("host for gateway %s could not be "+
+			"retrieved", newId)
+	}
+
+	// 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
+	h.hostList[oldPoolIndex] = newHost
+	// Use the GwId to keep track of the new random Host's index in the hostList
+	h.hostMap[*newId] = oldPoolIndex
+
+	// Clean up and move onto next Host
+	if oldHost != nil {
+		delete(h.hostMap, *oldHost.GetId())
+		go oldHost.Disconnect()
+	}
+	jww.DEBUG.Printf("Replaced Host at %d with new Host %s", oldPoolIndex, newId.String())
+	return nil
+}
+
+// Force-add the Gateways to the HostPool, each replacing a random Gateway
+func (h *HostPool) forceAdd(gwId *id.ID) error {
+	rng := h.rng.GetStream()
+	h.hostMux.Lock()
+	defer h.hostMux.Unlock()
+	defer rng.Close()
+
+	// Verify the GwId is not already in the hostMap
+	if _, ok := h.hostMap[*gwId]; ok {
+		// If it is, skip
+		return nil
+	}
+
+	// Randomly select another Gateway in the HostPool for replacement
+	poolIdx := readRangeUint32(0, h.poolParams.PoolSize, rng)
+	return h.replaceHost(gwId, poolIdx)
+}
+
+// Updates the internal HostPool with any changes to the NDF
+func (h *HostPool) updateConns() error {
+	// Prepare NDFs for comparison
+	newMap, err := convertNdfToMap(h.ndf)
+	if err != nil {
+		return errors.Errorf("Unable to convert new NDF to set: %+v", err)
+	}
+
+	// Handle adding Gateways
+	for gwId, ndfIdx := range newMap {
+		if _, ok := h.ndfMap[gwId]; !ok {
+			// If GwId in newMap is not in ndfMap, add the Gateway
+			h.addGateway(gwId.DeepCopy(), ndfIdx)
+		}
+	}
+
+	// Handle removing Gateways
+	for gwId := range h.ndfMap {
+		if _, ok := newMap[gwId]; !ok {
+			// If GwId in ndfMap is not in newMap, remove the Gateway
+			h.removeGateway(gwId.DeepCopy())
+		}
+	}
+
+	// Update the internal NDF set
+	h.ndfMap = newMap
+	return nil
+}
+
+// Takes ndf.Gateways and puts their IDs into a map object
+func convertNdfToMap(ndf *ndf.NetworkDefinition) (map[id.ID]int, error) {
+	result := make(map[id.ID]int)
+	if ndf == nil {
+		return result, nil
+	}
+
+	// Process gateway Id's into set
+	for i := range ndf.Gateways {
+		gw := ndf.Gateways[i]
+		gwId, err := id.Unmarshal(gw.ID)
+		if err != nil {
+			return nil, err
+		}
+		result[*gwId] = i
+	}
+
+	return result, nil
+}
+
+// updateConns helper for removing old Gateways
+func (h *HostPool) removeGateway(gwId *id.ID) {
+	h.manager.RemoveHost(gwId)
+	// If needed, replace the removed Gateway in the HostPool with a new one
+	if poolIndex, ok := h.hostMap[*gwId]; ok {
+		err := h.forceReplace(poolIndex)
+		if err != nil {
+			jww.ERROR.Printf("Unable to removeGateway: %+v", err)
+		}
+	}
+}
+
+// updateConns helper for adding new Gateways
+func (h *HostPool) addGateway(gwId *id.ID, ndfIndex int) {
+	gw := h.ndf.Gateways[ndfIndex]
+
+	//check if the host exists
+	host, ok := h.manager.GetHost(gwId)
+	if !ok {
+
+		// Check if gateway ID collides with an existing hard coded ID
+		if id.CollidesWithHardCodedID(gwId) {
+			jww.ERROR.Printf("Gateway ID invalid, collides with a "+
+				"hard coded ID. Invalid ID: %v", gwId.Marshal())
+		}
+
+		// Add the new gateway host
+		_, err := h.manager.AddHost(gwId, gw.Address, []byte(gw.TlsCertificate), h.poolParams.HostParams)
+		if err != nil {
+			jww.ERROR.Printf("Could not add gateway host %s: %+v", gwId, err)
+		}
+
+		// Send AddGateway event if we do not already possess keys for the GW
+		if !h.storage.Cmix().Has(gwId) {
+			ng := network.NodeGateway{
+				Node:    h.ndf.Nodes[ndfIndex],
+				Gateway: gw,
+			}
+
+			select {
+			case h.addGatewayChan <- ng:
+			default:
+				jww.WARN.Printf("Unable to send AddGateway event for id %s", gwId.String())
+			}
+		}
+
+	} else if host.GetAddress() != gw.Address {
+		host.UpdateAddress(gw.Address)
+	}
+}
+
+// getPoolSize determines the size of the HostPool based on the size of the NDF
+func getPoolSize(ndfLen, maxSize uint32) (uint32, error) {
+	// Verify the NDF has at least one Gateway for the HostPool
+	if ndfLen == 0 {
+		return 0, errors.Errorf("Unable to create HostPool: no gateways available")
+	}
+
+	// PoolSize = ceil(sqrt(len(ndf,Gateways)))
+	poolSize := uint32(math.Ceil(math.Sqrt(float64(ndfLen))))
+	if poolSize > maxSize {
+		return maxSize, nil
+	}
+	return poolSize, nil
+}
+
+// readUint32 reads an integer from an io.Reader (which should be a CSPRNG)
+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))
+	}
+	return binary.BigEndian.Uint32(rndBytes[:])
+}
+
+// readRangeUint32 reduces an integer from 0, MaxUint32 to the range start, end
+func readRangeUint32(start, end uint32, rng io.Reader) uint32 {
+	size := end - start
+	// note we could just do the part inside the () here, but then extra
+	// can == size which means a little bit of range is wastes, either
+	// choice seems negligible so we went with the "more correct"
+	extra := (math.MaxUint32%size + 1) % size
+	limit := math.MaxUint32 - extra
+	// Loop until we read something inside the limit
+	for {
+		res := readUint32(rng)
+		if res > limit {
+			continue
+		}
+		return (res % size) + start
+	}
+}
diff --git a/network/gateway/hostpool_test.go b/network/gateway/hostpool_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8181e6d49f0ea5522e8ae33eb6ac143516c45a26
--- /dev/null
+++ b/network/gateway/hostpool_test.go
@@ -0,0 +1,832 @@
+///////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                          //
+//                                                                           //
+// Use of this source code is governed by a license that can be found in the //
+// LICENSE file                                                              //
+///////////////////////////////////////////////////////////////////////////////
+
+package gateway
+
+import (
+	"fmt"
+	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/comms/network"
+	"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"
+	"reflect"
+	"testing"
+)
+
+// Unit test
+func TestNewHostPool(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))
+
+	// Pull all gateways from ndf into host manager
+	for _, gw := range testNdf.Gateways {
+
+		gwId, err := id.Unmarshal(gw.ID)
+		if err != nil {
+			t.Errorf("Failed to unmarshal ID in mock ndf: %v", err)
+		}
+		// Add mock gateway to manager
+		_, err = manager.AddHost(gwId, "", nil, connect.GetDefaultHostParams())
+		if err != nil {
+			t.Errorf("Could not add mock host to manager: %v", err)
+			t.FailNow()
+		}
+
+	}
+
+	// Call the constructor
+	_, err := newHostPool(params, rng, testNdf, manager,
+		testStorage, addGwChan)
+	if err != nil {
+		t.Fatalf("Failed to create mock host pool: %v", err)
+	}
+}
+
+// Unit test
+func TestHostPool_ManageHostPool(t *testing.T) {
+	manager := newMockManager()
+	rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG)
+	testNdf := getTestNdf(t)
+	testStorage := storage.InitTestingSession(t)
+	addGwChan := make(chan network.NodeGateway)
+
+	// Construct custom params
+	params := DefaultPoolParams()
+	params.MaxPoolSize = uint32(len(testNdf.Gateways))
+
+	// Pull all gateways from ndf into host manager
+	for _, gw := range testNdf.Gateways {
+
+		gwId, err := id.Unmarshal(gw.ID)
+		if err != nil {
+			t.Errorf("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.Errorf("Could not add mock host to manager: %v", err)
+			t.FailNow()
+		}
+
+	}
+
+	// Call the constructor
+	testPool, err := newHostPool(params, rng, testNdf, manager,
+		testStorage, addGwChan)
+	if err != nil {
+		t.Fatalf("Failed to create mock host pool: %v", err)
+	}
+
+	// Construct a list of new gateways/nodes to add to ndf
+	newGatewayLen := len(testNdf.Gateways)
+	newGateways := make([]ndf.Gateway, newGatewayLen)
+	newNodes := make([]ndf.Node, newGatewayLen)
+	for i := 0; i < newGatewayLen; i++ {
+		// Construct gateways
+		gwId := id.NewIdFromUInt(uint64(100+i), id.Gateway, t)
+		newGateways[i] = ndf.Gateway{ID: gwId.Bytes()}
+		// Construct nodes
+		nodeId := gwId.DeepCopy()
+		nodeId.SetType(id.Node)
+		newNodes[i] = ndf.Node{ID: nodeId.Bytes()}
+
+	}
+
+	newNdf := getTestNdf(t)
+	// Update the ndf, removing some gateways at a cutoff
+	newNdf.Gateways = newGateways
+	newNdf.Nodes = newNodes
+
+	testPool.UpdateNdf(newNdf)
+
+	// Check that old gateways are not in pool
+	for _, ndfGw := range testNdf.Gateways {
+		gwId, err := id.Unmarshal(ndfGw.ID)
+		if err != nil {
+			t.Errorf("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)
+		}
+	}
+}
+
+// Full happy path test
+func TestHostPool_ReplaceHost(t *testing.T) {
+	manager := newMockManager()
+	testNdf := getTestNdf(t)
+	newIndex := uint32(20)
+
+	// Construct a manager (bypass business logic in constructor)
+	hostPool := &HostPool{
+		manager:  manager,
+		hostList: make([]*connect.Host, newIndex+1),
+		hostMap:  make(map[id.ID]uint32),
+		ndf:      testNdf,
+	}
+
+	/* "Replace" a host with no entry */
+
+	// Pull a gateway ID from the ndf
+	gwIdOne, err := id.Unmarshal(testNdf.Gateways[0].ID)
+	if err != nil {
+		t.Errorf("Failed to unmarshal ID in mock ndf: %v", err)
+	}
+
+	// Add mock gateway to manager
+	_, err = manager.AddHost(gwIdOne, "", nil, connect.GetDefaultHostParams())
+	if err != nil {
+		t.Errorf("Could not add mock host to manager: %v", err)
+	}
+
+	// "Replace" (insert) the host
+	err = hostPool.replaceHost(gwIdOne, newIndex)
+	if err != nil {
+		t.Errorf("Could not replace host: %v", err)
+	}
+
+	// Check the state of the map has been correctly updated
+	retrievedIndex, ok := hostPool.hostMap[*gwIdOne]
+	if !ok {
+		t.Errorf("Expected insertion of gateway ID into map")
+	}
+	if retrievedIndex != newIndex {
+		t.Errorf("Index pulled from map not expected value."+
+			"\n\tExpected: %d"+
+			"\n\tReceived: %d", newIndex, retrievedIndex)
+	}
+
+	// Check that the state of the list list been correctly updated
+	retrievedHost := hostPool.hostList[newIndex]
+	if !gwIdOne.Cmp(retrievedHost.GetId()) {
+		t.Errorf("Id pulled from list is not expected."+
+			"\n\tExpected: %s"+
+			"\n\tReceived: %s", gwIdOne, retrievedHost.GetId())
+	}
+
+	/* Replace the initial host with a new host */
+
+	// Pull a different gateway ID from the ndf
+	gwIdTwo, err := id.Unmarshal(testNdf.Gateways[1].ID)
+	if err != nil {
+		t.Errorf("Failed to unmarshal ID in mock ndf: %v", err)
+	}
+
+	// Add second mock gateway to manager
+	_, err = manager.AddHost(gwIdTwo, "", nil, connect.GetDefaultHostParams())
+	if err != nil {
+		t.Errorf("Could not add mock host to manager: %v", err)
+	}
+
+	// Replace the old host
+	err = hostPool.replaceHost(gwIdTwo, newIndex)
+	if err != nil {
+		t.Errorf("Could not replace host: %v", err)
+	}
+
+	// Check that the state of the list been correctly updated for new host
+	retrievedHost = hostPool.hostList[newIndex]
+	if !gwIdTwo.Cmp(retrievedHost.GetId()) {
+		t.Errorf("Id pulled from list is not expected."+
+			"\n\tExpected: %s"+
+			"\n\tReceived: %s", gwIdTwo, retrievedHost.GetId())
+	}
+
+	// Check the state of the map has been correctly removed for the old gateway
+	retrievedOldIndex, ok := hostPool.hostMap[*gwIdOne]
+	if ok {
+		t.Errorf("Exoected old gateway to be cleared from map")
+	}
+	if retrievedOldIndex != 0 {
+		t.Errorf("Index pulled from map with old gateway as the key "+
+			"was not cleared."+
+			"\n\tExpected: %d"+
+			"\n\tReceived: %d", 0, retrievedOldIndex)
+	}
+
+	// Check the state of the map has been correctly updated for the old gateway
+	retrievedIndex, ok = hostPool.hostMap[*gwIdTwo]
+	if !ok {
+		t.Errorf("Expected insertion of gateway ID into map")
+	}
+	if retrievedIndex != newIndex {
+		t.Errorf("Index pulled from map using new gateway as the key "+
+			"was not updated."+
+			"\n\tExpected: %d"+
+			"\n\tReceived: %d", newIndex, retrievedIndex)
+	}
+
+}
+
+// Error path, could not get host
+func TestHostPool_ReplaceHost_Error(t *testing.T) {
+	manager := newMockManager()
+
+	// Construct a manager (bypass business logic in constructor)
+	hostPool := &HostPool{
+		manager:  manager,
+		hostList: make([]*connect.Host, 1),
+		hostMap:  make(map[id.ID]uint32),
+	}
+
+	// Construct an unknown gateway ID to the manager
+	gatewayId := id.NewIdFromString("BadGateway", id.Gateway, t)
+
+	err := hostPool.replaceHost(gatewayId, 0)
+	if err == nil {
+		t.Errorf("Expected error in happy path: Should not be able to find a host")
+	}
+
+}
+
+// Unit test
+func TestHostPool_ForceReplace(t *testing.T) {
+	manager := newMockManager()
+	rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG)
+	testNdf := getTestNdf(t)
+	testStorage := storage.InitTestingSession(t)
+	addGwChan := make(chan network.NodeGateway)
+
+	// Construct custom params
+	params := DefaultPoolParams()
+	params.PoolSize = uint32(len(testNdf.Gateways))
+
+	// Pull all gateways from ndf into host manager
+	for _, gw := range testNdf.Gateways {
+
+		gwId, err := id.Unmarshal(gw.ID)
+		if err != nil {
+			t.Errorf("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.Errorf("Could not add mock host to manager: %v", err)
+			t.FailNow()
+		}
+
+	}
+
+	// Call the constructor
+	testPool, err := newHostPool(params, rng, testNdf, manager,
+		testStorage, addGwChan)
+	if err != nil {
+		t.Fatalf("Failed to create mock host pool: %v", err)
+	}
+
+	// Add all gateways to hostPool's map
+	for index, gw := range testNdf.Gateways {
+		gwId, err := id.Unmarshal(gw.ID)
+		if err != nil {
+			t.Fatalf("Failed to unmarshal ID in mock ndf: %v", err)
+		}
+
+		err = testPool.replaceHost(gwId, uint32(index))
+		if err != nil {
+			t.Fatalf("Failed to replace host in set-up: %v", err)
+		}
+	}
+
+	oldGatewayIndex := 0
+	oldHost := testPool.hostList[oldGatewayIndex]
+
+	// Force replace the gateway at a given index
+	err = testPool.forceReplace(uint32(oldGatewayIndex))
+	if err != nil {
+		t.Errorf("Failed to force replace: %v", err)
+	}
+
+	// Ensure that old gateway has been removed from the map
+	if _, ok := testPool.hostMap[*oldHost.GetId()]; ok {
+		t.Errorf("Expected old host to be removed from map")
+	}
+
+	// Ensure we are disconnected from the old host
+	if isConnected, _ := oldHost.Connected(); isConnected {
+		t.Errorf("Failed to disconnect from old host %s", oldHost)
+	}
+
+}
+
+// Unit test
+func TestHostPool_CheckReplace(t *testing.T) {
+	manager := newMockManager()
+	rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG)
+	testNdf := getTestNdf(t)
+	testStorage := storage.InitTestingSession(t)
+	addGwChan := make(chan network.NodeGateway)
+
+	// Construct custom params
+	params := DefaultPoolParams()
+	params.MaxPoolSize = uint32(len(testNdf.Gateways)) - 5
+
+	// Pull all gateways from ndf into host manager
+	for _, gw := range testNdf.Gateways {
+
+		gwId, err := id.Unmarshal(gw.ID)
+		if err != nil {
+			t.Errorf("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.Errorf("Could not add mock host to manager: %v", err)
+			t.FailNow()
+		}
+
+	}
+
+	// Call the constructor
+	testPool, err := newHostPool(params, rng, testNdf, manager,
+		testStorage, addGwChan)
+	if err != nil {
+		t.Fatalf("Failed to create mock host pool: %v", err)
+	}
+
+	// Call check replace
+	oldGatewayIndex := 0
+	oldHost := testPool.hostList[oldGatewayIndex]
+	expectedError := fmt.Errorf(errorsList[0])
+	err = testPool.checkReplace(oldHost.GetId(), expectedError)
+	if err != nil {
+		t.Errorf("Failed to check replace: %v", err)
+	}
+
+	// Ensure that old gateway has been removed from the map
+	if _, ok := testPool.hostMap[*oldHost.GetId()]; ok {
+		t.Errorf("Expected old host to be removed from map")
+	}
+
+	// Ensure we are disconnected from the old host
+	if isConnected, _ := oldHost.Connected(); isConnected {
+		t.Errorf("Failed to disconnect from old host %s", oldHost)
+	}
+
+	// Check that an error not in the global list results in a no-op
+	goodGatewayIndex := 0
+	goodGateway := testPool.hostList[goodGatewayIndex]
+	unexpectedErr := fmt.Errorf("not in global error list")
+	err = testPool.checkReplace(oldHost.GetId(), unexpectedErr)
+	if err != nil {
+		t.Errorf("Failed to check replace: %v", err)
+	}
+
+	// Ensure that gateway with an unexpected error was not modified
+	if _, ok := testPool.hostMap[*goodGateway.GetId()]; !ok {
+		t.Errorf("Expected gateway with non-expected error to not be modified")
+	}
+
+	// Ensure gateway host has not been disconnected
+	if isConnected, _ := oldHost.Connected(); isConnected {
+		t.Errorf("Should not disconnect from  %s", oldHost)
+	}
+
+}
+
+// Unit test
+func TestHostPool_UpdateNdf(t *testing.T) {
+	manager := newMockManager()
+	testNdf := getTestNdf(t)
+	newIndex := uint32(20)
+
+	// Construct a manager (bypass business logic in constructor)
+	hostPool := &HostPool{
+		manager:  manager,
+		hostList: make([]*connect.Host, newIndex+1),
+		hostMap:  make(map[id.ID]uint32),
+		ndf:      testNdf,
+		storage:  storage.InitTestingSession(t),
+	}
+
+	// Construct a new Ndf different from original one above
+	newNdf := getTestNdf(t)
+	newGateway := ndf.Gateway{
+		ID: id.NewIdFromUInt(27, id.Gateway, t).Bytes(),
+	}
+	newNode := ndf.Node{
+		ID: id.NewIdFromUInt(27, id.Node, t).Bytes(),
+	}
+	newNdf.Gateways = append(newNdf.Gateways, newGateway)
+	newNdf.Nodes = append(newNdf.Nodes, newNode)
+
+	// Update pool with the new Ndf
+	hostPool.UpdateNdf(newNdf)
+
+	// Check that the host pool's ndf has been modified properly
+	if !reflect.DeepEqual(newNdf, hostPool.ndf) {
+		t.Errorf("Host pool ndf not updated to new ndf.")
+	}
+}
+
+// Full test
+func TestHostPool_GetPreferred(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.PoolSize = uint32(len(testNdf.Gateways))
+
+	// Pull all gateways from ndf into host manager
+	hostMap := make(map[id.ID]bool, 0)
+	targets := make([]*id.ID, 0)
+	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)
+		}
+
+		hostMap[*gwId] = true
+		targets = append(targets, gwId)
+
+	}
+
+	// Call the constructor
+	testPool, err := newHostPool(params, rng, testNdf, manager,
+		testStorage, addGwChan)
+	if err != nil {
+		t.Fatalf("Failed to create mock host pool: %v", err)
+	}
+
+	retrievedList := testPool.getPreferred(targets)
+	if len(retrievedList) != len(targets) {
+		t.Errorf("Requested list did not output requested length."+
+			"\n\tExpected: %d"+
+			"\n\tReceived: %v", len(targets), len(retrievedList))
+	}
+
+	// In case where all requested gateways are present
+	// ensure requested hosts were returned
+	for _, h := range retrievedList {
+		if !hostMap[*h.GetId()] {
+			t.Errorf("A target gateways which should have been returned was not."+
+				"\n\tExpected: %v", h.GetId())
+		}
+	}
+
+	// Replace a request with a gateway not in pool
+	targets[3] = id.NewIdFromUInt(74, id.Gateway, t)
+	retrievedList = testPool.getPreferred(targets)
+	if len(retrievedList) != len(targets) {
+		t.Errorf("Requested list did not output requested length."+
+			"\n\tExpected: %d"+
+			"\n\tReceived: %v", len(targets), len(retrievedList))
+	}
+
+	// In case where a requested gateway is not present
+	for _, h := range retrievedList {
+		if h.GetId().Cmp(targets[3]) {
+			t.Errorf("Should not have returned ID not in pool")
+		}
+	}
+
+}
+
+// Unit test
+func TestHostPool_GetAny(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))
+
+	// 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)
+		}
+
+	}
+
+	// Call the constructor
+	testPool, err := newHostPool(params, rng, testNdf, manager,
+		testStorage, addGwChan)
+	if err != nil {
+		t.Fatalf("Failed to create mock host pool: %v", err)
+	}
+
+	requested := 3
+	anyList := testPool.getAny(uint32(requested), nil)
+	if len(anyList) != requested {
+		t.Errorf("GetAnyList did not get requested length."+
+			"\n\tExpected: %v"+
+			"\n\tReceived: %v", requested, len(anyList))
+	}
+
+	for _, h := range anyList {
+		_, ok := manager.GetHost(h.GetId())
+		if !ok {
+			t.Errorf("Host %s in retrieved list not in manager", h)
+		}
+	}
+
+	// Request more than are in host list
+	largeRequest := uint32(requested * 1000)
+	largeRetrieved := testPool.getAny(largeRequest, nil)
+	if len(largeRetrieved) != len(testPool.hostList) {
+		t.Errorf("Large request should result in a list of all in host list")
+	}
+
+}
+
+// Unit test
+func TestHostPool_ForceAdd(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.PoolSize = uint32(len(testNdf.Gateways))
+
+	// 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)
+		}
+
+	}
+
+	// Call the constructor
+	testPool, err := newHostPool(params, rng, testNdf, manager,
+		testStorage, addGwChan)
+	if err != nil {
+		t.Fatalf("Failed to create mock host pool: %v", err)
+	}
+
+	// Construct a new gateway to add
+	gwId := id.NewIdFromUInt(uint64(100), id.Gateway, t)
+	// Add mock gateway to manager
+	_, err = manager.AddHost(gwId, "", nil, connect.GetDefaultHostParams())
+	if err != nil {
+		t.Fatalf("Could not add mock host to manager: %v", err)
+	}
+
+	// forceAdd gateway
+	err = testPool.forceAdd(gwId)
+	if err != nil {
+		t.Errorf("Could not add gateways: %v", err)
+	}
+
+	// check that gateways have been added to the map
+	if _, ok := testPool.hostMap[*gwId]; !ok {
+		t.Errorf("Failed to forcefully add new gateway ID: %v", gwId)
+	}
+}
+
+// Unit test which only adds information to ndf
+func TestHostPool_UpdateConns_AddGateways(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))
+
+	// 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)
+		}
+
+	}
+
+	// Call the constructor
+	testPool, err := newHostPool(params, rng, testNdf, manager,
+		testStorage, addGwChan)
+	if err != nil {
+		t.Fatalf("Failed to create mock host pool: %v", err)
+	}
+
+	// Construct a list of new gateways/nodes to add to ndf
+	newGatewayLen := 10
+	newGateways := make([]ndf.Gateway, newGatewayLen)
+	newNodes := make([]ndf.Node, newGatewayLen)
+	for i := 0; i < newGatewayLen; i++ {
+		// Construct gateways
+		gwId := id.NewIdFromUInt(uint64(100+i), id.Gateway, t)
+		newGateways[i] = ndf.Gateway{ID: gwId.Bytes()}
+		// Construct nodes
+		nodeId := gwId.DeepCopy()
+		nodeId.SetType(id.Node)
+		newNodes[i] = ndf.Node{ID: nodeId.Bytes()}
+
+	}
+
+	// Update the ndf
+	newNdf := getTestNdf(t)
+	newNdf.Gateways = append(newNdf.Gateways, newGateways...)
+	newNdf.Nodes = append(newNdf.Nodes, newNodes...)
+
+	testPool.UpdateNdf(newNdf)
+
+	// Update the connections
+	err = testPool.updateConns()
+	if err != nil {
+		t.Errorf("Failed to update connections: %v", err)
+	}
+
+	// Check that new gateways are in manager
+	for _, ndfGw := range newGateways {
+		gwId, err := id.Unmarshal(ndfGw.ID)
+		if err != nil {
+			t.Errorf("Failed to marshal gateway id for %v", ndfGw)
+		}
+		_, ok := testPool.getSpecific(gwId)
+		if !ok {
+			t.Errorf("Failed to find gateway %v in manager", gwId)
+		}
+	}
+
+}
+
+// Unit test which only adds information to ndf
+func TestHostPool_UpdateConns_RemoveGateways(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))
+
+	// Pull all gateways from ndf into host manager
+	for _, gw := range testNdf.Gateways {
+
+		gwId, err := id.Unmarshal(gw.ID)
+		if err != nil {
+			t.Errorf("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.Errorf("Could not add mock host to manager: %v", err)
+			t.FailNow()
+		}
+
+	}
+
+	// Call the constructor
+	testPool, err := newHostPool(params, rng, testNdf, manager,
+		testStorage, addGwChan)
+	if err != nil {
+		t.Fatalf("Failed to create mock host pool: %v", err)
+	}
+
+	// Construct a list of new gateways/nodes to add to ndf
+	newGatewayLen := len(testNdf.Gateways)
+	newGateways := make([]ndf.Gateway, newGatewayLen)
+	newNodes := make([]ndf.Node, newGatewayLen)
+	for i := 0; i < newGatewayLen; i++ {
+		// Construct gateways
+		gwId := id.NewIdFromUInt(uint64(100+i), id.Gateway, t)
+		newGateways[i] = ndf.Gateway{ID: gwId.Bytes()}
+		// Construct nodes
+		nodeId := gwId.DeepCopy()
+		nodeId.SetType(id.Node)
+		newNodes[i] = ndf.Node{ID: nodeId.Bytes()}
+
+	}
+
+	// Update the ndf, replacing old data entirely
+	newNdf := getTestNdf(t)
+	newNdf.Gateways = newGateways
+	newNdf.Nodes = newNodes
+
+	testPool.UpdateNdf(newNdf)
+
+	// Update the connections
+	err = testPool.updateConns()
+	if err != nil {
+		t.Errorf("Failed to update connections: %v", err)
+	}
+
+	// Check that old gateways are not in pool
+	for _, ndfGw := range testNdf.Gateways {
+		gwId, err := id.Unmarshal(ndfGw.ID)
+		if err != nil {
+			t.Errorf("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)
+		}
+	}
+}
+
+// Unit test
+func TestHostPool_AddGateway(t *testing.T) {
+	manager := newMockManager()
+	testNdf := getTestNdf(t)
+	newIndex := uint32(20)
+	params := DefaultPoolParams()
+	params.MaxPoolSize = uint32(len(testNdf.Gateways))
+
+	// Construct a manager (bypass business logic in constructor)
+	hostPool := &HostPool{
+		manager:        manager,
+		hostList:       make([]*connect.Host, newIndex+1),
+		hostMap:        make(map[id.ID]uint32),
+		ndf:            testNdf,
+		addGatewayChan: make(chan network.NodeGateway),
+		storage:        storage.InitTestingSession(t),
+	}
+
+	ndfIndex := 0
+
+	gwId, err := id.Unmarshal(testNdf.Gateways[ndfIndex].ID)
+	if err != nil {
+		t.Errorf("Failed to unmarshal ID in mock ndf: %v", err)
+	}
+
+	hostPool.addGateway(gwId, ndfIndex)
+
+	_, ok := manager.GetHost(gwId)
+	if !ok {
+		t.Errorf("Unsuccessfully added host to manager")
+	}
+}
+
+// Unit test
+func TestHostPool_RemoveGateway(t *testing.T) {
+	manager := newMockManager()
+	testNdf := getTestNdf(t)
+	newIndex := uint32(20)
+	params := DefaultPoolParams()
+	params.MaxPoolSize = uint32(len(testNdf.Gateways))
+
+	// Construct a manager (bypass business logic in constructor)
+	hostPool := &HostPool{
+		manager:        manager,
+		hostList:       make([]*connect.Host, newIndex+1),
+		hostMap:        make(map[id.ID]uint32),
+		ndf:            testNdf,
+		addGatewayChan: make(chan network.NodeGateway),
+		storage:        storage.InitTestingSession(t),
+		rng:            fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+	}
+
+	ndfIndex := 0
+
+	gwId, err := id.Unmarshal(testNdf.Gateways[ndfIndex].ID)
+	if err != nil {
+		t.Errorf("Failed to unmarshal ID in mock ndf: %v", err)
+	}
+
+	// Manually add host information
+	hostPool.addGateway(gwId, ndfIndex)
+
+	// Call the removal
+	hostPool.removeGateway(gwId)
+
+	// Check that the map and list have been updated
+	if hostPool.hostList[ndfIndex] != nil {
+		t.Errorf("Host list index was not set to nil after removal")
+	}
+
+	if _, ok := hostPool.hostMap[*gwId]; ok {
+		t.Errorf("Host map did not delete host entry")
+	}
+}
diff --git a/network/gateway/sender.go b/network/gateway/sender.go
new file mode 100644
index 0000000000000000000000000000000000000000..dcfcdbb5b88a0be02b67f4f61dfcaa01c1169489
--- /dev/null
+++ b/network/gateway/sender.go
@@ -0,0 +1,132 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2021 Privategrity Corporation                                   /
+//                                                                             /
+// All rights reserved.                                                        /
+////////////////////////////////////////////////////////////////////////////////
+
+// Contains gateway message sending wrappers
+
+package gateway
+
+import (
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/comms/network"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/xx_network/comms/connect"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/ndf"
+)
+
+// Sender Object used for sending that wraps the HostPool for providing destinations
+type Sender struct {
+	*HostPool
+}
+
+// NewSender Create a new Sender object wrapping a HostPool object
+func NewSender(poolParams PoolParams, rng *fastRNG.StreamGenerator, ndf *ndf.NetworkDefinition, getter HostManager,
+	storage *storage.Session, addGateway chan network.NodeGateway) (*Sender, error) {
+
+	hostPool, err := newHostPool(poolParams, rng, ndf, getter, storage, addGateway)
+	if err != nil {
+		return nil, err
+	}
+	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) {
+
+	proxies := s.getAny(s.poolParams.ProxyAttempts, nil)
+	for i := range proxies {
+		result, err := sendFunc(proxies[i])
+		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)
+			if err != nil {
+				jww.ERROR.Printf("Unable to checkReplace: %+v", err)
+			}
+		}
+	}
+
+	return nil, errors.Errorf("Unable to send to any proxies")
+}
+
+// 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) {
+
+	targetHosts := s.getPreferred(targets)
+	for i := range targetHosts {
+		result, err := sendFunc(targetHosts[i], targets[i])
+		if err == nil {
+			return result, nil
+		} else {
+			jww.WARN.Printf("Unable to SendToPreferred %s via %s: %s",
+				targets[i], 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)
+			}
+		}
+	}
+
+	return nil, errors.Errorf("Unable to send to any preferred")
+}
diff --git a/network/gateway/sender_test.go b/network/gateway/sender_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..4dd5d49c02ea525e547164f8c2c6392b526b30de
--- /dev/null
+++ b/network/gateway/sender_test.go
@@ -0,0 +1,249 @@
+///////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                          //
+//                                                                           //
+// Use of this source code is governed by a license that can be found in the //
+// LICENSE file                                                              //
+///////////////////////////////////////////////////////////////////////////////
+
+package gateway
+
+import (
+	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/comms/network"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/xx_network/comms/connect"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/id"
+	"reflect"
+	"testing"
+)
+
+// Unit test
+func TestNewSender(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))
+
+	_, err := NewSender(params, rng, testNdf, manager, testStorage, addGwChan)
+	if err != nil {
+		t.Fatalf("Failed to create mock sender: %v", err)
+	}
+}
+
+// Unit test
+func TestSender_SendToAny(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.PoolSize = uint32(len(testNdf.Gateways))
+
+	// 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)
+	}
+
+	// Add all gateways to hostPool's map
+	for index, gw := range testNdf.Gateways {
+		gwId, err := id.Unmarshal(gw.ID)
+		if err != nil {
+			t.Fatalf("Failed to unmarshal ID in mock ndf: %v", err)
+		}
+
+		err = sender.replaceHost(gwId, uint32(index))
+		if err != nil {
+			t.Fatalf("Failed to replace host in set-up: %v", err)
+		}
+	}
+
+	// Test sendToAny with test interfaces
+	result, err := sender.SendToAny(SendToAny_HappyPath)
+	if err != nil {
+		t.Errorf("Should not error in SendToAny happy path: %v", err)
+	}
+
+	if !reflect.DeepEqual(result, happyPathReturn) {
+		t.Errorf("Expected result not returnev via SendToAny interface."+
+			"\n\tExpected: %v"+
+			"\n\tReceived: %v", happyPathReturn, result)
+	}
+
+	_, err = sender.SendToAny(SendToAny_KnownError)
+	if err == nil {
+		t.Fatalf("Expected error path did not receive error")
+	}
+
+	_, err = sender.SendToAny(SendToAny_UnknownError)
+	if err == nil {
+		t.Fatalf("Expected error path did not receive error")
+	}
+
+}
+
+// Unit test
+func TestSender_SendToPreferred(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.PoolSize = 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.SendToPreferred([]*id.ID{preferredHost.GetId()}, SendToPreferred_HappyPath)
+	if err != nil {
+		t.Errorf("Should not error in SendToPreferred happy path: %v", err)
+	}
+
+	if !reflect.DeepEqual(result, happyPathReturn) {
+		t.Errorf("Expected result not returnev via SendToPreferred interface."+
+			"\n\tExpected: %v"+
+			"\n\tReceived: %v", happyPathReturn, result)
+	}
+
+	// Call a send which returns an error which triggers replacement
+	_, err = sender.SendToPreferred([]*id.ID{preferredHost.GetId()}, SendToPreferred_KnownError)
+	if err == nil {
+		t.Fatalf("Expected error path did not receive error")
+	}
+
+	// Check the host has been replaced
+	if _, ok := sender.hostMap[*preferredHost.GetId()]; ok {
+		t.Errorf("Expected host %s to be removed due to error", preferredHost)
+	}
+
+	// Ensure we are disconnected from the old host
+	if isConnected, _ := preferredHost.Connected(); isConnected {
+		t.Errorf("ForceReplace error: Failed to disconnect from old host %s", preferredHost)
+	}
+
+	// Get a new host to test on
+	preferredIndex = 4
+	preferredHost = sender.hostList[preferredIndex]
+
+	// Unknown error return will not trigger replacement
+	_, err = sender.SendToPreferred([]*id.ID{preferredHost.GetId()}, SendToPreferred_UnknownError)
+	if err == nil {
+		t.Fatalf("Expected error path did not receive error")
+	}
+
+	// Check the host has not been replaced
+	if _, ok := sender.hostMap[*preferredHost.GetId()]; !ok {
+		t.Errorf("Host %s should not have been removed due on an unknown error", preferredHost)
+	}
+
+	// Ensure we are disconnected from the old host
+	if isConnected, _ := preferredHost.Connected(); isConnected {
+		t.Errorf("Should not disconnect from  %s", preferredHost)
+	}
+
+}
+
+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
new file mode 100644
index 0000000000000000000000000000000000000000..0ec7dc11f8edde72a82affd3b718bcec121bf60c
--- /dev/null
+++ b/network/gateway/utils_test.go
@@ -0,0 +1,162 @@
+///////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                          //
+//                                                                           //
+// Use of this source code is governed by a license that can be found in the //
+// LICENSE file                                                              //
+///////////////////////////////////////////////////////////////////////////////
+
+package gateway
+
+import (
+	"fmt"
+	"gitlab.com/xx_network/comms/connect"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/ndf"
+)
+
+// Mock structure adhering to HostManager to be used for happy path
+type mockManager struct {
+	hosts map[string]*connect.Host
+}
+
+// Constructor for mockManager
+func newMockManager() *mockManager {
+	return &mockManager{
+		hosts: make(map[string]*connect.Host),
+	}
+}
+
+func (mhp *mockManager) GetHost(hostId *id.ID) (*connect.Host, bool) {
+	h, ok := mhp.hosts[hostId.String()]
+	return h, ok
+}
+
+func (mhp *mockManager) AddHost(hid *id.ID, address string,
+	cert []byte, params connect.HostParams) (host *connect.Host, err error) {
+	host, err = connect.NewHost(hid, address, cert, params)
+	if err != nil {
+		return nil, err
+	}
+
+	mhp.hosts[hid.String()] = host
+
+	return
+}
+
+func (mhp *mockManager) RemoveHost(hid *id.ID) {
+	delete(mhp.hosts, hid.String())
+}
+
+// Returns a mock
+func getTestNdf(face interface{}) *ndf.NetworkDefinition {
+	return &ndf.NetworkDefinition{
+		Gateways: []ndf.Gateway{{
+			ID:      id.NewIdFromUInt(0, id.Gateway, face)[:],
+			Address: "0.0.0.1",
+		}, {
+			ID:      id.NewIdFromUInt(1, id.Gateway, face)[:],
+			Address: "0.0.0.2",
+		}, {
+			ID:      id.NewIdFromUInt(2, id.Gateway, face)[:],
+			Address: "0.0.0.3",
+		}, {
+			ID:      id.NewIdFromUInt(3, id.Gateway, face)[:],
+			Address: "0.0.0.1",
+		}, {
+			ID:      id.NewIdFromUInt(4, id.Gateway, face)[:],
+			Address: "0.0.0.2",
+		}, {
+			ID:      id.NewIdFromUInt(5, id.Gateway, face)[:],
+			Address: "0.0.0.3",
+		}, {
+			ID:      id.NewIdFromUInt(6, id.Gateway, face)[:],
+			Address: "0.0.0.1",
+		}, {
+			ID:      id.NewIdFromUInt(7, id.Gateway, face)[:],
+			Address: "0.0.0.2",
+		}, {
+			ID:      id.NewIdFromUInt(8, id.Gateway, face)[:],
+			Address: "0.0.0.3",
+		}, {
+			ID:      id.NewIdFromUInt(9, id.Gateway, face)[:],
+			Address: "0.0.0.1",
+		}, {
+			ID:      id.NewIdFromUInt(10, id.Gateway, face)[:],
+			Address: "0.0.0.2",
+		}, {
+			ID:      id.NewIdFromUInt(11, id.Gateway, face)[:],
+			Address: "0.0.0.3",
+		}},
+		Nodes: []ndf.Node{{
+			ID:      id.NewIdFromUInt(0, id.Node, face)[:],
+			Address: "0.0.0.1",
+		}, {
+			ID:      id.NewIdFromUInt(1, id.Node, face)[:],
+			Address: "0.0.0.2",
+		}, {
+			ID:      id.NewIdFromUInt(2, id.Node, face)[:],
+			Address: "0.0.0.3",
+		}, {
+			ID:      id.NewIdFromUInt(3, id.Node, face)[:],
+			Address: "0.0.0.1",
+		}, {
+			ID:      id.NewIdFromUInt(4, id.Node, face)[:],
+			Address: "0.0.0.2",
+		}, {
+			ID:      id.NewIdFromUInt(5, id.Node, face)[:],
+			Address: "0.0.0.3",
+		}, {
+			ID:      id.NewIdFromUInt(6, id.Node, face)[:],
+			Address: "0.0.0.1",
+		}, {
+			ID:      id.NewIdFromUInt(7, id.Node, face)[:],
+			Address: "0.0.0.2",
+		}, {
+			ID:      id.NewIdFromUInt(8, id.Node, face)[:],
+			Address: "0.0.0.3",
+		}, {
+			ID:      id.NewIdFromUInt(9, id.Node, face)[:],
+			Address: "0.0.0.1",
+		}, {
+			ID:      id.NewIdFromUInt(10, id.Node, face)[:],
+			Address: "0.0.0.2",
+		}, {
+			ID:      id.NewIdFromUInt(11, id.Node, face)[:],
+			Address: "0.0.0.3",
+		}},
+	}
+}
+
+const happyPathReturn = "happyPathReturn"
+
+func SendToPreferred_HappyPath(host *connect.Host, target *id.ID) (interface{}, error) {
+	return happyPathReturn, nil
+}
+
+func SendToPreferred_KnownError(host *connect.Host, target *id.ID) (interface{}, error) {
+	return nil, fmt.Errorf(errorsList[0])
+}
+
+func SendToPreferred_UnknownError(host *connect.Host, target *id.ID) (interface{}, error) {
+	return nil, fmt.Errorf("Unexpected error: Oopsie")
+}
+
+func SendToAny_HappyPath(host *connect.Host) (interface{}, error) {
+	return happyPathReturn, nil
+}
+
+func SendToAny_KnownError(host *connect.Host) (interface{}, error) {
+	return nil, fmt.Errorf(errorsList[0])
+}
+
+func SendToAny_UnknownError(host *connect.Host) (interface{}, error) {
+	return nil, fmt.Errorf("Unexpected error: Oopsie")
+}
+
+func SendToSpecific_HappyPath(host *connect.Host, target *id.ID) (interface{}, bool, error) {
+	return happyPathReturn, false, nil
+}
+
+func SendToSpecific_Abort(host *connect.Host, target *id.ID) (interface{}, bool, error) {
+	return nil, true, fmt.Errorf(errorsList[0])
+}
diff --git a/network/manager.go b/network/manager.go
index 76fdeb0b5559fe0fc64bf6e8bf4b8f3aaa7eca29..e2b6c2c0c05d2d5ce48f846d09eb1f765e52dba5 100644
--- a/network/manager.go
+++ b/network/manager.go
@@ -15,6 +15,7 @@ import (
 	"gitlab.com/elixxir/client/interfaces"
 	"gitlab.com/elixxir/client/interfaces/params"
 	"gitlab.com/elixxir/client/network/ephemeral"
+	"gitlab.com/elixxir/client/network/gateway"
 	"gitlab.com/elixxir/client/network/health"
 	"gitlab.com/elixxir/client/network/internal"
 	"gitlab.com/elixxir/client/network/message"
@@ -35,6 +36,8 @@ import (
 type manager struct {
 	// parameters of the network
 	param params.Network
+	// handles message sending
+	sender *gateway.Sender
 
 	//Shared data with all sub managers
 	internal.Internal
@@ -43,11 +46,8 @@ type manager struct {
 	round   *rounds.Manager
 	message *message.Manager
 
-	//map of polls for debugging
-	tracker *pollTracker
-
-	//tracks already checked rounds
-	checked *checkedRounds
+	//number of polls done in a period of time
+	tracker *uint64
 }
 
 // NewManager builds a new reception manager object using inputted key fields
@@ -67,11 +67,12 @@ func NewManager(session *storage.Session, switchboard *switchboard.Switchboard,
 	// set them here when they are needed on startup
 	session.E2e().SetE2ESessionParams(params.E2EParams)
 
+	tracker := uint64(0)
+
 	//create manager object
 	m := manager{
 		param:   params,
-		tracker: newPollTracker(),
-		checked: newCheckedRounds(),
+		tracker: &tracker,
 	}
 
 	m.Internal = internal.Internal{
@@ -86,18 +87,22 @@ func NewManager(session *storage.Session, switchboard *switchboard.Switchboard,
 		ReceptionID:      session.User().GetCryptographicIdentity().GetReceptionID(),
 	}
 
-	// register the node registration channel early so login connection updates
-	// get triggered for registration if necessary
-	instance.SetAddGatewayChan(m.NodeRegistration)
+	// Set up gateway.Sender
+	poolParams := gateway.DefaultPoolParams()
+	m.sender, err = gateway.NewSender(poolParams, rng,
+		ndf, comms, session, m.NodeRegistration)
+	if err != nil {
+		return nil, err
+	}
 
 	//create sub managers
-	m.message = message.NewManager(m.Internal, m.param.Messages, m.NodeRegistration)
-	m.round = rounds.NewManager(m.Internal, m.param.Rounds, m.message.GetMessageReceptionChannel())
+	m.message = message.NewManager(m.Internal, m.param.Messages, m.NodeRegistration, m.sender)
+	m.round = rounds.NewManager(m.Internal, m.param.Rounds, m.message.GetMessageReceptionChannel(), m.sender)
 
 	return &m, nil
 }
 
-// StartRunners kicks off all network reception goroutines ("threads").
+// Follow StartRunners kicks off all network reception goroutines ("threads").
 // Started Threads are:
 //   - Network Follower (/network/follow.go)
 //   - Historical Round Retrieval (/network/rounds/historical.go)
@@ -118,7 +123,7 @@ func (m *manager) Follow(report interfaces.ClientErrorReport) (stoppable.Stoppab
 	multi.Add(healthStop)
 
 	// Node Updates
-	multi.Add(node.StartRegistration(m.Instance, m.Session, m.Rng,
+	multi.Add(node.StartRegistration(m.GetSender(), m.Session, m.Rng,
 		m.Comms, m.NodeRegistration, m.param.ParallelNodeRegistrations)) // Adding/Keys
 	//TODO-remover
 	//m.runners.Add(StartNodeRemover(m.Context))        // Removing
@@ -149,7 +154,12 @@ func (m *manager) GetInstance() *network.Instance {
 	return m.Instance
 }
 
-// triggers a check on garbled messages to see if they can be decrypted
+// GetSender returns the gateway.Sender object
+func (m *manager) GetSender() *gateway.Sender {
+	return m.sender
+}
+
+// CheckGarbledMessages triggers a check on garbled messages to see if they can be decrypted
 // this should be done when a new e2e client is added in case messages were
 // received early or arrived out of order
 func (m *manager) CheckGarbledMessages() {
diff --git a/network/message/critical.go b/network/message/critical.go
index 0ebaed5d32390c35f92a6a96557782be6336ff1e..1dad92c821e756e3c89c6d013c5d78c9e09a1015 100644
--- a/network/message/critical.go
+++ b/network/message/critical.go
@@ -95,7 +95,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(msg, rid, param)
+			round, _, err := m.SendCMIX(m.sender, msg, rid, param)
 			//if the message fail to send, notify the buffer so it can be handled
 			//in the future and exit
 			if err != nil {
diff --git a/network/message/garbled_test.go b/network/message/garbled_test.go
index 0f5c3a074641af02ef3e156372bd7957eedba5aa..9d021ae488ed26717aade2d76d87ba688c7f9001 100644
--- a/network/message/garbled_test.go
+++ b/network/message/garbled_test.go
@@ -4,6 +4,7 @@ import (
 	"encoding/binary"
 	"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/network/message/parse"
 	"gitlab.com/elixxir/client/storage"
@@ -57,12 +58,18 @@ func TestManager_CheckGarbledMessages(t *testing.T) {
 		Instance:         nil,
 		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(err.Error())
+	}
 	m := NewManager(i, params.Messages{
 		MessageReceptionBuffLen:        20,
 		MessageReceptionWorkerPoolSize: 20,
 		MaxChecksGarbledMessage:        20,
 		GarbledMessageWait:             time.Hour,
-	}, nil)
+	}, nil, sender)
 
 	e2ekv := i.Session.E2e()
 	err = e2ekv.AddPartner(sess2.GetUser().TransmissionID, sess2.E2e().GetDHPublicKey(), e2ekv.GetDHPrivateKey(),
diff --git a/network/message/manager.go b/network/message/manager.go
index 807ccc07066cf2281e557b6e07d09dd4180d3c15..7728910aa7e62186c682dcb97b297cb470dcac58 100644
--- a/network/message/manager.go
+++ b/network/message/manager.go
@@ -10,6 +10,7 @@ package message
 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/parse"
 	"gitlab.com/elixxir/client/stoppable"
@@ -21,6 +22,7 @@ type Manager struct {
 	param       params.Messages
 	partitioner parse.Partitioner
 	internal.Internal
+	sender *gateway.Sender
 
 	messageReception chan Bundle
 	nodeRegistration chan network.NodeGateway
@@ -29,7 +31,7 @@ type Manager struct {
 }
 
 func NewManager(internal internal.Internal, param params.Messages,
-	nodeRegistration chan network.NodeGateway) *Manager {
+	nodeRegistration chan network.NodeGateway, sender *gateway.Sender) *Manager {
 	dummyMessage := format.NewMessage(internal.Session.Cmix().GetGroup().GetP().ByteLen())
 	m := Manager{
 		param:            param,
@@ -38,6 +40,7 @@ func NewManager(internal internal.Internal, param params.Messages,
 		networkIsHealthy: make(chan bool, 1),
 		triggerGarbled:   make(chan struct{}, 100),
 		nodeRegistration: nodeRegistration,
+		sender:           sender,
 	}
 	m.Internal = internal
 	return &m
diff --git a/network/message/sendCmix.go b/network/message/sendCmix.go
index 1aced6efe44eaef52f17a08392d4c52a8d6eae7c..a3659870b8489cc3ab42e322ae5d6a79f73a54d0 100644
--- a/network/message/sendCmix.go
+++ b/network/message/sendCmix.go
@@ -12,6 +12,7 @@ import (
 	"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"
@@ -29,7 +30,6 @@ import (
 
 // interface for SendCMIX comms; allows mocking this in testing
 type sendCmixCommsInterface interface {
-	GetHost(hostId *id.ID) (*connect.Host, bool)
 	SendPutMessage(host *connect.Host, message *pb.GatewaySlot) (*pb.GatewaySlotResponse, error)
 }
 
@@ -38,9 +38,9 @@ const sendTimeBuffer = 2500 * time.Millisecond
 
 // WARNING: Potentially Unsafe
 // Public manager function to send a message over CMIX
-func (m *Manager) SendCMIX(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, param params.CMIX) (id.Round, ephemeral.Id, error) {
 	msgCopy := msg.Copy()
-	return sendCmixHelper(msgCopy, recipient, param, m.Instance, m.Session, m.nodeRegistration, m.Rng, m.TransmissionID, m.Comms)
+	return sendCmixHelper(sender, msgCopy, recipient, param, m.Instance, m.Session, m.nodeRegistration, m.Rng, m.TransmissionID, m.Comms)
 }
 
 // Payloads send are not End to End encrypted, MetaData is NOT protected with
@@ -51,7 +51,7 @@ func (m *Manager) SendCMIX(msg format.Message, recipient *id.ID, param params.CM
 // 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(msg format.Message, recipient *id.ID, param params.CMIX, instance *network.Instance,
+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) {
 
@@ -142,15 +142,6 @@ func sendCmixHelper(msg format.Message, recipient *id.ID, param params.CMIX, ins
 		firstGateway := topology.GetNodeAtIndex(0).DeepCopy()
 		firstGateway.SetType(id.Gateway)
 
-		transmitGateway, ok := comms.GetHost(firstGateway)
-		if !ok {
-			jww.ERROR.Printf("Failed to get host for gateway %s when "+
-				"sending to %s (msgDigest: %s)", transmitGateway, recipient,
-				msg.Digest())
-			time.Sleep(param.RetryDelay)
-			continue
-		}
-
 		//encrypt the message
 		stream = rng.GetStream()
 		salt := make([]byte, 32)
@@ -187,46 +178,57 @@ func sendCmixHelper(msg format.Message, recipient *id.ID, param params.CMIX, ins
 		jww.INFO.Printf("Sending to EphID %d (%s) on round %d, "+
 			"(msgDigest: %s, ecrMsgDigest: %s) via gateway %s",
 			ephID.Int64(), recipient, bestRound.ID, msg.Digest(),
-			encMsg.Digest(), transmitGateway.GetId())
-		//		//Send the payload
-		gwSlotResp, err := comms.SendPutMessage(transmitGateway, wrappedMsg)
+			encMsg.Digest(), firstGateway.String())
+
+		// Send the payload
+		result, err := sender.SendToSpecific(firstGateway, 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
+				}
+			}
+			return result, false, err
+		})
+
 		//if the comm errors or the message fails to send, continue retrying.
 		//return if it sends properly
 		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)
-				continue
-			} 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(), transmitGateway.GetId(), err)
-				//if we failed to send due to the gateway not recognizing our
-				// authorization, renegotiate with the node to refresh it
-				nodeID := transmitGateway.GetId().DeepCopy()
-				nodeID.SetType(id.Node)
-				//delete the keys
-				session.Cmix().Remove(nodeID)
-				//trigger
-				go handleMissingNodeKeys(instance, nodeRegistration, []*id.ID{nodeID})
-				continue
-			}
 			jww.ERROR.Printf("Failed to send to EphID %d (%s) on "+
-				"round %d, bailing: %+v", ephID.Int64(), recipient,
+				"round %d, trying a new round: %+v", ephID.Int64(), recipient,
 				bestRound.ID, err)
-			return 0, ephemeral.Id{}, errors.WithMessage(err, "Failed to put cmix message")
-		} else if gwSlotResp.Accepted {
+			continue
+		}
+
+		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)
 			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",
-				transmitGateway.GetId(), ephID.Int64(), recipient, bestRound.ID)
+				firstGateway.String(), ephID.Int64(), recipient, bestRound.ID)
 		}
 	}
 	return 0, ephemeral.Id{}, errors.New("failed to send the message, " +
diff --git a/network/message/sendCmix_test.go b/network/message/sendCmix_test.go
index 4b801679c0371c8ab4294a69c7e27883ef78801d..44a123b65d681ab6a46f9f696c2ef0e6ad08cc33 100644
--- a/network/message/sendCmix_test.go
+++ b/network/message/sendCmix_test.go
@@ -1,8 +1,10 @@
 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"
@@ -40,6 +42,16 @@ func (mc *MockSendCMIXComms) GetHost(hostId *id.ID) (*connect.Host, bool) {
 	})
 	return h, true
 }
+
+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,
@@ -115,24 +127,36 @@ func Test_attemptSendCmix(t *testing.T) {
 		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.Messages{
 		MessageReceptionBuffLen:        20,
 		MessageReceptionWorkerPoolSize: 20,
 		MaxChecksGarbledMessage:        20,
 		GarbledMessageWait:             time.Hour,
-	}, nil)
+	}, 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(msgCmix, sess2.GetUser().ReceptionID, params.GetDefaultCMIX(),
+	_, _, err = sendCmixHelper(sender, msgCmix, sess2.GetUser().ReceptionID, 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)
+		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" +
@@ -168,5 +192,19 @@ func getNDF() *ndf.NetworkDefinition {
 				"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 78061232830f167adf91d21f5805fa2e00b58916..16c14701d47ead025dae9758fa4697ce6c5ea007 100644
--- a/network/message/sendE2E.go
+++ b/network/message/sendE2E.go
@@ -95,7 +95,7 @@ func (m *Manager) SendE2E(msg message.Send, param params.E2E) ([]id.Round, e2e.M
 		wg.Add(1)
 		go func(i int) {
 			var err error
-			roundIds[i], _, err = m.SendCMIX(msgEnc, msg.Recipient,
+			roundIds[i], _, err = m.SendCMIX(m.sender, msgEnc, msg.Recipient,
 				param.CMIX)
 			if err != nil {
 				errCh <- err
diff --git a/network/message/sendUnsafe.go b/network/message/sendUnsafe.go
index ef28cf2d789d1f5f0a097413046b5a0c50b0a230..df7f05cb8df4ba368f3a3a19368dcb13214f7d49 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(msgCmix, msg.Recipient, param.CMIX)
+			roundIds[i], _, err = m.SendCMIX(m.sender, msgCmix, msg.Recipient, param.CMIX)
 			if err != nil {
 				errCh <- err
 			}
diff --git a/network/node/register.go b/network/node/register.go
index a27529b6eb612e9c3ae49a4e07393dbe67d8c347..c89d2dd55371bef074a3d417f5ca05ae66d4e550 100644
--- a/network/node/register.go
+++ b/network/node/register.go
@@ -13,6 +13,7 @@ import (
 	"fmt"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/network/gateway"
 	"gitlab.com/elixxir/client/stoppable"
 	"gitlab.com/elixxir/client/storage"
 	"gitlab.com/elixxir/client/storage/cmix"
@@ -33,14 +34,13 @@ import (
 )
 
 type RegisterNodeCommsInterface interface {
-	GetHost(hostId *id.ID) (*connect.Host, bool)
 	SendRequestNonceMessage(host *connect.Host,
 		message *pb.NonceRequest) (*pb.Nonce, error)
 	SendConfirmNonceMessage(host *connect.Host,
 		message *pb.RequestRegistrationConfirmation) (*pb.RegistrationConfirmation, error)
 }
 
-func StartRegistration(instance *network.Instance, session *storage.Session, rngGen *fastRNG.StreamGenerator, comms RegisterNodeCommsInterface,
+func StartRegistration(sender *gateway.Sender, session *storage.Session, rngGen *fastRNG.StreamGenerator, comms RegisterNodeCommsInterface,
 	c chan network.NodeGateway, numParallel uint) stoppable.Stoppable {
 
 	multi := stoppable.NewMulti("NodeRegistrations")
@@ -48,14 +48,14 @@ func StartRegistration(instance *network.Instance, session *storage.Session, rng
 	for i := uint(0); i < numParallel; i++ {
 		stop := stoppable.NewSingle(fmt.Sprintf("NodeRegistration %d", i))
 
-		go registerNodes(session, rngGen, comms, stop, c)
+		go registerNodes(sender, session, rngGen, comms, stop, c)
 		multi.Add(stop)
 	}
 
 	return multi
 }
 
-func registerNodes(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()
@@ -71,7 +71,7 @@ func registerNodes(session *storage.Session, rngGen *fastRNG.StreamGenerator, co
 			t.Stop()
 			return
 		case gw := <-c:
-			err := registerWithNode(comms, gw, regSignature, uci, cmix, rng)
+			err := registerWithNode(sender, comms, gw, regSignature, uci, cmix, rng)
 			if err != nil {
 				jww.ERROR.Printf("Failed to register node: %+v", err)
 			}
@@ -82,7 +82,7 @@ func registerNodes(session *storage.Session, rngGen *fastRNG.StreamGenerator, co
 
 //registerWithNode serves as a helper for RegisterWithNodes
 // It registers a user with a specific in the client's ndf.
-func registerWithNode(comms RegisterNodeCommsInterface, ngw network.NodeGateway, regSig []byte,
+func registerWithNode(sender *gateway.Sender, comms RegisterNodeCommsInterface, ngw network.NodeGateway, regSig []byte,
 	uci *user.CryptographicIdentity, store *cmix.Store, rng csprng.Source) error {
 	nodeID, err := ngw.Node.GetNodeId()
 	if err != nil {
@@ -109,7 +109,7 @@ func registerWithNode(comms RegisterNodeCommsInterface, ngw network.NodeGateway,
 		userNum := int(uci.GetTransmissionID().Bytes()[7])
 		h := sha256.New()
 		h.Reset()
-		h.Write([]byte(strconv.Itoa(int(4000 + userNum))))
+		h.Write([]byte(strconv.Itoa(4000 + userNum)))
 
 		transmissionKey = store.GetGroup().NewIntFromBytes(h.Sum(nil))
 		jww.INFO.Printf("transmissionKey: %v", transmissionKey.Bytes())
@@ -118,7 +118,7 @@ func registerWithNode(comms RegisterNodeCommsInterface, ngw network.NodeGateway,
 		// keys
 		transmissionHash, _ := hash.NewCMixHash()
 
-		nonce, dhPub, err := requestNonce(comms, gatewayID, regSig, uci, store, rng)
+		nonce, dhPub, err := requestNonce(sender, comms, gatewayID, regSig, uci, store, rng)
 		if err != nil {
 			return errors.Errorf("Failed to request nonce: %+v", err)
 		}
@@ -127,8 +127,8 @@ func registerWithNode(comms RegisterNodeCommsInterface, ngw network.NodeGateway,
 		serverPubDH := store.GetGroup().NewIntFromBytes(dhPub)
 
 		// Confirm received nonce
-		jww.INFO.Println("Register: Confirming received nonce")
-		err = confirmNonce(comms, uci.GetTransmissionID().Bytes(),
+		jww.INFO.Printf("Register: Confirming received nonce from node %s", nodeID.String())
+		err = confirmNonce(sender, comms, uci.GetTransmissionID().Bytes(),
 			nonce, uci.GetTransmissionRSA(), gatewayID)
 		if err != nil {
 			errMsg := fmt.Sprintf("Register: Unable to confirm nonce: %v", err)
@@ -145,7 +145,7 @@ func registerWithNode(comms RegisterNodeCommsInterface, ngw network.NodeGateway,
 	return nil
 }
 
-func requestNonce(comms RegisterNodeCommsInterface, gwId *id.ID, regHash []byte,
+func requestNonce(sender *gateway.Sender, comms RegisterNodeCommsInterface, gwId *id.ID, regHash []byte,
 	uci *user.CryptographicIdentity, store *cmix.Store, rng csprng.Source) ([]byte, []byte, error) {
 	dhPub := store.GetDHPublicKey().Bytes()
 	opts := rsa.NewDefaultOptions()
@@ -162,34 +162,37 @@ func requestNonce(comms RegisterNodeCommsInterface, gwId *id.ID, regHash []byte,
 	}
 
 	// Request nonce message from gateway
-	jww.INFO.Printf("Register: Requesting nonce from gateway %v",
-		gwId.Bytes())
-
-	host, ok := comms.GetHost(gwId)
-	if !ok {
-		return nil, nil, errors.Errorf("Failed to find host with ID %s", gwId.String())
-	}
-	nonceResponse, err := comms.SendRequestNonceMessage(host,
-		&pb.NonceRequest{
-			Salt:            uci.GetTransmissionSalt(),
-			ClientRSAPubKey: string(rsa.CreatePublicKeyPem(uci.GetTransmissionRSA().GetPublic())),
-			ClientSignedByServer: &messages.RSASignature{
-				Signature: regHash,
-			},
-			ClientDHPubKey: dhPub,
-			RequestSignature: &messages.RSASignature{
-				Signature: clientSig,
-			},
-		})
-
+	jww.INFO.Printf("Register: Requesting nonce from gateway %v", gwId.String())
+
+	result, err := sender.SendToAny(func(host *connect.Host) (interface{}, error) {
+		nonceResponse, err := comms.SendRequestNonceMessage(host,
+			&pb.NonceRequest{
+				Salt:            uci.GetTransmissionSalt(),
+				ClientRSAPubKey: string(rsa.CreatePublicKeyPem(uci.GetTransmissionRSA().GetPublic())),
+				ClientSignedByServer: &messages.RSASignature{
+					Signature: regHash,
+				},
+				ClientDHPubKey: dhPub,
+				RequestSignature: &messages.RSASignature{
+					Signature: clientSig,
+				},
+				Target: gwId.Marshal(),
+			})
+		if err != nil {
+			errMsg := fmt.Sprintf("Register: Failed requesting nonce from gateway: %+v", err)
+			return nil, errors.New(errMsg)
+		}
+		if nonceResponse.Error != "" {
+			err := errors.New(fmt.Sprintf("requestNonce: nonceResponse error: %s", nonceResponse.Error))
+			return nil, err
+		}
+		return nonceResponse, nil
+	})
 	if err != nil {
-		errMsg := fmt.Sprintf("Register: Failed requesting nonce from gateway: %+v", err)
-		return nil, nil, errors.New(errMsg)
-	}
-	if nonceResponse.Error != "" {
-		err := errors.New(fmt.Sprintf("requestNonce: nonceResponse error: %s", nonceResponse.Error))
 		return nil, nil, err
 	}
+	nonceResponse := result.(*pb.Nonce)
+
 	// Use Client keypair to sign Server nonce
 	return nonceResponse.Nonce, nonceResponse.DHPubKey, nil
 }
@@ -197,7 +200,7 @@ func requestNonce(comms RegisterNodeCommsInterface, gwId *id.ID, regHash []byte,
 // 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(comms RegisterNodeCommsInterface, UID, nonce []byte,
+func confirmNonce(sender *gateway.Sender, comms RegisterNodeCommsInterface, UID, nonce []byte,
 	privateKeyRSA *rsa.PrivateKey, gwID *id.ID) error {
 	opts := rsa.NewDefaultOptions()
 	opts.Hash = hash.CMixHash
@@ -224,22 +227,19 @@ func confirmNonce(comms RegisterNodeCommsInterface, UID, nonce []byte,
 		NonceSignedByClient: &messages.RSASignature{
 			Signature: sig,
 		},
+		Target: gwID.Marshal(),
 	}
 
-	host, ok := comms.GetHost(gwID)
-	if !ok {
-		return errors.Errorf("Failed to find host with ID %s", gwID.String())
-	}
-	confirmResponse, err := comms.SendConfirmNonceMessage(host, msg)
-	if err != nil {
-		err := errors.New(fmt.Sprintf(
-			"confirmNonce: Unable to send signed nonce! %s", err))
-		return err
-	}
-	if confirmResponse.Error != "" {
-		err := errors.New(fmt.Sprintf(
-			"confirmNonce: Error confirming nonce: %s", confirmResponse.Error))
-		return err
-	}
-	return nil
+	_, err = sender.SendToAny(func(host *connect.Host) (interface{}, error) {
+		confirmResponse, err := comms.SendConfirmNonceMessage(host, msg)
+		if err != nil {
+			return nil, err
+		} else if confirmResponse.Error != "" {
+			err := errors.New(fmt.Sprintf(
+				"confirmNonce: Error confirming nonce: %s", confirmResponse.Error))
+			return nil, err
+		}
+		return confirmResponse, nil
+	})
+	return err
 }
diff --git a/network/polltracker.go b/network/polltracker.go
index 48574c7d8e19eb4e1d73090af0c017e3c9b8187b..63739e19afbb1a959face8c7859f23c704a7cb66 100644
--- a/network/polltracker.go
+++ b/network/polltracker.go
@@ -13,7 +13,7 @@ func newPollTracker() *pollTracker {
 	return &pt
 }
 
-//tracks a single poll
+// Track tracks a single poll
 func (pt *pollTracker) Track(ephID ephemeral.Id, source *id.ID) {
 	if _, exists := (*pt)[*source]; !exists {
 		(*pt)[*source] = make(map[int64]uint)
@@ -25,7 +25,7 @@ func (pt *pollTracker) Track(ephID ephemeral.Id, source *id.ID) {
 	}
 }
 
-//reports all resent polls
+// Report reports all recent polls
 func (pt *pollTracker) Report() string {
 	report := ""
 	numReports := uint(0)
diff --git a/network/rounds/check.go b/network/rounds/check.go
index a22bafd8e7931ecbbe673b2df1c071bad4fb329c..03cd83718495b71ef5455e9112dfe95fdda89fe3 100644
--- a/network/rounds/check.go
+++ b/network/rounds/check.go
@@ -11,6 +11,7 @@ import (
 	"encoding/binary"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/storage/reception"
+	"gitlab.com/elixxir/client/storage/rounds"
 	"gitlab.com/xx_network/primitives/id"
 )
 
@@ -27,7 +28,12 @@ import (
 // Retrieval
 // false: no message
 // true: message
-func Checker(roundID id.Round, filters []*RemoteFilter) bool {
+func Checker(roundID id.Round, filters []*RemoteFilter, cr *rounds.CheckedRounds) bool {
+	// Skip checking if the round is already checked
+	if cr.IsChecked(roundID) {
+		return true
+	}
+
 	//find filters that could have the round and check them
 	serialRid := serializeRound(roundID)
 	for _, filter := range filters {
diff --git a/network/rounds/historical.go b/network/rounds/historical.go
index 6d2d76de30a44151e55cecc863f2113eb2a7a3fb..45aed7fdfaa7b296a46de79645e19fd010f88634 100644
--- a/network/rounds/historical.go
+++ b/network/rounds/historical.go
@@ -9,7 +9,6 @@ package rounds
 
 import (
 	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/client/network/gateway"
 	"gitlab.com/elixxir/client/storage/reception"
 	pb "gitlab.com/elixxir/comms/mixmessages"
 	"gitlab.com/xx_network/comms/connect"
@@ -87,13 +86,6 @@ func (m *Manager) processHistoricalRounds(comm historicalRoundsComms, quitCh <-c
 			continue
 		}
 
-		//find a gateway to request about the roundRequests
-		gwHost, err := gateway.Get(m.Instance.GetPartialNdf().Get(), comm, rng)
-		if err != nil {
-			jww.FATAL.Panicf("Failed to track network, NDF has corrupt "+
-				"data: %s", err)
-		}
-
 		rounds := make([]uint64, len(roundRequests))
 		for i, rr := range roundRequests {
 			rounds[i] = uint64(rr.rid)
@@ -104,18 +96,21 @@ func (m *Manager) processHistoricalRounds(comm historicalRoundsComms, quitCh <-c
 			Rounds: rounds,
 		}
 
-		jww.DEBUG.Printf("Requesting Historical rounds %v from "+
-			"gateway %s", rounds, gwHost.GetId())
+		result, err := m.sender.SendToAny(func(host *connect.Host) (interface{}, error) {
+			jww.DEBUG.Printf("Requesting Historical rounds %v from "+
+				"gateway %s", rounds, host.GetId())
+			return comm.RequestHistoricalRounds(host, hr)
+		})
 
-		response, err := comm.RequestHistoricalRounds(gwHost, hr)
 		if err != nil {
 			jww.ERROR.Printf("Failed to request historical roundRequests "+
-				"data for rounds %v: %s", rounds, response)
+				"data for rounds %v: %s", rounds, err)
 			// if the check fails to resolve, break the loop and so they will be
 			// checked again
 			timerCh = time.NewTimer(m.params.HistoricalRoundsPeriod).C
 			continue
 		}
+		response := result.(*pb.HistoricalRoundsResponse)
 
 		// process the returned historical roundRequests.
 		for i, roundInfo := range response.Rounds {
diff --git a/network/rounds/manager.go b/network/rounds/manager.go
index 6e94ef391a3812af4029298d225ec74ccf03e4aa..942e86319efe8ab05711b901360edbcd37865978 100644
--- a/network/rounds/manager.go
+++ b/network/rounds/manager.go
@@ -10,6 +10,7 @@ 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"
@@ -19,6 +20,7 @@ type Manager struct {
 	params params.Rounds
 
 	internal.Internal
+	sender *gateway.Sender
 
 	historicalRounds    chan historicalRoundRequest
 	lookupRoundMessages chan roundLookup
@@ -26,13 +28,14 @@ type Manager struct {
 }
 
 func NewManager(internal internal.Internal, params params.Rounds,
-	bundles chan<- message.Bundle) *Manager {
+	bundles chan<- message.Bundle, sender *gateway.Sender) *Manager {
 	m := &Manager{
 		params: params,
 
 		historicalRounds:    make(chan historicalRoundRequest, params.HistoricalRoundsBufferLen),
 		lookupRoundMessages: make(chan roundLookup, params.LookupRoundsBufferLen),
 		messageBundles:      bundles,
+		sender:              sender,
 	}
 
 	m.Internal = internal
diff --git a/network/rounds/retrieve.go b/network/rounds/retrieve.go
index effd66b7fcac84db95255cc458c997a36d134f2b..0d90355c64bb2adaff003442737e09468a5b05f8 100644
--- a/network/rounds/retrieve.go
+++ b/network/rounds/retrieve.go
@@ -10,7 +10,6 @@ package rounds
 import (
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/client/network/gateway"
 	"gitlab.com/elixxir/client/network/message"
 	"gitlab.com/elixxir/client/storage/reception"
 	pb "gitlab.com/elixxir/comms/mixmessages"
@@ -44,52 +43,35 @@ func (m *Manager) processMessageRetrieval(comms messageRetrievalComms,
 			done = true
 		case rl := <-m.lookupRoundMessages:
 			ri := rl.roundInfo
-			var bundle message.Bundle
 
-			// Get a shuffled list of gateways in the round
-			gwHosts, err := gateway.GetAllShuffled(comms, ri)
-			if err != nil {
-				jww.WARN.Printf("Failed to get gateway hosts from "+
-					"round %v, not requesting from them",
-					ri.ID)
-				break
-			}
-
-			// Attempt to request messages for every gateway in the list.
-			// If we retrieve without error, then we exit. If we error, then
-			// we retry with the next gateway in the list until we exhaust the list
-			for i, gwHost := range gwHosts {
-				// Attempt to request for this gateway
-				bundle, err = m.getMessagesFromGateway(id.Round(ri.ID), rl.identity, comms, gwHost)
+			// Convert gateways in round to proper ID format
+			gwIds := make([]*id.ID, len(ri.Topology))
+			for i, idBytes := range ri.Topology {
+				gwId, err := id.Unmarshal(idBytes)
 				if err != nil {
-
-					jww.WARN.Printf("Failed on gateway [%d/%d] to get messages for round %v",
-						i, len(gwHosts), ri.ID)
-
-					// Retry for the next gateway in the list
-					continue
+					jww.FATAL.Panicf("processMessageRetrieval: Unable to unmarshal: %+v", err)
 				}
-
-				// If a non-error request, no longer retry
-				break
-
-			}
-			gwIDs := make([]*id.ID, 0)
-			for _, gwHost := range gwHosts {
-				gwIDs = append(gwIDs, gwHost.GetId())
+				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
 			if err != nil {
 				jww.ERROR.Printf("Failed to get pickup round %d "+
-					"from all gateways (%v): final gateway %s returned : %s",
-					id.Round(ri.ID), gwIDs, gwHosts[len(gwHosts)-1].GetId(), err)
-			} else if len(bundle.Messages) != 0 {
+					"from all gateways (%v): %s",
+					id.Round(ri.ID), gwIds, err)
+			}
+
+			if len(bundle.Messages) != 0 {
 				// If successful and there are messages, we send them to another thread
 				bundle.Identity = rl.identity
 				m.messageBundles <- bundle
 			}
+
 		}
 	}
 }
@@ -97,40 +79,49 @@ 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, gwHost *connect.Host) (message.Bundle, error) {
+	comms messageRetrievalComms, gwIds []*id.ID) (message.Bundle, error) {
+
+	// Send to the gateways using backup proxies
+	result, err := m.sender.SendToPreferred(gwIds, func(host *connect.Host, target *id.ID) (interface{}, 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())
+
+		// send the request
+		msgReq := &pb.GetMessages{
+			ClientID: identity.EphId[:],
+			RoundID:  uint64(roundID),
+			Target:   target.Marshal(),
+		}
 
-	jww.DEBUG.Printf("Trying to get messages for round %v for ephmeralID %d (%v)  "+
-		"via Gateway: %s", roundID, identity.EphId.Int64(), identity.Source.String(), gwHost.GetId())
+		// 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)
+		}
+
+		return msgResp, err
+	})
 
-	// send the request
-	msgReq := &pb.GetMessages{
-		ClientID: identity.EphId[:],
-		RoundID:  uint64(roundID),
-	}
-	msgResp, err := comms.RequestMessages(gwHost, msgReq)
 	// 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 "+
-			"request messages from %s for round %d", gwHost.GetId(), roundID)
-	}
-	// if the gateway doesnt have the round, return an error
-	if !msgResp.GetHasRound() {
-		return message.Bundle{}, errors.Errorf(noRoundError)
+			"request messages for round %d", roundID)
 	}
+	msgResp := result.(*pb.GetMessagesResponse)
 
 	// If there are no messages print a warning. Due to the probabilistic nature
 	// of the bloom filters, false positives will happen some times
 	msgs := msgResp.GetMessages()
 	if msgs == nil || len(msgs) == 0 {
-		jww.WARN.Printf("host %s has no messages for client %s "+
+		jww.WARN.Printf("no messages for client %s "+
 			" in round %d. This happening every once in a while is normal,"+
-			" but can be indicitive of a problem if it is consistant", gwHost,
+			" but can be indicative of a problem if it is consistent",
 			m.TransmissionID, roundID)
 		return message.Bundle{}, nil
 	}
 
-	jww.INFO.Printf("Received %d messages in Round %v via Gateway %s for %d (%s)",
-		len(msgs), roundID, gwHost.GetId(), identity.EphId.Int64(), identity.Source)
+	jww.INFO.Printf("Received %d messages in Round %v for %d (%s)",
+		len(msgs), roundID, identity.EphId.Int64(), identity.Source)
 
 	//build the bundle of messages to send to the message processor
 	bundle := message.Bundle{
diff --git a/network/rounds/retrieve_test.go b/network/rounds/retrieve_test.go
index 2efd2c89c6865ba335d2e167cab343bb786370db..abd79533dd9189cdef93f8f37aba4b8ad810b8d2 100644
--- a/network/rounds/retrieve_test.go
+++ b/network/rounds/retrieve_test.go
@@ -8,11 +8,15 @@ package rounds
 
 import (
 	"bytes"
+	"gitlab.com/elixxir/client/network/gateway"
 	"gitlab.com/elixxir/client/network/message"
 	"gitlab.com/elixxir/client/storage/reception"
 	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"
@@ -25,6 +29,17 @@ func TestManager_ProcessMessageRetrieval(t *testing.T) {
 	roundId := id.Round(5)
 	mockComms := &mockMessageRetrievalComms{testingSignature: t}
 	quitChan := make(chan struct{})
+	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)
@@ -103,8 +118,19 @@ func TestManager_ProcessMessageRetrieval(t *testing.T) {
 func TestManager_ProcessMessageRetrieval_NoRound(t *testing.T) {
 	// General initializations
 	testManager := newManager(t)
+	p := gateway.DefaultPoolParams()
+	p.MaxPoolSize = 1
 	roundId := id.Round(5)
 	mockComms := &mockMessageRetrievalComms{testingSignature: t}
+	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.sender, _ = gateway.NewSender(p,
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		testNdf, mockComms, testManager.Session, nil)
 	quitChan := make(chan struct{})
 
 	// Create a local channel so reception is possible (testManager.messageBundles is
@@ -130,7 +156,7 @@ func TestManager_ProcessMessageRetrieval_NoRound(t *testing.T) {
 			},
 		}
 
-		idList := [][]byte{dummyGateway.Bytes()}
+		idList := [][]byte{dummyGateway.Marshal()}
 
 		roundInfo := &pb.RoundInfo{
 			ID:       uint64(roundId),
@@ -172,6 +198,17 @@ func TestManager_ProcessMessageRetrieval_FalsePositive(t *testing.T) {
 	roundId := id.Round(5)
 	mockComms := &mockMessageRetrievalComms{testingSignature: t}
 	quitChan := make(chan struct{})
+	testNdf := getNDF()
+	nodeId := id.NewIdFromString(FalsePositive, 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)
@@ -306,6 +343,17 @@ func TestManager_ProcessMessageRetrieval_MultipleGateways(t *testing.T) {
 	roundId := id.Round(5)
 	mockComms := &mockMessageRetrievalComms{testingSignature: t}
 	quitChan := make(chan struct{})
+	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)
diff --git a/network/rounds/utils_test.go b/network/rounds/utils_test.go
index c963199bedc637e13a050ac9c08d769d8c4a7acf..d352078dcd1219b188c8c7fde0b807748d8c3521 100644
--- a/network/rounds/utils_test.go
+++ b/network/rounds/utils_test.go
@@ -14,6 +14,7 @@ import (
 	pb "gitlab.com/elixxir/comms/mixmessages"
 	"gitlab.com/xx_network/comms/connect"
 	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/ndf"
 	"testing"
 )
 
@@ -28,7 +29,6 @@ func newManager(face interface{}) *Manager {
 			TransmissionID: sess1.GetUser().TransmissionID,
 		},
 	}
-
 	return testManager
 }
 
@@ -43,6 +43,15 @@ type mockMessageRetrievalComms struct {
 	testingSignature *testing.T
 }
 
+func (mmrc *mockMessageRetrievalComms) AddHost(hid *id.ID, address string, cert []byte, params connect.HostParams) (host *connect.Host, err error) {
+	host, _ = mmrc.GetHost(nil)
+	return host, nil
+}
+
+func (mmrc *mockMessageRetrievalComms) RemoveHost(hid *id.ID) {
+
+}
+
 func (mmrc *mockMessageRetrievalComms) GetHost(hostId *id.ID) (*connect.Host, bool) {
 	h, _ := connect.NewHost(hostId, "0.0.0.0", []byte(""), connect.HostParams{
 		MaxRetries:  0,
@@ -92,3 +101,42 @@ func (mmrc *mockMessageRetrievalComms) RequestMessages(host *connect.Host,
 
 	return nil, nil
 }
+
+func getNDF() *ndf.NetworkDefinition {
+	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",
+		},
+	}
+}
diff --git a/network/send.go b/network/send.go
index 0e3e9020aa187716e3a7dd7da9f538bff47ba3e9..d70a0c6661c2ee6f4c40f313219baa7cbb86fd52 100644
--- a/network/send.go
+++ b/network/send.go
@@ -28,7 +28,7 @@ func (m *manager) SendCMIX(msg format.Message, recipient *id.ID, param params.CM
 			"network is not healthy")
 	}
 
-	return m.message.SendCMIX(msg, recipient, param)
+	return m.message.SendCMIX(m.GetSender(), msg, recipient, param)
 }
 
 // SendUnsafe sends an unencrypted payload to the provided recipient
diff --git a/single/manager_test.go b/single/manager_test.go
index 5ee50ed606ff203253729aae941e248c224df533..2ce788f7b86ba44fd61a2aaa68814d1d62a49a02 100644
--- a/single/manager_test.go
+++ b/single/manager_test.go
@@ -14,6 +14,7 @@ import (
 	"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/reception"
@@ -323,6 +324,10 @@ func (tnm *testNetworkManager) InProgressRegistrations() int {
 	return 0
 }
 
+func (t *testNetworkManager) GetSender() *gateway.Sender {
+	return nil
+}
+
 func getNDF() *ndf.NetworkDefinition {
 	return &ndf.NetworkDefinition{
 		E2E: ndf.Group{
diff --git a/storage/auth/store.go b/storage/auth/store.go
index a03dee9eaced47223f597f14d78c6c093ede6cfd..d6730d03d8fcde7db0ee9edb76cb056942be8b7c 100644
--- a/storage/auth/store.go
+++ b/storage/auth/store.go
@@ -355,17 +355,18 @@ func (s *Store) GetRequest(partner *id.ID) (RequestType, *SentRequest, contact.C
 	}
 }
 
-// Fail is one of two calls after using a request. This one is to be used when
+// Done is one of two calls after using a request. This one is to be used when
 // the use is unsuccessful. It will allow any thread waiting on access to
 // continue using the structure.
 // It does not return an error because an error is not handleable.
-func (s *Store) Fail(partner *id.ID) {
+func (s *Store) Done(partner *id.ID) {
 	s.mux.RLock()
 	r, ok := s.requests[*partner]
 	s.mux.RUnlock()
 
 	if !ok {
-		jww.ERROR.Panicf("Request cannot be failed, not found: %s", partner)
+		jww.ERROR.Panicf("Request cannot be finished, not " +
+			"found: %s", partner)
 		return
 	}
 
diff --git a/storage/auth/store_test.go b/storage/auth/store_test.go
index 94d0318f3d702cd7f9fb58860ada31a1b6f54b17..9f94169dc07eefe12c2d7ec9685d3d256fa2f326 100644
--- a/storage/auth/store_test.go
+++ b/storage/auth/store_test.go
@@ -526,11 +526,11 @@ func TestStore_Fail(t *testing.T) {
 		}
 	}()
 
-	s.Fail(c.ID)
+	s.Done(c.ID)
 
 	// Check if the request's mutex is locked
 	if reflect.ValueOf(&s.requests[*c.ID].mux).Elem().FieldByName("state").Int() != 0 {
-		t.Errorf("Fail() did not unlock mutex.")
+		t.Errorf("Done() did not unlock mutex.")
 	}
 }
 
@@ -540,11 +540,11 @@ func TestStore_Fail_RequestNotInMap(t *testing.T) {
 
 	defer func() {
 		if r := recover(); r == nil {
-			t.Errorf("Fail() did not panic when the request is not in map.")
+			t.Errorf("Done() did not panic when the request is not in map.")
 		}
 	}()
 
-	s.Fail(id.NewIdFromUInt(rand.Uint64(), id.User, t))
+	s.Done(id.NewIdFromUInt(rand.Uint64(), id.User, t))
 }
 
 // Happy path: receive request.
diff --git a/storage/reception/IdentityUse.go b/storage/reception/IdentityUse.go
index 93fc6b8d6c374cbf166d1a55e3c302a46192704f..2c8b9df3b64601651d7489e96935ff1a3d360db9 100644
--- a/storage/reception/IdentityUse.go
+++ b/storage/reception/IdentityUse.go
@@ -22,6 +22,7 @@ type IdentityUse struct {
 
 	UR *rounds.UnknownRounds
 	ER *rounds.EarliestRound
+	CR *rounds.CheckedRounds
 }
 
 // setSamplingPeriod add the Request mask as a random buffer around the sampling
diff --git a/storage/reception/fake_test.go b/storage/reception/fake_test.go
index 35ff0e1727a76645ef350b8d1e4a7cd5a76691e9..2c748ac2258e0950e8901a2748c0057c498770ef 100644
--- a/storage/reception/fake_test.go
+++ b/storage/reception/fake_test.go
@@ -24,7 +24,7 @@ func Test_generateFakeIdentity(t *testing.T) {
 		"\"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}"
+		"\"EndRequest\":\"0001-01-01T00:00:00Z\",\"Fake\":true,\"UR\":null,\"ER\":null,\"CR\":null}"
 
 	timestamp := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
 
@@ -36,8 +36,8 @@ func Test_generateFakeIdentity(t *testing.T) {
 	receivedJson, _ := json.Marshal(received)
 
 	if expected != string(receivedJson) {
-		t.Errorf("The fake identity was generated incorrectly.\n "+
-			"expected: %s\nreceived: %s", expected, receivedJson)
+		t.Errorf("The fake identity was generated incorrectly."+
+			"\nexpected: %s\nreceived: %s", expected, receivedJson)
 	}
 }
 
diff --git a/storage/reception/registration.go b/storage/reception/registration.go
index 70647129c7c621b9a757b9d42ec82ef66f3c1bd3..0535f092366ce03f556f21ad61b39b8b65a97b63 100644
--- a/storage/reception/registration.go
+++ b/storage/reception/registration.go
@@ -2,6 +2,8 @@ package reception
 
 import (
 	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/interfaces/params"
 	"gitlab.com/elixxir/client/storage/rounds"
 	"gitlab.com/elixxir/client/storage/versioned"
 	"gitlab.com/xx_network/primitives/id"
@@ -17,6 +19,7 @@ type registration struct {
 	Identity
 	UR *rounds.UnknownRounds
 	ER *rounds.EarliestRound
+	CR *rounds.CheckedRounds
 	kv *versioned.KV
 }
 
@@ -46,6 +49,11 @@ func newRegistration(reg Identity, kv *versioned.KV) (*registration, error) {
 	urParams.Stored = !reg.Ephemeral
 	r.UR = rounds.NewUnknownRounds(kv, urParams)
 	r.ER = rounds.NewEarliestRound(!reg.Ephemeral, kv)
+	cr, err := rounds.NewCheckedRounds(int(params.GetDefaultNetwork().KnownRoundsThreshold), kv)
+	if err != nil {
+		jww.FATAL.Printf("Failed to create new CheckedRounds for registration: %+v", err)
+	}
+	r.CR = cr
 
 	// If this is not ephemeral, then store everything
 	if !reg.Ephemeral {
@@ -71,11 +79,24 @@ func loadRegistration(EphId ephemeral.Id, Source *id.ID, startValid time.Time,
 			"for %s", regPrefix(EphId, Source, startValid))
 	}
 
+	cr, err := rounds.LoadCheckedRounds(int(params.GetDefaultNetwork().KnownRoundsThreshold), kv)
+	if err != nil {
+		jww.ERROR.Printf("Making new CheckedRounds, loading of CheckedRounds "+
+			"failed: %+v", err)
+
+		cr, err = rounds.NewCheckedRounds(int(params.GetDefaultNetwork().KnownRoundsThreshold), kv)
+		if err != nil {
+			jww.FATAL.Printf("Failed to create new CheckedRounds for "+
+				"registration after CheckedRounds load failure: %+v", err)
+		}
+	}
+
 	r := &registration{
 		Identity: reg,
 		kv:       kv,
 		UR:       rounds.LoadUnknownRounds(kv, rounds.DefaultUnknownRoundsParams()),
 		ER:       rounds.LoadEarliestRound(kv),
+		CR:       cr,
 	}
 
 	return r, nil
diff --git a/storage/reception/store.go b/storage/reception/store.go
index f87ee416abaecd01e3955549080e462b3ae1e3ad..bd78f0b7682d3c06c2b47211f518729cb63cbfc1 100644
--- a/storage/reception/store.go
+++ b/storage/reception/store.go
@@ -382,5 +382,6 @@ func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error
 		Fake:     false,
 		UR:       selected.UR,
 		ER:       selected.ER,
+		CR:       selected.CR,
 	}, nil
 }
diff --git a/storage/rounds/checkedRounds.go b/storage/rounds/checkedRounds.go
new file mode 100644
index 0000000000000000000000000000000000000000..a7e3ebdecd425a3ba97902a2197fd3189b7b6f59
--- /dev/null
+++ b/storage/rounds/checkedRounds.go
@@ -0,0 +1,164 @@
+package rounds
+
+import (
+	"container/list"
+	"encoding/binary"
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/storage/utility"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+const itemsPerBlock = 50
+
+type CheckedRounds struct {
+	// Map of round IDs for quick lookup if an ID is stored
+	m map[id.Round]interface{}
+
+	// List of round IDs in order of age; oldest in front and newest in back
+	l *list.List
+
+	// List of recently added round IDs that need to be stored
+	recent []id.Round
+
+	// Saves round IDs in blocks to storage
+	store *utility.BlockStore
+
+	// The maximum number of round IDs to store before pruning the oldest
+	maxRounds int
+}
+
+// NewCheckedRounds returns a new CheckedRounds with an initialized map.
+func NewCheckedRounds(maxRounds int, kv *versioned.KV) (*CheckedRounds, error) {
+	// Calculate the number of blocks of size itemsPerBlock are needed to store
+	// numRoundsToKeep number of round IDs
+	numBlocks := maxRounds / itemsPerBlock
+	if maxRounds%itemsPerBlock != 0 {
+		numBlocks++
+	}
+
+	// Create a new BlockStore for storing the round IDs to storage
+	store, err := utility.NewBlockStore(itemsPerBlock, numBlocks, kv)
+	if err != nil {
+		return nil, errors.Errorf("failed to save new checked rounds to storage: %+v", err)
+	}
+
+	// Create new CheckedRounds
+	return newCheckedRounds(maxRounds, store), nil
+}
+
+// newCheckedRounds initialises the lists in CheckedRounds.
+func newCheckedRounds(maxRounds int, store *utility.BlockStore) *CheckedRounds {
+	return &CheckedRounds{
+		m:         make(map[id.Round]interface{}),
+		l:         list.New(),
+		recent:    []id.Round{},
+		store:     store,
+		maxRounds: maxRounds,
+	}
+}
+
+// LoadCheckedRounds restores the list from storage.
+func LoadCheckedRounds(maxRounds int, kv *versioned.KV) (*CheckedRounds, error) {
+	// Get rounds from storage
+	store, rounds, err := utility.LoadBlockStore(kv)
+	if err != nil {
+		return nil, errors.Errorf("failed to load CheckedRounds from storage: %+v", err)
+	}
+
+	// Create new CheckedRounds
+	cr := newCheckedRounds(maxRounds, store)
+
+	// Unmarshal round ID byte list into the new CheckedRounds
+	cr.unmarshal(rounds)
+
+	return cr, nil
+}
+
+// SaveCheckedRounds stores the list to storage.
+func (cr *CheckedRounds) SaveCheckedRounds() error {
+
+	// Save to disk
+	err := cr.store.Store(cr)
+	if err != nil {
+		return errors.Errorf("failed to store recent CheckedRounds: %+v", err)
+	}
+
+	// Save to disk
+	return nil
+}
+
+// Next pops the oldest recent round ID from the list and returns it as bytes.
+// Returns false if the list is empty
+func (cr *CheckedRounds) Next() ([]byte, bool) {
+	if len(cr.recent) > 0 {
+		b := make([]byte, 8)
+		binary.LittleEndian.PutUint64(b, uint64(cr.recent[0]))
+		cr.recent = cr.recent[1:]
+		return b, true
+	}
+
+	return nil, false
+}
+
+// Check determines if the round ID has been added to the checklist. If it has
+// not, then it is added and the function returns true. Otherwise, if it already
+// exists, then the function returns false.
+func (cr *CheckedRounds) Check(rid id.Round) bool {
+	// Add the round ID to the checklist if it does not exist and return true
+	if _, exists := cr.m[rid]; !exists {
+		cr.m[rid] = nil    // Add ID to the map
+		cr.l.PushBack(rid) // Add ID to the end of the list
+		cr.recent = append(cr.recent, rid)
+
+		// The commented out code below works the same as the Prune function but
+		// occurs when adding a round ID to the list. It was decided to use
+		// Prune instead so that it does not block even though the savings are
+		// probably negligible.
+		// // Remove the oldest round ID the list is full
+		// if cr.l.Len() > cr.maxRounds {
+		// 	oldestID := cr.l.Remove(cr.l.Front()) // Remove oldest from list
+		// 	delete(cr.m, oldestID.(id.Round))     // Remove oldest from map
+		// }
+
+		return true
+	}
+
+	return false
+}
+
+// IsChecked determines if the round has been added to the checklist.
+func (cr *CheckedRounds) IsChecked(rid id.Round) bool {
+	_, exists := cr.m[rid]
+	return exists
+}
+
+// Prune any rounds that are earlier than the earliestAllowed.
+func (cr *CheckedRounds) Prune() {
+	if len(cr.m) < cr.maxRounds {
+		return
+	}
+	earliestAllowed := cr.l.Back().Value.(id.Round) - id.Round(cr.maxRounds) + 1
+
+	// Iterate over all the round IDs and remove any that are too old
+	for e := cr.l.Front(); e != nil; {
+		if e.Value.(id.Round) < earliestAllowed {
+			delete(cr.m, e.Value.(id.Round))
+			lastE := e
+			e = e.Next()
+			cr.l.Remove(lastE)
+		} else {
+			break
+		}
+	}
+}
+
+// unmarshal unmarshalls the list of byte slices into the CheckedRounds map and
+// list.
+func (cr *CheckedRounds) unmarshal(rounds [][]byte) {
+	for _, round := range rounds {
+		rid := id.Round(binary.LittleEndian.Uint64(round))
+		cr.m[rid] = nil
+		cr.l.PushBack(rid)
+	}
+}
diff --git a/storage/rounds/checkedRounds_test.go b/storage/rounds/checkedRounds_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..816c6fce7c311b71c5dd2d33f4b5fa07b2ec7ec9
--- /dev/null
+++ b/storage/rounds/checkedRounds_test.go
@@ -0,0 +1,217 @@
+package rounds
+
+import (
+	"container/list"
+	"encoding/binary"
+	"gitlab.com/elixxir/client/storage/utility"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/xx_network/primitives/id"
+	"reflect"
+	"testing"
+)
+
+// Happy path.
+func Test_newCheckedRounds(t *testing.T) {
+	maxRounds := 230
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	// Create a new BlockStore for storing the round IDs to storage
+	store, err := utility.NewBlockStore(itemsPerBlock, maxRounds/itemsPerBlock+1, kv)
+	if err != nil {
+		t.Errorf("Failed to create new BlockStore: %+v", err)
+	}
+
+	expected := &CheckedRounds{
+		m:         make(map[id.Round]interface{}),
+		l:         list.New(),
+		recent:    []id.Round{},
+		store:     store,
+		maxRounds: maxRounds,
+	}
+
+	received, err := NewCheckedRounds(maxRounds, kv)
+	if err != nil {
+		t.Errorf("NewCheckedRounds() returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(expected, received) {
+		t.Errorf("NewCheckedRounds() did not return the exepcted CheckedRounds."+
+			"\nexpected: %+v\nreceived: %+v", expected, received)
+	}
+}
+
+// Tests that a CheckedRounds that has been saved and loaded from storage
+// matches the original.
+func TestCheckedRounds_SaveCheckedRounds_TestLoadCheckedRounds(t *testing.T) {
+	// Create new CheckedRounds and add rounds to it
+	kv := versioned.NewKV(make(ekv.Memstore))
+	cr, err := NewCheckedRounds(50, kv)
+	if err != nil {
+		t.Errorf("failed to make new CheckedRounds: %+v", err)
+	}
+	for i := id.Round(0); i < 100; i++ {
+		cr.Check(i)
+	}
+
+	err = cr.SaveCheckedRounds()
+	if err != nil {
+		t.Errorf("SaveCheckedRounds() returned an error: %+v", err)
+	}
+
+	cr.Prune()
+
+	newCR, err := LoadCheckedRounds(50, kv)
+	if err != nil {
+		t.Errorf("LoadCheckedRounds() returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(cr, newCR) {
+		t.Errorf("Failed to store and load CheckedRounds."+
+			"\nexpected: %+v\nreceived: %+v", cr, newCR)
+	}
+}
+
+// Happy path.
+func TestCheckedRounds_Next(t *testing.T) {
+	cr := newCheckedRounds(100, nil)
+	rounds := make([][]byte, 10)
+	for i := id.Round(0); i < 10; i++ {
+		cr.Check(i)
+	}
+
+	for i := id.Round(0); i < 10; i++ {
+		round, exists := cr.Next()
+		if !exists {
+			t.Error("Next() returned false when there should be more IDs.")
+		}
+
+		rounds[i] = round
+	}
+	round, exists := cr.Next()
+	if exists {
+		t.Errorf("Next() returned true when the list should be empty: %d", round)
+	}
+
+	testCR := newCheckedRounds(100, nil)
+	testCR.unmarshal(rounds)
+
+	if !reflect.DeepEqual(cr, testCR) {
+		t.Errorf("unmarshal() did not return the expected CheckedRounds."+
+			"\nexpected: %+v\nreceived: %+v", cr, testCR)
+	}
+}
+
+// Happy path.
+func Test_checkedRounds_Check(t *testing.T) {
+	cr := newCheckedRounds(100, nil)
+	var expected []id.Round
+	for i := id.Round(1); i < 11; i++ {
+		if i%2 == 0 {
+			if !cr.Check(i) {
+				t.Errorf("Check() returned false when the round ID should have been added (%d).", i)
+			}
+
+			val := cr.l.Back().Value.(id.Round)
+			if val != i {
+				t.Errorf("Check() did not add the round ID to the back of the list."+
+					"\nexpected: %d\nreceived: %d", i, val)
+			}
+			expected = append(expected, i)
+		}
+	}
+
+	if !reflect.DeepEqual(cr.recent, expected) {
+		t.Errorf("Unexpected list of recent rounds."+
+			"\nexpected: %+v\nreceived: %+v", expected, cr.recent)
+	}
+
+	for i := id.Round(1); i < 11; i++ {
+		result := cr.Check(i)
+		if i%2 == 0 {
+			if result {
+				t.Errorf("Check() returned true when the round ID should not have been added (%d).", i)
+			}
+		} else if !result {
+			t.Errorf("Check() returned false when the round ID should have been added (%d).", i)
+		} else {
+			expected = append(expected, i)
+		}
+	}
+
+	if !reflect.DeepEqual(cr.recent, expected) {
+		t.Errorf("Unexpected list of recent rounds."+
+			"\nexpected: %+v\nreceived: %+v", expected, cr.recent)
+	}
+}
+
+// Happy path.
+func TestCheckedRounds_IsChecked(t *testing.T) {
+	cr := newCheckedRounds(100, nil)
+
+	for i := id.Round(0); i < 100; i += 2 {
+		cr.Check(i)
+	}
+
+	for i := id.Round(0); i < 100; i++ {
+		if i%2 == 0 {
+			if !cr.IsChecked(i) {
+				t.Errorf("IsChecked() falsly reported round ID %d as not checked.", i)
+			}
+		} else if cr.IsChecked(i) {
+			t.Errorf("IsChecked() falsly reported round ID %d as checked.", i)
+		}
+	}
+}
+
+// Happy path.
+func Test_checkedRounds_Prune(t *testing.T) {
+	cr := newCheckedRounds(5, nil)
+	for i := id.Round(0); i < 10; i++ {
+		cr.Check(i)
+	}
+
+	cr.Prune()
+
+	if len(cr.m) != 5 || cr.l.Len() != 5 {
+		t.Errorf("Prune() did not remove the correct number of round IDs."+
+			"\nexpected: %d\nmap:      %d\nlist:     %d", 5,
+			len(cr.m), cr.l.Len())
+	}
+}
+
+// Happy path: length of the list is not too long and does not need to be pruned.
+func Test_checkedRounds_Prune_NoChange(t *testing.T) {
+	cr := newCheckedRounds(100, nil)
+	for i := id.Round(0); i < 10; i++ {
+		cr.Check(i)
+	}
+
+	cr.Prune()
+
+	if len(cr.m) != 10 || cr.l.Len() != 10 {
+		t.Errorf("Prune() did not remove the correct number of round IDs."+
+			"\nexpected: %d\nmap:      %d\nlist:     %d", 5,
+			len(cr.m), cr.l.Len())
+	}
+}
+
+// Happy path.
+func TestCheckedRounds_unmarshal(t *testing.T) {
+	expected := newCheckedRounds(100, nil)
+	rounds := make([][]byte, 10)
+	for i := id.Round(0); i < 10; i++ {
+		expected.Check(i)
+		rounds[i] = make([]byte, 8)
+		binary.LittleEndian.PutUint64(rounds[i], uint64(i))
+	}
+	expected.recent = []id.Round{}
+
+	cr := newCheckedRounds(100, nil)
+	cr.unmarshal(rounds)
+
+	if !reflect.DeepEqual(expected, cr) {
+		t.Errorf("unmarshal() did not return the expected CheckedRounds."+
+			"\nexpected: %+v\nreceived: %+v", expected, cr)
+	}
+}
diff --git a/storage/utility/blockStore.go b/storage/utility/blockStore.go
new file mode 100644
index 0000000000000000000000000000000000000000..a405e9b966175b77f8a9803e2efbe72bf2cf27af
--- /dev/null
+++ b/storage/utility/blockStore.go
@@ -0,0 +1,274 @@
+package utility
+
+import (
+	"bytes"
+	"encoding/binary"
+	"encoding/json"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/xx_network/primitives/netTime"
+	"strconv"
+)
+
+// Sizes in bytes
+const (
+	int64Size      = 8
+	marshalledSize = 4 * int64Size
+)
+
+// Error messages
+const (
+	bsBuffLengthErr   = "length of buffer %d != %d expected"
+	bsKvSaveErr       = "failed to save blockStore to KV: %+v"
+	bsKvLoadErr       = "failed to get BlockStore from storage: %+v"
+	bsKvUnmarshalErr  = "failed to unmarshal BlockStore loaded from storage: %+v"
+	bJsonMarshalErr   = "failed to JSON marshal block %d: %+v"
+	bKvSaveErr        = "failed to save block %d to KV: %+v"
+	bKvDeleteErr      = "failed to delete block %d from KV: %+v"
+	bKvLoadErr        = "failed to get block %d from KV: %+v"
+	bJsonUnmarshalErr = "failed to JSON marshal block %d: %+v"
+)
+
+// Storage keys and parts
+const (
+	delimiter         = "/"
+	blockStoreKey     = "blockStore"
+	blockStoreVersion = 0
+	blockKey          = "block"
+	blockVersion      = 0
+)
+
+type Iterator interface {
+	Next() ([]byte, bool)
+}
+
+type BlockStore struct {
+	block      [][]byte
+	numBlocks  int // The maximum number of blocks saved to the kv
+	blockSize  int // The maximum number of items allowed in a block
+	firstSaved int // The index of the oldest block in the list
+	lastSaved  int // The index of the newest block in the list
+	kv         *versioned.KV
+}
+
+// NewBlockStore returns a new BlockStore and saves it to storage.
+func NewBlockStore(numBlocks, blockSize int, kv *versioned.KV) (*BlockStore, error) {
+	bs := &BlockStore{
+		block:      make([][]byte, 0, blockSize),
+		numBlocks:  numBlocks,
+		blockSize:  blockSize,
+		firstSaved: 0,
+		lastSaved:  0,
+		kv:         kv,
+	}
+
+	return bs, bs.save()
+}
+
+// LoadBlockStore returns the BlockStore from storage and a concatenation of all
+// blocks in storage.
+func LoadBlockStore(kv *versioned.KV) (*BlockStore, [][]byte, error) {
+	bs := &BlockStore{kv: kv}
+
+	// Get BlockStore parameters from storage
+	err := bs.load()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// LoadBlockStore each block from storage and join together into single slice
+	var data, block [][]byte
+	for i := bs.firstSaved; i <= bs.lastSaved; i++ {
+		// Get the block from storage
+		block, err = bs.loadBlock(i)
+		if err != nil {
+			return nil, nil, err
+		}
+
+		// Append block to the rest of the data
+		data = append(data, block...)
+	}
+
+	// Save the last block into memory
+	bs.block = block
+
+	return bs, data, nil
+}
+
+// Store stores all items in the Iterator to storage in blocks.
+func (bs *BlockStore) Store(iter Iterator) error {
+	// Iterate through all items in the Iterator and add each to the block in
+	// memory. When the block is full, it is saved to storage and a new block is
+	// added to until the iterator returns false.
+	for item, exists := iter.Next(); exists; item, exists = iter.Next() {
+		// If the block is full, save it to storage and start a new block
+		if len(bs.block) >= bs.blockSize {
+			if err := bs.saveBlock(); err != nil {
+				return err
+			}
+
+			bs.lastSaved++
+			bs.block = make([][]byte, 0, bs.blockSize)
+		}
+
+		// Append the item to the block in memory
+		bs.block = append(bs.block, item)
+	}
+
+	// Save the current partially filled block to storage
+	if err := bs.saveBlock(); err != nil {
+		return err
+	}
+
+	// Calculate the new first saved index
+	oldFirstSaved := bs.firstSaved
+	if (bs.lastSaved+1)-bs.firstSaved > bs.numBlocks {
+		bs.firstSaved = (bs.lastSaved + 1) - bs.numBlocks
+	}
+
+	// Save the BlockStorage parameters to storage
+	if err := bs.save(); err != nil {
+		return err
+	}
+
+	// Delete all old blocks
+	bs.pruneBlocks(oldFirstSaved)
+
+	return nil
+}
+
+// saveBlock saves the block to an indexed storage.
+func (bs *BlockStore) saveBlock() error {
+	// JSON marshal block
+	data, err := json.Marshal(bs.block)
+	if err != nil {
+		return errors.Errorf(bJsonMarshalErr, bs.lastSaved, err)
+	}
+
+	// Construct versioning object
+	obj := versioned.Object{
+		Version:   blockVersion,
+		Timestamp: netTime.Now(),
+		Data:      data,
+	}
+
+	// Save to storage
+	err = bs.kv.Set(bs.getKey(bs.lastSaved), blockVersion, &obj)
+	if err != nil {
+		return errors.Errorf(bKvSaveErr, bs.lastSaved, err)
+	}
+
+	return nil
+}
+
+// loadBlock loads the block with the index from storage.
+func (bs *BlockStore) loadBlock(i int) ([][]byte, error) {
+	// Get the data from the kv
+	obj, err := bs.kv.Get(bs.getKey(i), blockVersion)
+	if err != nil {
+		return nil, errors.Errorf(bKvLoadErr, i, err)
+	}
+
+	// Unmarshal the block
+	var block [][]byte
+	err = json.Unmarshal(obj.Data, &block)
+	if err != nil {
+		return nil, errors.Errorf(bJsonUnmarshalErr, i, err)
+	}
+
+	return block, nil
+}
+
+// pruneBlocks reduces the number of saved blocks to numBlocks by removing the
+// oldest blocks.
+func (bs *BlockStore) pruneBlocks(firstSaved int) {
+	// Exit if no blocks need to be pruned
+	if (bs.lastSaved+1)-firstSaved < bs.numBlocks {
+		return
+	}
+
+	// Delete all blocks before the firstSaved index
+	for ; firstSaved < bs.firstSaved; firstSaved++ {
+		err := bs.kv.Delete(bs.getKey(firstSaved), blockVersion)
+		if err != nil {
+			jww.WARN.Printf(bKvDeleteErr, bs.firstSaved, err)
+		}
+	}
+}
+
+// getKey produces a block storage key for the given index.
+func (bs *BlockStore) getKey(i int) string {
+	return blockKey + delimiter + strconv.Itoa(i)
+}
+
+// save saves the parameters in BlockStore to storage. It does not save any
+// block data.
+func (bs *BlockStore) save() error {
+	// Construct versioning object
+	obj := versioned.Object{
+		Version:   blockStoreVersion,
+		Timestamp: netTime.Now(),
+		Data:      bs.marshal(),
+	}
+
+	// Save to storage
+	err := bs.kv.Set(blockStoreKey, blockStoreVersion, &obj)
+	if err != nil {
+		return errors.Errorf(bsKvSaveErr, err)
+	}
+
+	return nil
+}
+
+// load loads BlockStore parameters from storage.
+func (bs *BlockStore) load() error {
+	// Get the data from the kv
+	obj, err := bs.kv.Get(blockStoreKey, blockStoreVersion)
+	if err != nil {
+		return errors.Errorf(bsKvLoadErr, err)
+	}
+
+	// Unmarshal the data into a BlockStore
+	err = bs.unmarshal(obj.Data)
+	if err != nil {
+		return errors.Errorf(bsKvUnmarshalErr, err)
+	}
+
+	return nil
+}
+
+// marshal marshals the BlockStore integer values to a byte slice.
+func (bs *BlockStore) marshal() []byte {
+	// Build list of values to store
+	values := []int{bs.numBlocks, bs.blockSize, bs.firstSaved, bs.lastSaved}
+
+	// Convert each value to a byte slice and store
+	var buff bytes.Buffer
+	for _, val := range values {
+		b := make([]byte, int64Size)
+		binary.LittleEndian.PutUint64(b, uint64(val))
+		buff.Write(b)
+	}
+
+	// Return the bytes
+	return buff.Bytes()
+}
+
+// unmarshal unmarshalls the BlockStore int values from the buffer. An error is
+// returned if the length of the bytes is incorrect.
+func (bs *BlockStore) unmarshal(b []byte) error {
+	// Return an error if the buffer is not the expected length
+	if len(b) != marshalledSize {
+		return errors.Errorf(bsBuffLengthErr, len(b), marshalledSize)
+	}
+
+	// Convert the byte slices to ints and store
+	buff := bytes.NewBuffer(b)
+	bs.numBlocks = int(binary.LittleEndian.Uint64(buff.Next(int64Size)))
+	bs.blockSize = int(binary.LittleEndian.Uint64(buff.Next(int64Size)))
+	bs.firstSaved = int(binary.LittleEndian.Uint64(buff.Next(int64Size)))
+	bs.lastSaved = int(binary.LittleEndian.Uint64(buff.Next(int64Size)))
+
+	return nil
+}
diff --git a/storage/utility/blockStore_test.go b/storage/utility/blockStore_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c9e2fb56daef0cff51eb7e283b463f9782ac4cf6
--- /dev/null
+++ b/storage/utility/blockStore_test.go
@@ -0,0 +1,414 @@
+package utility
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/xx_network/primitives/netTime"
+	"math/rand"
+	"reflect"
+	"strings"
+	"testing"
+)
+
+type iter [][]byte
+
+func (it *iter) Next() ([]byte, bool) {
+	if len(*it) > 0 {
+		item := (*it)[0]
+		*it = (*it)[1:]
+		return item, true
+	}
+
+	return nil, false
+}
+
+// Happy path.
+func TestNewBlockStore(t *testing.T) {
+	expected := &BlockStore{
+		block:      make([][]byte, 0, 20),
+		numBlocks:  50,
+		blockSize:  20,
+		firstSaved: 0,
+		lastSaved:  0,
+		kv:         versioned.NewKV(make(ekv.Memstore)),
+	}
+
+	bs, err := NewBlockStore(expected.numBlocks, expected.blockSize, expected.kv)
+	if err != nil {
+		t.Errorf("NewBlockStore() returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(expected, bs) {
+		t.Errorf("NewBlockStore() did not return the expected BlockStore."+
+			"\nexpected: %+v\nreceived: %+v", expected, bs)
+	}
+}
+
+// Tests BlockStore storing and loading data in multiple situations.
+func TestBlockStore_Store_LoadBlockStore(t *testing.T) {
+	values := []struct {
+		blockSize, numBlocks                  int
+		expectedFirstSaved, expectedLastSaved int
+		dataCutIndex                          int
+	}{
+		{3, 5, 0, 3, 0},  // Multiple blocks, last block partial, no pruning
+		{10, 5, 0, 0, 0}, // Single block, last block full, no pruning
+		{15, 5, 0, 0, 0}, // Single block, last block partial, no pruning
+
+		{2, 3, 2, 4, 4}, // Multiple blocks, last block partial, pruned
+		{5, 1, 1, 1, 5}, // Single block, last block full, pruned
+		{4, 1, 2, 2, 8}, // Single block, last block partial, pruned
+	}
+
+	for i, v := range values {
+		// Create the initial data to store
+		iter := make(iter, 10)
+		for i := uint64(0); i < 10; i++ {
+			iter[i] = make([]byte, 8)
+			binary.LittleEndian.PutUint64(iter[i], i)
+		}
+
+		// Calculate the expected data
+		expected := make([][]byte, len(iter[v.dataCutIndex:]))
+		copy(expected, iter[v.dataCutIndex:])
+
+		bs, err := NewBlockStore(v.numBlocks, v.blockSize, versioned.NewKV(make(ekv.Memstore)))
+		if err != nil {
+			t.Errorf("Failed to create new BlockStore (%d): %+v", i, err)
+		}
+
+		// Attempt to store the data
+		err = bs.Store(&iter)
+		if err != nil {
+			t.Errorf("Store() returned an error (%d): %+v", i, err)
+		}
+
+		if bs.firstSaved != v.expectedFirstSaved {
+			t.Errorf("Store() did not return the expected firstSaved (%d)."+
+				"\nexpected: %d\nreceived: %d", i, v.expectedFirstSaved, bs.firstSaved)
+		}
+
+		if bs.lastSaved != v.expectedLastSaved {
+			t.Errorf("Store() did not return the expected lastSaved (%d)."+
+				"\nexpected: %d\nreceived: %d", i, v.expectedLastSaved, bs.lastSaved)
+		}
+
+		// Attempt to load the data
+		loadBS, data, err := LoadBlockStore(bs.kv)
+		if err != nil {
+			t.Errorf("LoadBlockStore() returned an error (%d): %+v", i, err)
+		}
+
+		// Check if the loaded BlockStore is correct
+		if !reflect.DeepEqual(bs, loadBS) {
+			t.Errorf("Loading wrong BlockStore from storage (%d)."+
+				"\nexpected: %+v\nreceived: %+v", i, bs, loadBS)
+		}
+
+		// Check if the loaded data is correct
+		if !reflect.DeepEqual(expected, data) {
+			t.Errorf("Loading wrong data from storage (%d)."+
+				"\nexpected: %+v\nreceived: %+v", i, expected, data)
+		}
+	}
+}
+
+// Tests that a block is successfully saved and loaded from storage.
+func TestBlockStore_saveBlock_loadBlock(t *testing.T) {
+	prng := rand.New(rand.NewSource(42))
+	bs := &BlockStore{
+		block:      make([][]byte, 0, 20),
+		numBlocks:  50,
+		blockSize:  20,
+		firstSaved: 0,
+		lastSaved:  0,
+		kv:         versioned.NewKV(make(ekv.Memstore)),
+	}
+
+	for i := range bs.block {
+		bs.block[i] = make([]byte, 32)
+		prng.Read(bs.block[i])
+	}
+
+	err := bs.saveBlock()
+	if err != nil {
+		t.Errorf("saveBlock() returned an error: %+v", err)
+	}
+
+	newBS := &BlockStore{kv: bs.kv}
+	block, err := newBS.loadBlock(0)
+	if err != nil {
+		t.Errorf("loadBlock() returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(bs.block, block) {
+		t.Errorf("Failed to save and load block to storage."+
+			"\nexpected: %+v\nreceived: %+v", bs.block, block)
+	}
+}
+
+// Error path: failed to save to KV.
+func TestBlockStore_saveBlock_SaveError(t *testing.T) {
+	prng := rand.New(rand.NewSource(42))
+	bs := &BlockStore{
+		block:      make([][]byte, 0, 20),
+		numBlocks:  50,
+		blockSize:  20,
+		firstSaved: 0,
+		lastSaved:  0,
+		kv:         versioned.NewKV(make(ekv.Memstore)),
+	}
+
+	for i := range bs.block {
+		bs.block[i] = make([]byte, 32)
+		prng.Read(bs.block[i])
+	}
+
+	err := bs.saveBlock()
+	if err != nil {
+		t.Errorf("saveBlock() returned an error: %+v", err)
+	}
+
+	newBS := &BlockStore{kv: bs.kv}
+	block, err := newBS.loadBlock(0)
+	if err != nil {
+		t.Errorf("loadBlock() returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(bs.block, block) {
+		t.Errorf("Failed to save and load block to storage."+
+			"\nexpected: %+v\nreceived: %+v", bs.block, block)
+	}
+}
+
+// Error path: loading of nonexistent key returns an error.
+func TestBlockStore_loadBlock_LoadStorageError(t *testing.T) {
+	expectedErr := strings.SplitN(bKvLoadErr, "%", 2)[0]
+	bs := &BlockStore{kv: versioned.NewKV(make(ekv.Memstore))}
+	_, err := bs.loadBlock(0)
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("loadBlock() did not return the expected error."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Error path: unmarshalling of invalid data fails.
+func TestBlockStore_loadBlock_UnmarshalError(t *testing.T) {
+	bs := &BlockStore{kv: versioned.NewKV(make(ekv.Memstore))}
+	expectedErr := strings.SplitN(bJsonUnmarshalErr, "%", 2)[0]
+
+	// Construct object with invalid data
+	obj := versioned.Object{
+		Version:   blockVersion,
+		Timestamp: netTime.Now(),
+		Data:      []byte("invalid JSON"),
+	}
+
+	// Save to storage
+	err := bs.kv.Set(bs.getKey(bs.lastSaved), blockVersion, &obj)
+	if err != nil {
+		t.Errorf("Failed to save data to KV: %+v", err)
+	}
+
+	_, err = bs.loadBlock(0)
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("loadBlock() did not return the expected error."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Happy path.
+func TestBlockStore_pruneBlocks(t *testing.T) {
+	bs := &BlockStore{
+		block:      make([][]byte, 0, 32),
+		numBlocks:  5,
+		blockSize:  32,
+		firstSaved: 0,
+		lastSaved:  0,
+		kv:         versioned.NewKV(make(ekv.Memstore)),
+	}
+
+	// Save blocks to storage
+	for ; bs.lastSaved < 15; bs.lastSaved++ {
+		if err := bs.saveBlock(); err != nil {
+			t.Errorf("Failed to save block %d: %+v", bs.lastSaved, err)
+		}
+	}
+
+	// Calculate the new first saved index
+	oldFirstSaved := bs.firstSaved
+	bs.firstSaved = bs.lastSaved - bs.numBlocks
+
+	// Prune blocks
+	bs.pruneBlocks(oldFirstSaved)
+
+	// Check that the old blocks were deleted
+	for i := 0; i < bs.lastSaved-bs.numBlocks; i++ {
+		if _, err := bs.kv.Get(bs.getKey(i), blockVersion); err == nil {
+			t.Errorf("pruneBlocks() failed to delete old block %d: %+v", i, err)
+		}
+	}
+
+	// Check that the new blocks were not deleted
+	for i := bs.firstSaved; i < bs.lastSaved; i++ {
+		if _, err := bs.kv.Get(bs.getKey(i), blockVersion); err != nil {
+			t.Errorf("pruneBlocks() deleted block %d: %+v", i, err)
+		}
+	}
+
+	// Call pruneBlocks when there are no blocks to prune
+	oldFirstSaved = bs.firstSaved
+	bs.firstSaved = bs.lastSaved - bs.numBlocks
+	bs.pruneBlocks(oldFirstSaved)
+
+	// Check that the new blocks were not deleted
+	for i := bs.firstSaved; i < bs.lastSaved; i++ {
+		if _, err := bs.kv.Get(bs.getKey(i), blockVersion); err != nil {
+			t.Errorf("pruneBlocks() deleted block %d: %+v", i, err)
+		}
+	}
+}
+
+// Consistency test.
+func TestBlockStore_getKey_Consistency(t *testing.T) {
+	expectedKeys := []string{
+		"block/0", "block/1", "block/2", "block/3", "block/4",
+		"block/5", "block/6", "block/7", "block/8", "block/9",
+	}
+	var bs BlockStore
+
+	for i, expected := range expectedKeys {
+		key := bs.getKey(i)
+		if key != expected {
+			t.Errorf("getKey did not return the correct key for the index %d."+
+				"\nexpected: %s\nreceived: %s", i, expected, key)
+		}
+	}
+}
+
+// Tests that a BlockStore can be saved and loaded from the KV correctly.
+func TestBlockStore_save_load(t *testing.T) {
+	bs := &BlockStore{
+		numBlocks: 5, blockSize: 6, firstSaved: 7, lastSaved: 8,
+		kv: versioned.NewKV(make(ekv.Memstore)),
+	}
+
+	err := bs.save()
+	if err != nil {
+		t.Errorf("save() returned an error: %+v", err)
+	}
+
+	testBS := &BlockStore{kv: bs.kv}
+	err = testBS.load()
+	if err != nil {
+		t.Errorf("load() returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(bs, testBS) {
+		t.Errorf("Failed to save and load BlockStore to KV."+
+			"\nexpected: %+v\nreceived: %+v", bs, testBS)
+	}
+}
+
+// Error path: loading of unsaved BlockStore fails.
+func TestBlockStore_load_KvGetError(t *testing.T) {
+	expectedErr := strings.SplitN(bsKvLoadErr, "%", 2)[0]
+
+	testBS := &BlockStore{kv: versioned.NewKV(make(ekv.Memstore))}
+	err := testBS.load()
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("load() did not return an error for a nonexistent item in storage."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Error path: unmarshalling of invalid data fails.
+func TestBlockStore_load_UnmarshalError(t *testing.T) {
+	expectedErr := strings.SplitN(bsKvUnmarshalErr, "%", 2)[0]
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	// Construct invalid versioning object
+	obj := versioned.Object{
+		Version:   blockStoreVersion,
+		Timestamp: netTime.Now(),
+		Data:      []byte("invalid data"),
+	}
+
+	// Save to storage
+	err := kv.Set(blockStoreKey, blockStoreVersion, &obj)
+	if err != nil {
+		t.Fatalf("failed to save object to storage: %+v", err)
+	}
+
+	// Try to retrieve invalid object
+	testBS := &BlockStore{kv: kv}
+	err = testBS.load()
+	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("load() did not return an error for a nonexistent item in storage."+
+			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Consistency test.
+func TestBlockStore_unmarshal(t *testing.T) {
+	buff := []byte{5, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0,
+		0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0}
+	expected := &BlockStore{numBlocks: 5, blockSize: 6, firstSaved: 7, lastSaved: 8}
+
+	bs := &BlockStore{}
+	err := bs.unmarshal(buff)
+	if err != nil {
+		t.Errorf("unmarshal() returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(expected, bs) {
+		t.Errorf("unmarshal() did not return the expected BlockStore."+
+			"\nexpected: %+v\nreceived: %+v", expected, bs)
+	}
+}
+
+// Error path: length of buffer incorrect.
+func TestBlockStore_unmarshal_BuffLengthError(t *testing.T) {
+	expectedErr := fmt.Sprintf(bsBuffLengthErr, 0, marshalledSize)
+	bs := BlockStore{}
+	err := bs.unmarshal([]byte{})
+	if err == nil || err.Error() != expectedErr {
+		t.Errorf("unmarshal() did not return the expected error for a buffer "+
+			"of the wrong size.\nexpected: %s\nreceived: %+v", expectedErr, err)
+	}
+}
+
+// Consistency test.
+func TestBlockStore_marshal(t *testing.T) {
+	expected := []byte{5, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0,
+		0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0}
+	bs := &BlockStore{numBlocks: 5, blockSize: 6, firstSaved: 7, lastSaved: 8}
+
+	buff := bs.marshal()
+
+	if !bytes.Equal(expected, buff) {
+		t.Errorf("marshal() did not return the expected bytes."+
+			"\nexpected: %+v\nreceived: %+v", expected, buff)
+	}
+}
+
+// Tests that marshal and unmarshal work together.
+func TestBlockStore_marshal_unmarshal(t *testing.T) {
+	bs := &BlockStore{numBlocks: 5, blockSize: 6, firstSaved: 7, lastSaved: 8}
+
+	buff := bs.marshal()
+
+	testBS := &BlockStore{}
+	err := testBS.unmarshal(buff)
+	if err != nil {
+		t.Errorf("unmarshal() returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(bs, testBS) {
+		t.Errorf("failed to marshal and unmarshal BlockStore."+
+			"\nexpected: %+v\nreceived: %+v", bs, testBS)
+	}
+}