diff --git a/api/client.go b/api/client.go
index 97241cf2919891313a25849335efde607e4272b0..7c23d024a5996778319aa06a78ee659909aa13ac 100644
--- a/api/client.go
+++ b/api/client.go
@@ -716,6 +716,11 @@ func (c *Client) GetE2EHandler() e2e.Handler {
 	return c.e2e
 }
 
+// GetEventReporter returns the event reporter
+func (c *Client) GetEventReporter() event.Reporter {
+	return c.events
+}
+
 // GetBackup returns a pointer to the backup container so that the backup can be
 // set and triggered.
 func (c *Client) GetBackup() *backup.Backup {
diff --git a/api/precan.go b/api/precan.go
index f85210df17ac5f37845201cedb1b68430f6b12e3..a563faa230bf83a0bca8da52330cf9d1262f8b42 100644
--- a/api/precan.go
+++ b/api/precan.go
@@ -9,6 +9,7 @@ package api
 
 import (
 	"encoding/binary"
+	"gitlab.com/elixxir/crypto/diffieHellman"
 	"math/rand"
 
 	"github.com/cloudflare/circl/dh/sidh"
@@ -33,10 +34,7 @@ func createPrecannedUser(precannedID uint, rng csprng.Source, cmix,
 	prng := rand.New(rand.NewSource(int64(precannedID)))
 	prime := e2e.GetPBytes()
 	keyLen := len(prime)
-	e2eKeyBytes, err := csprng.GenerateInGroup(prime, keyLen, prng)
-	if err != nil {
-		jww.FATAL.Panicf(err.Error())
-	}
+	e2eKey := diffieHellman.GeneratePrivateKey(keyLen, e2e, prng)
 
 	// Salt, UID, etc gen
 	salt := make([]byte, SaltSize)
@@ -57,7 +55,8 @@ func createPrecannedUser(precannedID uint, rng csprng.Source, cmix,
 		ReceptionID:      &userID,
 		ReceptionSalt:    salt,
 		Precanned:        true,
-		E2eDhPrivateKey:  e2e.NewIntFromBytes(e2eKeyBytes),
+		E2eDhPrivateKey:  e2eKey,
+		E2eDhPublicKey:   diffieHellman.GeneratePublicKey(e2eKey, e2e),
 		TransmissionRSA:  rsaKey,
 		ReceptionRSA:     rsaKey,
 	}
diff --git a/api/user.go b/api/user.go
index 5913185b4bec08d63680a9e2e3d89d6632dca736..f0538a1a9a092512d6619c291f393679a81e32e9 100644
--- a/api/user.go
+++ b/api/user.go
@@ -8,6 +8,7 @@
 package api
 
 import (
+	"gitlab.com/elixxir/crypto/diffieHellman"
 	"regexp"
 	"runtime"
 	"strings"
@@ -33,10 +34,10 @@ func createNewUser(rng *fastRNG.StreamGenerator, cmix,
 	e2e *cyclic.Group) user.Info {
 	// CMIX Keygen
 	var transmissionRsaKey, receptionRsaKey *rsa.PrivateKey
+	var e2eKey *cyclic.Int
+	var transmissionSalt, receptionSalt []byte
 
-	var e2eKeyBytes, transmissionSalt, receptionSalt []byte
-
-	e2eKeyBytes, transmissionSalt, receptionSalt,
+	transmissionSalt, receptionSalt, e2eKey,
 		transmissionRsaKey, receptionRsaKey = createKeys(rng, e2e)
 
 	// Salt, UID, etc gen
@@ -85,13 +86,14 @@ func createNewUser(rng *fastRNG.StreamGenerator, cmix,
 		ReceptionSalt:    receptionSalt,
 		ReceptionRSA:     receptionRsaKey,
 		Precanned:        false,
-		E2eDhPrivateKey:  e2e.NewIntFromBytes(e2eKeyBytes),
+		E2eDhPrivateKey:  e2eKey,
+		E2eDhPublicKey:   diffieHellman.GeneratePublicKey(e2eKey, e2e),
 	}
 }
 
 func createKeys(rng *fastRNG.StreamGenerator,
-	e2e *cyclic.Group) (e2eKeyBytes,
-	transmissionSalt, receptionSalt []byte,
+	e2e *cyclic.Group) (
+	transmissionSalt, receptionSalt []byte, e2eKey *cyclic.Int,
 	transmissionRsaKey, receptionRsaKey *rsa.PrivateKey) {
 	wg := sync.WaitGroup{}
 
@@ -101,9 +103,7 @@ func createKeys(rng *fastRNG.StreamGenerator,
 		defer wg.Done()
 		var err error
 		rngStream := rng.GetStream()
-		prime := e2e.GetPBytes()
-		keyLen := len(prime)
-		e2eKeyBytes, err = csprng.GenerateInGroup(prime, keyLen,
+		e2eKey = diffieHellman.GeneratePrivateKey(len(e2e.GetPBytes()), e2e,
 			rngStream)
 		rngStream.Close()
 		if err != nil {
@@ -148,10 +148,8 @@ func createNewVanityUser(rng csprng.Source, cmix,
 	// DH Keygen
 	prime := e2e.GetPBytes()
 	keyLen := len(prime)
-	e2eKeyBytes, err := csprng.GenerateInGroup(prime, keyLen, rng)
-	if err != nil {
-		jww.FATAL.Panicf(err.Error())
-	}
+
+	e2eKey := diffieHellman.GeneratePrivateKey(keyLen, e2e, rng)
 
 	// RSA Keygen (4096 bit defaults)
 	transmissionRsaKey, err := rsa.GenerateKey(rng, rsa.DefaultRSABitLen)
@@ -261,6 +259,7 @@ func createNewVanityUser(rng csprng.Source, cmix,
 		ReceptionSalt:    receptionSalt,
 		ReceptionRSA:     receptionRsaKey,
 		Precanned:        false,
-		E2eDhPrivateKey:  e2e.NewIntFromBytes(e2eKeyBytes),
+		E2eDhPrivateKey:  e2eKey,
+		E2eDhPublicKey:   diffieHellman.GeneratePublicKey(e2eKey, e2e),
 	}
 }
diff --git a/bindings/group.go b/bindings/group.go
index 3fce2e43b790c06dc64e0eb15490b5d795746060..761796628c249db88b718ad8a8935f8e765ab13e 100644
--- a/bindings/group.go
+++ b/bindings/group.go
@@ -20,7 +20,7 @@ import (
 
 // GroupChat object contains the group chat manager.
 type GroupChat struct {
-	m *gc.Manager
+	m gc.GroupChat
 }
 
 // GroupRequestFunc contains a function callback that is called when a group
@@ -48,7 +48,7 @@ func NewGroupManager(client *Client, requestFunc GroupRequestFunc,
 
 	// Create a new group chat manager
 	// TODO: Need things from storage, services, etc?
-	m, err := gc.NewManager(&client.api, requestCallback, receiveCallback)
+	m, err := gc.NewManager(client.api, requestCallback, receiveCallback)
 	if err != nil {
 		return nil, err
 	}
diff --git a/bindings/ud.go b/bindings/ud.go
index 23195fd751822311853a869cf92f0c4bbb2b48b5..ea8f5390abb2b749d14c50b10ec21b9bf8e596d5 100644
--- a/bindings/ud.go
+++ b/bindings/ud.go
@@ -41,8 +41,8 @@ func NewUserDiscovery(client *Client, username string) (*UserDiscovery, error) {
 	stream := client.api.GetRng().GetStream()
 	defer stream.Close()
 	m, err := udPackage.NewManager(client.api.GetNetworkInterface(),
-		client.api.GetE2e(), client.api.NetworkFollowerStatus,
-		client.api.GetEventManager(),
+		client.api.GetE2EHandler(), client.api.NetworkFollowerStatus,
+		client.api.GetEventReporter(),
 		client.api.GetComms(), client.api.GetStorage(),
 		stream,
 		username, client.api.GetStorage().GetKV())
@@ -60,7 +60,7 @@ func NewUserDiscovery(client *Client, username string) (*UserDiscovery, error) {
 // instantiation of the manager by NewUserDiscovery.
 func LoadUserDiscovery(client *Client) (*UserDiscovery, error) {
 	m, err := udPackage.LoadManager(client.api.GetNetworkInterface(),
-		client.api.GetE2e(), client.api.GetEventManager(),
+		client.api.GetE2EHandler(), client.api.GetEventReporter(),
 		client.api.GetComms(), client.api.GetStorage(),
 		client.api.GetStorage().GetKV())
 
@@ -119,7 +119,11 @@ func NewUserDiscoveryFromBackup(client *Client,
 			"registered phone number")
 	}
 
-	m, err := udPackage.NewManagerFromBackup(client.api.GetNetworkInterface(), client.api.GetE2e(), client.api.NetworkFollowerStatus, client.api.GetEventManager(), client.api.GetComms(), client.api.GetStorage(), emailFact, phoneFact, client.api.GetStorage().GetKV())
+	m, err := udPackage.NewManagerFromBackup(client.api.GetNetworkInterface(),
+		client.api.GetE2EHandler(), client.api.NetworkFollowerStatus,
+		client.api.GetEventReporter(), client.api.GetComms(),
+		client.api.GetStorage(), emailFact, phoneFact,
+		client.api.GetStorage().GetKV())
 	if err != nil {
 		return nil, errors.WithMessage(err,
 			"Failed to create User Discovery Manager")
@@ -228,8 +232,8 @@ func (ud UserDiscovery) Search(client *Client,
 	}
 
 	rids, _, err := udPackage.Search(
-		client.api.GetNetworkInterface(), client.api.GetEventManager(),
-		stream, client.api.GetE2e().GetGroup(), udContact,
+		client.api.GetNetworkInterface(), client.api.GetEventReporter(),
+		stream, client.api.GetE2EHandler().GetGroup(), udContact,
 		cb, factList, p)
 
 	if err != nil {
@@ -285,8 +289,8 @@ func (ud UserDiscovery) SearchSingle(client *Client, f string, callback SingleSe
 	}
 
 	rids, _, err := udPackage.Search(client.api.GetNetworkInterface(),
-		client.api.GetEventManager(),
-		stream, client.api.GetE2e().GetGroup(), udContact,
+		client.api.GetEventReporter(),
+		stream, client.api.GetE2EHandler().GetGroup(), udContact,
 		cb, []fact.Fact{fObj}, p)
 
 	if err != nil {
@@ -361,7 +365,7 @@ func (ud UserDiscovery) Lookup(client *Client,
 	}
 
 	rid, _, err := udPackage.Lookup(client.api.GetNetworkInterface(),
-		stream, client.api.GetE2e().GetGroup(),
+		stream, client.api.GetE2EHandler().GetGroup(),
 		udContact,
 		cb, uid, p)
 
@@ -447,7 +451,7 @@ func (ud UserDiscovery) MultiLookup(client *Client,
 			stream := client.api.GetRng().GetStream()
 			defer stream.Close()
 			_, _, err := udPackage.Lookup(client.api.GetNetworkInterface(),
-				stream, client.api.GetE2e().GetGroup(),
+				stream, client.api.GetE2EHandler().GetGroup(),
 				udContact, cb, localID, p)
 			if err != nil {
 				results <- lookupResponse{
diff --git a/cmd/group.go b/cmd/group.go
index df0c3902b838a644ab7403462bc47e149b240aa8..1f7e1140887961f1d8e29c1524ad1e216a17016e 100644
--- a/cmd/group.go
+++ b/cmd/group.go
@@ -112,7 +112,7 @@ var groupCmd = &cobra.Command{
 
 // initGroupManager creates a new group chat manager and starts the process
 // service.
-func initGroupManager(client *api.Client) (*groupChat.Manager,
+func initGroupManager(client *api.Client) (groupChat.GroupChat,
 	chan groupChat.MessageReceive, chan groupStore.Group) {
 	recChan := make(chan groupChat.MessageReceive, 10)
 	receiveCb := func(msg groupChat.MessageReceive) {
@@ -138,7 +138,7 @@ func initGroupManager(client *api.Client) (*groupChat.Manager,
 
 // createGroup creates a new group with the provided name and sends out requests
 // to the list of user IDs found at the given file path.
-func createGroup(name, msg []byte, filePath string, gm *groupChat.Manager) {
+func createGroup(name, msg []byte, filePath string, gm groupChat.GroupChat) {
 	userIdStrings := ReadLines(filePath)
 	userIDs := make([]*id.ID, 0, len(userIdStrings))
 	for _, userIdStr := range userIdStrings {
@@ -160,7 +160,7 @@ func createGroup(name, msg []byte, filePath string, gm *groupChat.Manager) {
 }
 
 // resendRequests resends group requests for the group ID.
-func resendRequests(groupIdString string, gm *groupChat.Manager) {
+func resendRequests(groupIdString string, gm groupChat.GroupChat) {
 	groupID, _ := parseRecipient(groupIdString)
 	rids, status, err := gm.ResendRequest(groupID)
 	if err != nil {
@@ -176,7 +176,7 @@ func resendRequests(groupIdString string, gm *groupChat.Manager) {
 // joinGroup joins a group when a request is received on the group request
 // channel.
 func joinGroup(reqChan chan groupStore.Group, timeout time.Duration,
-	gm *groupChat.Manager) {
+	gm groupChat.GroupChat) {
 	jww.INFO.Print("Waiting for group request to be received.")
 	fmt.Println("Waiting for group request to be received.")
 
@@ -198,7 +198,7 @@ func joinGroup(reqChan chan groupStore.Group, timeout time.Duration,
 }
 
 // leaveGroup leaves the group.
-func leaveGroup(groupIdString string, gm *groupChat.Manager) {
+func leaveGroup(groupIdString string, gm groupChat.GroupChat) {
 	groupID, _ := parseRecipient(groupIdString)
 	jww.INFO.Printf("Leaving group %s.", groupID)
 
@@ -212,7 +212,7 @@ func leaveGroup(groupIdString string, gm *groupChat.Manager) {
 }
 
 // sendGroup send the message to the group.
-func sendGroup(groupIdString string, msg []byte, gm *groupChat.Manager) {
+func sendGroup(groupIdString string, msg []byte, gm groupChat.GroupChat) {
 	groupID, _ := parseRecipient(groupIdString)
 
 	jww.INFO.Printf("Sending to group %s message %q", groupID, msg)
@@ -249,7 +249,7 @@ func messageWait(numMessages uint, timeout time.Duration,
 }
 
 // listGroups prints a list of all groups.
-func listGroups(gm *groupChat.Manager) {
+func listGroups(gm groupChat.GroupChat) {
 	for i, gid := range gm.GetGroups() {
 		jww.INFO.Printf("Group %d: %s", i, gid)
 	}
@@ -258,7 +258,7 @@ func listGroups(gm *groupChat.Manager) {
 }
 
 // showGroup prints all the information of the group.
-func showGroup(groupIdString string, gm *groupChat.Manager) {
+func showGroup(groupIdString string, gm groupChat.GroupChat) {
 	groupID, _ := parseRecipient(groupIdString)
 
 	grp, ok := gm.GetGroup(groupID)
diff --git a/cmd/ud.go b/cmd/ud.go
index 9a74663735f4e696949267d94addc1b27660dbfc..431f774f3f6d56f81fca53f2709b1f00903caf9a 100644
--- a/cmd/ud.go
+++ b/cmd/ud.go
@@ -10,6 +10,13 @@ package cmd
 
 import (
 	"fmt"
+	"gitlab.com/elixxir/client/single"
+	"gitlab.com/elixxir/client/ud"
+	"gitlab.com/elixxir/client/xxmutils"
+	"gitlab.com/elixxir/primitives/fact"
+	"gitlab.com/xx_network/primitives/utils"
+	"strings"
+	"time"
 
 	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
@@ -26,12 +33,12 @@ var udCmd = &cobra.Command{
 	Short: "Register for and search users using the xx network user discovery service.",
 	Args:  cobra.NoArgs,
 	Run: func(cmd *cobra.Command, args []string) {
-		// client := initClient()
+		client := initClient()
 
-		// // get user and save contact to file
-		// user := client.GetUser()
-		// jww.INFO.Printf("User: %s", user.ReceptionID)
-		// writeContact(user.GetContact())
+		// get user and save contact to file
+		user := client.GetUser()
+		jww.INFO.Printf("User: %s", user.ReceptionID)
+		writeContact(user.GetContact())
 
 		// // Set up reception handler
 		// swBoard := client.GetSwitchboard()
@@ -57,194 +64,211 @@ var udCmd = &cobra.Command{
 		// 	})
 		// }
 
-		// err := client.StartNetworkFollower(50 * time.Millisecond)
-		// if err != nil {
-		// 	jww.FATAL.Panicf("%+v", err)
-		// }
-
-		// // Wait until connected or crash on timeout
-		// connected := make(chan bool, 10)
-		// client.GetHealth().AddChannel(connected)
-		// waitUntilConnected(connected)
-
-		// // Make single-use manager and start receiving process
-		// singleMng := old.NewManager(client)
-		// err = client.AddService(singleMng.StartProcesses)
-		// if err != nil {
-		// 	jww.FATAL.Panicf("Failed to add single use process: %+v", err)
-		// }
-
-		// // Make user discovery manager
-		// userDiscoveryMgr, err := ud.NewManager(client, singleMng)
-		// if err != nil {
-		// 	jww.FATAL.Panicf("Failed to create new UD manager: %+v", err)
-		// }
-
-		// userToRegister := viper.GetString("register")
-		// if userToRegister != "" {
-		// 	err = userDiscoveryMgr.Register(userToRegister)
-		// 	if err != nil {
-		// 		fmt.Printf("Failed to register user %s: %s\n",
-		// 			userToRegister, err.Error())
-		// 		jww.FATAL.Panicf("Failed to register user %s: %+v", userToRegister, err)
-		// 	}
-		// }
-
-		// var newFacts fact.FactList
-		// phone := viper.GetString("addphone")
-		// if phone != "" {
-		// 	f, err := fact.NewFact(fact.Phone, phone)
-		// 	if err != nil {
-		// 		jww.FATAL.Panicf("Failed to create new fact: %+v", err)
-		// 	}
-		// 	newFacts = append(newFacts, f)
-		// }
-
-		// email := viper.GetString("addemail")
-		// if email != "" {
-		// 	f, err := fact.NewFact(fact.Email, email)
-		// 	if err != nil {
-		// 		jww.FATAL.Panicf("Failed to create new fact: %+v", err)
-		// 	}
-		// 	newFacts = append(newFacts, f)
-		// }
-
-		// for i := 0; i < len(newFacts); i++ {
-		// 	r, err := userDiscoveryMgr.SendRegisterFact(newFacts[i])
-		// 	if err != nil {
-		// 		fmt.Printf("Failed to register fact: %s\n",
-		// 			newFacts[i])
-		// 		jww.FATAL.Panicf("Failed to send register fact: %+v", err)
-		// 	}
-		// 	// TODO Store the code?
-		// 	jww.INFO.Printf("Fact Add Response: %+v", r)
-		// }
-
-		// confirmID := viper.GetString("confirm")
-		// if confirmID != "" {
-		// 	err = userDiscoveryMgr.SendConfirmFact(confirmID, confirmID)
-		// 	if err != nil {
-		// 		fmt.Printf("Couldn't confirm fact: %s\n",
-		// 			err.Error())
-		// 		jww.FATAL.Panicf("%+v", err)
-		// 	}
-		// }
-
-		// // Handle lookup (verification) process
-		// // Note: Cryptographic verification occurs above the bindings layer
-		// lookupIDStr := viper.GetString("lookup")
-		// if lookupIDStr != "" {
-		// 	lookupID, _ := parseRecipient(lookupIDStr)
-		// 	//if !ok {
-		// 	//	jww.FATAL.Panicf("Could not parse recipient: %s", lookupIDStr)
-		// 	//}
-		// 	err = userDiscoveryMgr.Lookup(lookupID,
-		// 		func(newContact contact.Contact, err error) {
-		// 			if err != nil {
-		// 				jww.FATAL.Panicf("UserDiscovery Lookup error: %+v", err)
-		// 			}
-		// 			printContact(newContact)
-		// 		}, 30*time.Second)
-
-		// 	if err != nil {
-		// 		jww.WARN.Printf("Failed UD lookup: %+v", err)
-		// 	}
-
-		// 	time.Sleep(31 * time.Second)
-		// }
-
-		// if viper.GetString("batchadd") != "" {
-		// 	idListFile, err := utils.ReadFile(viper.GetString("batchadd"))
-		// 	if err != nil {
-		// 		fmt.Printf("BATCHADD: Couldn't read file: %s\n",
-		// 			err.Error())
-		// 		jww.FATAL.Panicf("BATCHADD: Couldn't read file: %+v", err)
-		// 	}
-		// 	restored, _, _, err := xxmutils.RestoreContactsFromBackup(
-		// 		idListFile, client, userDiscoveryMgr, nil)
-		// 	if err != nil {
-		// 		jww.FATAL.Panicf("%+v", err)
-		// 	}
-		// 	for i := 0; i < len(restored); i++ {
-		// 		uid := restored[i]
-		// 		for !client.HasAuthenticatedChannel(uid) {
-		// 			time.Sleep(time.Second)
-		// 		}
-		// 		jww.INFO.Printf("Authenticated channel established for %s", uid)
-		// 	}
-		// }
-		// usernameSearchStr := viper.GetString("searchusername")
-		// emailSearchStr := viper.GetString("searchemail")
-		// phoneSearchStr := viper.GetString("searchphone")
-
-		// var facts fact.FactList
-		// if usernameSearchStr != "" {
-		// 	f, err := fact.NewFact(fact.Username, usernameSearchStr)
-		// 	if err != nil {
-		// 		jww.FATAL.Panicf("Failed to create new fact: %+v", err)
-		// 	}
-		// 	facts = append(facts, f)
-		// }
-		// if emailSearchStr != "" {
-		// 	f, err := fact.NewFact(fact.Email, emailSearchStr)
-		// 	if err != nil {
-		// 		jww.FATAL.Panicf("Failed to create new fact: %+v", err)
-		// 	}
-		// 	facts = append(facts, f)
-		// }
-		// if phoneSearchStr != "" {
-		// 	f, err := fact.NewFact(fact.Phone, phoneSearchStr)
-		// 	if err != nil {
-		// 		jww.FATAL.Panicf("Failed to create new fact: %+v", err)
-		// 	}
-		// 	facts = append(facts, f)
-		// }
-
-		// userToRemove := viper.GetString("remove")
-		// if userToRemove != "" {
-		// 	f, err := fact.NewFact(fact.Username, userToRemove)
-		// 	if err != nil {
-		// 		jww.FATAL.Panicf(
-		// 			"Failed to create new fact: %+v", err)
-		// 	}
-		// 	err = userDiscoveryMgr.RemoveUser(f)
-		// 	if err != nil {
-		// 		fmt.Printf("Couldn't remove user %s\n",
-		// 			userToRemove)
-		// 		jww.FATAL.Panicf(
-		// 			"Failed to remove user %s: %+v",
-		// 			userToRemove, err)
-		// 	}
-		// 	fmt.Printf("Removed user from discovery: %s\n",
-		// 		userToRemove)
-		// }
-
-		// if len(facts) == 0 {
-		// 	err = client.StopNetworkFollower()
-		// 	if err != nil {
-		// 		jww.WARN.Print(err)
-		// 	}
-		// 	return
-		// }
-
-		// err = userDiscoveryMgr.Search(facts,
-		// 	func(contacts []contact.Contact, err error) {
-		// 		if err != nil {
-		// 			jww.FATAL.Panicf("%+v", err)
-		// 		}
-		// 		for _, c := range contacts {
-		// 			printContact(c)
-		// 		}
-		// 	}, 90*time.Second)
-		// if err != nil {
-		// 	jww.FATAL.Panicf("%+v", err)
-		// }
-
-		// time.Sleep(91 * time.Second)
-		// err = client.StopNetworkFollower()
-		// if err != nil {
-		// 	jww.WARN.Print(err)
-		// }
+		err := client.StartNetworkFollower(50 * time.Millisecond)
+		if err != nil {
+			jww.FATAL.Panicf("%+v", err)
+		}
+
+		// Wait until connected or crash on timeout
+		connected := make(chan bool, 10)
+		client.GetNetworkInterface().AddHealthCallback(
+			func(isconnected bool) {
+				connected <- isconnected
+			})
+		waitUntilConnected(connected)
+
+		// Make user discovery manager
+		stream := client.GetRng().GetStream()
+		defer stream.Close()
+		userToRegister := viper.GetString("register")
+		userDiscoveryMgr, err := ud.NewManager(client.GetNetworkInterface(),
+			client.GetE2EHandler(), client.NetworkFollowerStatus,
+			client.GetEventReporter(),
+			client.GetComms(), client.GetStorage(),
+			stream,
+			userToRegister, client.GetStorage().GetKV())
+		if err != nil {
+			if strings.Contains(err.Error(), ud.IsRegisteredErr) {
+				userDiscoveryMgr, err = ud.LoadManager(client.GetNetworkInterface(),
+					client.GetE2EHandler(), client.GetEventReporter(),
+					client.GetComms(),
+					client.GetStorage(), client.GetStorage().GetKV())
+				if err != nil {
+					jww.FATAL.Panicf("Failed to load UD manager: %+v", err)
+				}
+			} else {
+				jww.FATAL.Panicf("Failed to create new UD manager: %+v", err)
+
+			}
+		}
+
+		var newFacts fact.FactList
+		phone := viper.GetString("addphone")
+		if phone != "" {
+			f, err := fact.NewFact(fact.Phone, phone)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to create new fact: %+v", err)
+			}
+			newFacts = append(newFacts, f)
+		}
+
+		email := viper.GetString("addemail")
+		if email != "" {
+			f, err := fact.NewFact(fact.Email, email)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to create new fact: %+v", err)
+			}
+			newFacts = append(newFacts, f)
+		}
+
+		for i := 0; i < len(newFacts); i++ {
+			r, err := userDiscoveryMgr.SendRegisterFact(newFacts[i])
+			if err != nil {
+				fmt.Printf("Failed to register fact: %s\n",
+					newFacts[i])
+				jww.FATAL.Panicf("Failed to send register fact: %+v", err)
+			}
+			// TODO Store the code?
+			jww.INFO.Printf("Fact Add Response: %+v", r)
+		}
+
+		confirmID := viper.GetString("confirm")
+		if confirmID != "" {
+			err = userDiscoveryMgr.ConfirmFact(confirmID, confirmID)
+			if err != nil {
+				fmt.Printf("Couldn't confirm fact: %s\n",
+					err.Error())
+				jww.FATAL.Panicf("%+v", err)
+			}
+		}
+
+		udContact, err := userDiscoveryMgr.GetContact()
+		if err != nil {
+			fmt.Printf("Failed to get user discovery contact object: %+v", err)
+			jww.FATAL.Printf("Failed to get user discovery contact object: %+v", err)
+		}
+
+		// Handle lookup (verification) process
+		// Note: Cryptographic verification occurs above the bindings layer
+		lookupIDStr := viper.GetString("lookup")
+		if lookupIDStr != "" {
+			lookupID, _ := parseRecipient(lookupIDStr)
+			//if !ok {
+			//	jww.FATAL.Panicf("Could not parse recipient: %s", lookupIDStr)
+			//}
+
+			cb := func(newContact contact.Contact, err error) {
+				if err != nil {
+					jww.FATAL.Panicf("UserDiscovery Lookup error: %+v", err)
+				}
+				printContact(newContact)
+			}
+			_, _, err = ud.Lookup(client.GetNetworkInterface(),
+				stream, client.GetE2EHandler().GetGroup(),
+				udContact, cb, lookupID, single.GetDefaultRequestParams())
+			if err != nil {
+				jww.WARN.Printf("Failed UD lookup: %+v", err)
+			}
+
+			time.Sleep(31 * time.Second)
+		}
+
+		if viper.GetString("batchadd") != "" {
+			idListFile, err := utils.ReadFile(viper.GetString("batchadd"))
+			if err != nil {
+				fmt.Printf("BATCHADD: Couldn't read file: %s\n",
+					err.Error())
+				jww.FATAL.Panicf("BATCHADD: Couldn't read file: %+v", err)
+			}
+			restored, _, _, err := xxmutils.RestoreContactsFromBackup(
+				idListFile, client, userDiscoveryMgr, nil)
+			if err != nil {
+				jww.FATAL.Panicf("%+v", err)
+			}
+			for i := 0; i < len(restored); i++ {
+				uid := restored[i]
+				for !client.HasAuthenticatedChannel(uid) {
+					time.Sleep(time.Second)
+				}
+				jww.INFO.Printf("Authenticated channel established for %s", uid)
+			}
+		}
+		usernameSearchStr := viper.GetString("searchusername")
+		emailSearchStr := viper.GetString("searchemail")
+		phoneSearchStr := viper.GetString("searchphone")
+
+		var facts fact.FactList
+		if usernameSearchStr != "" {
+			f, err := fact.NewFact(fact.Username, usernameSearchStr)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to create new fact: %+v", err)
+			}
+			facts = append(facts, f)
+		}
+		if emailSearchStr != "" {
+			f, err := fact.NewFact(fact.Email, emailSearchStr)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to create new fact: %+v", err)
+			}
+			facts = append(facts, f)
+		}
+		if phoneSearchStr != "" {
+			f, err := fact.NewFact(fact.Phone, phoneSearchStr)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to create new fact: %+v", err)
+			}
+			facts = append(facts, f)
+		}
+
+		userToRemove := viper.GetString("remove")
+		if userToRemove != "" {
+			f, err := fact.NewFact(fact.Username, userToRemove)
+			if err != nil {
+				jww.FATAL.Panicf(
+					"Failed to create new fact: %+v", err)
+			}
+			err = userDiscoveryMgr.PermanentDeleteAccount(f)
+			if err != nil {
+				fmt.Printf("Couldn't remove user %s\n",
+					userToRemove)
+				jww.FATAL.Panicf(
+					"Failed to remove user %s: %+v",
+					userToRemove, err)
+			}
+			fmt.Printf("Removed user from discovery: %s\n",
+				userToRemove)
+		}
+
+		if len(facts) == 0 {
+			err = client.StopNetworkFollower()
+			if err != nil {
+				jww.WARN.Print(err)
+			}
+			return
+		}
+
+		cb := func(contacts []contact.Contact, err error) {
+			if err != nil {
+				jww.FATAL.Panicf("%+v", err)
+			}
+			for _, c := range contacts {
+				printContact(c)
+			}
+		}
+
+		_, _, err = ud.Search(client.GetNetworkInterface(),
+			client.GetEventReporter(),
+			stream, client.GetE2EHandler().GetGroup(),
+			udContact, cb, facts, single.GetDefaultRequestParams())
+		if err != nil {
+			jww.FATAL.Panicf("%+v", err)
+		}
+
+		time.Sleep(91 * time.Second)
+		err = client.StopNetworkFollower()
+		if err != nil {
+			jww.WARN.Print(err)
+		}
 	},
 }
 
diff --git a/cmix/remoteFilters.go b/cmix/remoteFilters.go
index 45afbd1a3d1ae7cecbb57a6d47cda17af34f3ba8..37cf100e60396c612df73e85b31e5d07c5688d2f 100644
--- a/cmix/remoteFilters.go
+++ b/cmix/remoteFilters.go
@@ -25,10 +25,10 @@ func NewRemoteFilter(data *mixmessages.ClientBloom) *RemoteFilter {
 
 type RemoteFilter struct {
 	data   *mixmessages.ClientBloom
-	filter *bloom.Ring
+	filter *bloom.Bloom
 }
 
-func (rf *RemoteFilter) GetFilter() *bloom.Ring {
+func (rf *RemoteFilter) GetFilter() *bloom.Bloom {
 
 	if rf.filter == nil {
 		var err error
diff --git a/go.mod b/go.mod
index 412d59c59862e11e2ea681e4b3048c8c49f0ade2..5cc7bdbc98c27ad9aad0f873ef32a516cc24c9f8 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/spf13/cobra v1.1.1
 	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/bloomfilter v0.0.0-20211222005329-7d931ceead6f
 	gitlab.com/elixxir/comms v0.0.4-0.20220323190139-9ed75f3a8b2c
 	gitlab.com/elixxir/crypto v0.0.7-0.20220425192911-a23209a58073
 	gitlab.com/elixxir/ekv v0.1.7
diff --git a/go.sum b/go.sum
index 449abdbda8c4778ece9f8de9852071e33cf18f92..b2b24d4d75babae71f71578a773220de0cf06b99 100644
--- a/go.sum
+++ b/go.sum
@@ -274,6 +274,8 @@ 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/bloomfilter v0.0.0-20211222005329-7d931ceead6f h1:yXGvNBqzZwAhDYlSnxPRbgor6JWoOt1Z7s3z1O9JR40=
+gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k=
 gitlab.com/elixxir/comms v0.0.4-0.20220308183624-c2183e687a03 h1:4eNjO3wCyHgxpGeq2zgDb5SsdTcQaG5IZjBOuEL6KgM=
 gitlab.com/elixxir/comms v0.0.4-0.20220308183624-c2183e687a03/go.mod h1:4yMdU+Jee5W9lqkZGHJAuipEhW7FloT0eyVEFUJza+E=
 gitlab.com/elixxir/comms v0.0.4-0.20220323190139-9ed75f3a8b2c h1:ajjTw08YjRjl3HvtBNGtoCWhOg8k8upqmTweH18wkC4=
@@ -283,6 +285,8 @@ gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp0
 gitlab.com/elixxir/crypto v0.0.7-0.20220222221347-95c7ae58da6b/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10=
 gitlab.com/elixxir/crypto v0.0.7-0.20220309234716-1ba339865787 h1:+qmsWov412+Yn7AKUhTbOcDgAydNXlNLPmFpO2W5LwY=
 gitlab.com/elixxir/crypto v0.0.7-0.20220309234716-1ba339865787/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10=
+gitlab.com/elixxir/crypto v0.0.7-0.20220317172048-3de167bd9406 h1:PRA8OJMXuy9JZmUuZ442AIE/tWY7HisqezyLNhpZZ9w=
+gitlab.com/elixxir/crypto v0.0.7-0.20220317172048-3de167bd9406/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10=
 gitlab.com/elixxir/crypto v0.0.7-0.20220325215559-7489d68d7714 h1:epnov8zyFWod14MUNtGHSbZCVSkZjN4NvoiBs1TgEV8=
 gitlab.com/elixxir/crypto v0.0.7-0.20220325215559-7489d68d7714/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10=
 gitlab.com/elixxir/crypto v0.0.7-0.20220325224306-705ce59288bb h1:WdlmG+KPaM2Pjo1EFiFFPYEVSMV64Di1CitQnXGWBOQ=
diff --git a/groupChat/group.go b/groupChat/group.go
index 8878a62cdf750823bd4c886fbfbfdca1ac81c13d..80f98edc0643867bfafce987a856f132131af858 100644
--- a/groupChat/group.go
+++ b/groupChat/group.go
@@ -21,6 +21,7 @@ package groupChat
 
 import (
 	gs "gitlab.com/elixxir/client/groupChat/groupStore"
+	"gitlab.com/elixxir/crypto/group"
 	"gitlab.com/xx_network/primitives/id"
 	"time"
 )
@@ -52,7 +53,8 @@ type GroupChat interface {
 	// Send sends a message to all GroupChat members using Client.SendManyCMIX.
 	// The send fails if the message is too long. Returns the ID of the round
 	// sent on and the timestamp of the message send.
-	Send(groupID *id.ID, message []byte) (id.Round, time.Time, error)
+	Send(groupID *id.ID, tag string, message []byte) (
+		id.Round, time.Time, group.MessageID, error)
 
 	// GetGroups returns a list of all registered GroupChat IDs.
 	GetGroups() []*id.ID
@@ -63,6 +65,15 @@ type GroupChat interface {
 
 	// NumGroups returns the number of groups the user is a part of.
 	NumGroups() int
+
+	/* ===== Services ======================================================= */
+
+	// AddService adds a service for all group chat partners of the given tag,
+	// which will call back on the given processor.
+	AddService(g gs.Group, tag string, p Processor)
+
+	// RemoveService removes all services for the given tag.
+	RemoveService(g gs.Group, tag string, p Processor)
 }
 
 // RequestCallback is called when a GroupChat request is received.
diff --git a/groupChat/groupStore/group.go b/groupChat/groupStore/group.go
index 28b9555e367cf7f761dd437de792bac97fc6789b..2c7d3e91739af0494f74316fac517b937cd86e86 100644
--- a/groupChat/groupStore/group.go
+++ b/groupChat/groupStore/group.go
@@ -244,17 +244,17 @@ func (g Group) GoString() string {
 		idString = g.ID.String()
 	}
 
-	str := make([]string, 9)
-
-	str[0] = "Name:" + fmt.Sprintf("%q", g.Name)
-	str[1] = "ID:" + idString
-	str[2] = "Key:" + g.Key.String()
-	str[3] = "IdPreimage:" + g.IdPreimage.String()
-	str[4] = "KeyPreimage:" + g.KeyPreimage.String()
-	str[5] = "InitMessage:" + fmt.Sprintf("%q", g.InitMessage)
-	str[6] = "Created:" + g.Created.String()
-	str[7] = "Members:" + g.Members.String()
-	str[8] = "DhKeys:" + g.DhKeys.GoString()
+	str := []string{
+		"Name:" + fmt.Sprintf("%q", g.Name),
+		"ID:" + idString,
+		"Key:" + g.Key.String(),
+		"IdPreimage:" + g.IdPreimage.String(),
+		"KeyPreimage:" + g.KeyPreimage.String(),
+		"InitMessage:" + fmt.Sprintf("%q", g.InitMessage),
+		"Created:" + g.Created.String(),
+		"Members:" + g.Members.String(),
+		"DhKeys:" + g.DhKeys.GoString(),
+	}
 
 	return "{" + strings.Join(str, ", ") + "}"
 }
diff --git a/groupChat/groupStore/store_test.go b/groupChat/groupStore/store_test.go
index e19b3928bd26eeffa17c040b973965f64927dfa5..2f52d9a3106b9b032f48bee5815ca422241cac3a 100644
--- a/groupChat/groupStore/store_test.go
+++ b/groupChat/groupStore/store_test.go
@@ -24,7 +24,7 @@ import (
 // Unit test of NewStore.
 func TestNewStore(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 
 	expectedStore := &Store{
@@ -90,7 +90,7 @@ func TestNewStore(t *testing.T) {
 
 func TestNewOrLoadStore(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 
 	store, err := NewOrLoadStore(kv, user)
@@ -126,7 +126,7 @@ func TestNewOrLoadStore(t *testing.T) {
 // Unit test of LoadStore.
 func TestLoadStore(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 
 	store, err := NewStore(kv, user)
@@ -162,7 +162,7 @@ func TestLoadStore(t *testing.T) {
 // Error path: show that LoadStore returns an error when no group store can be
 // found in storage.
 func TestLoadStore_GetError(t *testing.T) {
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(rand.New(rand.NewSource(42)))
 	expectedErr := strings.SplitN(kvGetGroupListErr, "%", 2)[0]
 
@@ -177,7 +177,7 @@ func TestLoadStore_GetError(t *testing.T) {
 // Error path: show that loadStore returns an error when no group can be found
 // in storage.
 func Test_loadStore_GetGroupError(t *testing.T) {
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(rand.New(rand.NewSource(42)))
 	var idList []byte
 	for i := 0; i < 10; i++ {
@@ -249,7 +249,7 @@ func TestStore_Len(t *testing.T) {
 // Unit test of Store.Add.
 func TestStore_Add(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 
 	store, err := NewStore(kv, user)
@@ -281,7 +281,7 @@ func TestStore_Add(t *testing.T) {
 // groups.
 func TestStore_Add_MapFullError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 	expectedErr := strings.SplitN(maxGroupsErr, "%", 2)[0]
 
@@ -309,7 +309,7 @@ func TestStore_Add_MapFullError(t *testing.T) {
 // that is already in the map.
 func TestStore_Add_GroupExistsError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 	expectedErr := strings.SplitN(groupExistsErr, "%", 2)[0]
 
@@ -334,7 +334,7 @@ func TestStore_Add_GroupExistsError(t *testing.T) {
 // Unit test of Store.Remove.
 func TestStore_Remove(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 
 	store, err := NewStore(kv, user)
@@ -374,7 +374,7 @@ func TestStore_Remove(t *testing.T) {
 // given ID is found in the map.
 func TestStore_Remove_RemoveGroupNotInMemoryError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 	expectedErr := strings.SplitN(groupRemoveErr, "%", 2)[0]
 
@@ -423,7 +423,7 @@ func TestStore_GroupIDs(t *testing.T) {
 // Unit test of Store.Get.
 func TestStore_Get(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 
 	store, err := NewStore(kv, user)
@@ -451,7 +451,7 @@ func TestStore_Get(t *testing.T) {
 
 // Error path: shows that Store.Get return false if no group is found.
 func TestStore_Get_NoGroupError(t *testing.T) {
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(rand.New(rand.NewSource(42)))
 
 	store, err := NewStore(kv, user)
@@ -469,7 +469,7 @@ func TestStore_Get_NoGroupError(t *testing.T) {
 // Unit test of Store.GetByKeyFp.
 func TestStore_GetByKeyFp(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 
 	store, err := NewStore(kv, user)
@@ -502,7 +502,7 @@ func TestStore_GetByKeyFp(t *testing.T) {
 // Error path: shows that Store.GetByKeyFp return false if no group is found.
 func TestStore_GetByKeyFp_NoGroupError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(prng)
 
 	store, err := NewStore(kv, user)
@@ -523,7 +523,7 @@ func TestStore_GetByKeyFp_NoGroupError(t *testing.T) {
 
 // Unit test of Store.GetUser.
 func TestStore_GetUser(t *testing.T) {
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := randMember(rand.New(rand.NewSource(42)))
 
 	store, err := NewStore(kv, user)
@@ -539,7 +539,7 @@ func TestStore_GetUser(t *testing.T) {
 
 // Unit test of Store.SetUser.
 func TestStore_SetUser(t *testing.T) {
-	kv := versioned.NewKV(make(ekv.Memstore))
+	kv := versioned.NewKV(ekv.MakeMemstore())
 	prng := rand.New(rand.NewSource(42))
 	oldUser := randMember(prng)
 	newUser := randMember(prng)
diff --git a/groupChat/makeGroup.go b/groupChat/makeGroup.go
index 32be2b8318562da54cf0841cdda018aa411db58d..e7bee96972e5a015bf3afa2ac00f2be49dc3adb5 100644
--- a/groupChat/makeGroup.go
+++ b/groupChat/makeGroup.go
@@ -50,7 +50,7 @@ const (
 // each member of the groupChat to add them to the groupChat. It blocks until
 // all the groupChat requests are sent. Returns an error if at least one request
 // to a member fails to send.
-func (m Manager) MakeGroup(membership []*id.ID, name, msg []byte) (gs.Group,
+func (m *manager) MakeGroup(membership []*id.ID, name, msg []byte) (gs.Group,
 	[]id.Round, RequestStatus, error) {
 	// Return an error if the message is too long
 	if len(msg) > MaxInitMessageSize {
@@ -81,7 +81,7 @@ func (m Manager) MakeGroup(membership []*id.ID, name, msg []byte) (gs.Group,
 	g := gs.NewGroup(
 		name, groupID, groupKey, idPreimage, keyPreimage, msg, created, mem, dkl)
 
-	jww.DEBUG.Printf("Created new group %q with ID %s and %d members %s",
+	jww.DEBUG.Printf("[GC] Created new group %q with ID %s and %d members %s",
 		g.Name, g.ID, len(g.Members), g.Members)
 
 	// Send all group requests
@@ -96,7 +96,7 @@ func (m Manager) MakeGroup(membership []*id.ID, name, msg []byte) (gs.Group,
 // buildMembership retrieves the contact object for each member ID and creates a
 // new membership from them. The caller is set as the leader. For a member to be
 // added, the group leader must have an authenticated channel with the member.
-func (m Manager) buildMembership(members []*id.ID) (group.Membership,
+func (m *manager) buildMembership(members []*id.ID) (group.Membership,
 	gs.DhKeyList, error) {
 	// Return an error if the membership list has too few or too many members
 	if len(members) < group.MinParticipants {
diff --git a/groupChat/makeGroup_test.go b/groupChat/makeGroup_test.go
index a9541b84471d9d35db12787731916d761f5294e7..7ccccf4fadd8996fa147107f0f65bf037c5e39ee 100644
--- a/groupChat/makeGroup_test.go
+++ b/groupChat/makeGroup_test.go
@@ -25,8 +25,8 @@ import (
 	"testing"
 )
 
-// Tests that Manager.MakeGroup adds a group and returns the expected status.
-func TestManager_MakeGroup(t *testing.T) {
+// Tests that manager.MakeGroup adds a group and returns the expected status.
+func Test_manager_MakeGroup(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 	memberIDs, members, dkl := addPartners(m, t)
@@ -76,7 +76,7 @@ func TestManager_MakeGroup(t *testing.T) {
 
 // Error path: make sure an error and the correct status is returned when the
 // message is too large.
-func TestManager_MakeGroup_MaxMessageSizeError(t *testing.T) {
+func Test_manager_MakeGroup_MaxMessageSizeError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 	expectedErr := fmt.Sprintf(
@@ -96,7 +96,7 @@ func TestManager_MakeGroup_MaxMessageSizeError(t *testing.T) {
 
 // Error path: make sure an error and the correct status is returned when the
 // membership list is too small.
-func TestManager_MakeGroup_MembershipSizeError(t *testing.T) {
+func Test_manager_MakeGroup_MembershipSizeError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 	expectedErr := fmt.Sprintf(
@@ -117,7 +117,7 @@ func TestManager_MakeGroup_MembershipSizeError(t *testing.T) {
 
 // Error path: make sure an error and the correct status is returned when adding
 // a group failed because the user is a part of too many groups already.
-func TestManager_MakeGroup_AddGroupError(t *testing.T) {
+func Test_manager_MakeGroup_AddGroupError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, gs.MaxGroupChats, 0, nil, nil, t)
 	memberIDs, _, _ := addPartners(m, t)
@@ -130,8 +130,8 @@ func TestManager_MakeGroup_AddGroupError(t *testing.T) {
 	}
 }
 
-// Unit test of Manager.buildMembership.
-func TestManager_buildMembership(t *testing.T) {
+// Unit test of manager.buildMembership.
+func Test_manager_buildMembership(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManager(prng, t)
 	memberIDs, expected, expectedDKL := addPartners(m, t)
@@ -154,7 +154,7 @@ func TestManager_buildMembership(t *testing.T) {
 
 // Error path: an error is returned when the number of members in the membership
 // list is too few.
-func TestManager_buildMembership_MinParticipantsError(t *testing.T) {
+func Test_manager_buildMembership_MinParticipantsError(t *testing.T) {
 	m, _ := newTestManager(rand.New(rand.NewSource(42)), t)
 	memberIDs := make([]*id.ID, group.MinParticipants-1)
 	expectedErr := fmt.Sprintf(
@@ -169,7 +169,7 @@ func TestManager_buildMembership_MinParticipantsError(t *testing.T) {
 
 // Error path: an error is returned when the number of members in the membership
 // list is too many.
-func TestManager_buildMembership_MaxParticipantsError(t *testing.T) {
+func Test_manager_buildMembership_MaxParticipantsError(t *testing.T) {
 	m, _ := newTestManager(rand.New(rand.NewSource(42)), t)
 	memberIDs := make([]*id.ID, group.MaxParticipants+1)
 	expectedErr := fmt.Sprintf(
@@ -183,7 +183,7 @@ func TestManager_buildMembership_MaxParticipantsError(t *testing.T) {
 }
 
 // Error path: error returned when a partner cannot be found
-func TestManager_buildMembership_GetPartnerContactError(t *testing.T) {
+func Test_manager_buildMembership_GetPartnerContactError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManager(prng, t)
 	memberIDs, _, _ := addPartners(m, t)
@@ -200,7 +200,7 @@ func TestManager_buildMembership_GetPartnerContactError(t *testing.T) {
 }
 
 // Error path: error returned when a member ID appears twice on the list.
-func TestManager_buildMembership_DuplicateContactError(t *testing.T) {
+func Test_manager_buildMembership_DuplicateContactError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManager(prng, t)
 	memberIDs, _, _ := addPartners(m, t)
@@ -281,7 +281,7 @@ func TestRequestStatus_Message(t *testing.T) {
 
 // addPartners returns a list of user IDs and their matching membership and adds
 // them as partners.
-func addPartners(m *Manager, t *testing.T) ([]*id.ID, group.Membership,
+func addPartners(m *manager, t *testing.T) ([]*id.ID, group.Membership,
 	gs.DhKeyList) {
 	memberIDs := make([]*id.ID, 10)
 	members := group.Membership{m.gs.GetUser()}
diff --git a/groupChat/manager.go b/groupChat/manager.go
index 19c4de8bbca37c62fb7284e9c52701b865f7abba..9b018013ec9dcc7b9bc1ae0d295982e1e6e25617 100644
--- a/groupChat/manager.go
+++ b/groupChat/manager.go
@@ -36,37 +36,36 @@ const (
 	leaveGroupErr    = "failed to leave group %s: %+v"
 )
 
-// GroupCmix is a subset of the cmix.Client interface containing only the methods needed by GroupChat
+// GroupCmix is a subset of the cmix.Client interface containing only the
+// methods needed by GroupChat
 type GroupCmix interface {
 	SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (
 		id.Round, []ephemeral.Id, error)
-	AddService(clientID *id.ID, newService message.Service,
-		response message.Processor)
-	DeleteService(clientID *id.ID, toDelete message.Service,
-		processor message.Processor)
+	AddService(
+		clientID *id.ID, newService message.Service, response message.Processor)
+	DeleteService(
+		clientID *id.ID, toDelete message.Service, processor message.Processor)
 	GetMaxMessageLength() int
 }
 
-// GroupE2e is a subset of the e2e.Handler interface containing only the methods needed by GroupChat
+// GroupE2e is a subset of the e2e.Handler interface containing only the methods
+// needed by GroupChat
 type GroupE2e interface {
 	SendE2E(mt catalog.MessageType, recipient *id.ID, payload []byte,
 		params e2e.Params) ([]id.Round, crypto.MessageID, time.Time, error)
-	RegisterListener(senderID *id.ID,
-		messageType catalog.MessageType,
+	RegisterListener(senderID *id.ID, messageType catalog.MessageType,
 		newListener receive.Listener) receive.ListenerID
 	AddService(tag string, processor message.Processor) error
-	AddPartner(partnerID *id.ID,
-		partnerPubKey, myPrivKey *cyclic.Int,
-		partnerSIDHPubKey *sidh.PublicKey,
-		mySIDHPrivKey *sidh.PrivateKey, sendParams,
-		receiveParams session.Params) (partner.Manager, error)
+	AddPartner(partnerID *id.ID, partnerPubKey, myPrivKey *cyclic.Int,
+		partnerSIDHPubKey *sidh.PublicKey, mySIDHPrivKey *sidh.PrivateKey,
+		sendParams, receiveParams session.Params) (partner.Manager, error)
 	GetPartner(partnerID *id.ID) (partner.Manager, error)
 	GetHistoricalDHPubkey() *cyclic.Int
 	GetHistoricalDHPrivkey() *cyclic.Int
 }
 
-// Manager handles the list of groups a user is a part of.
-type Manager struct {
+// manager handles the list of groups a user is a part of.
+type manager struct {
 	e2e GroupE2e
 
 	receptionId *id.ID
@@ -82,7 +81,7 @@ type Manager struct {
 // NewManager creates a new group chat manager
 func NewManager(services GroupCmix, e2e GroupE2e, receptionId *id.ID,
 	rng *fastRNG.StreamGenerator, grp *cyclic.Group, kv *versioned.KV,
-	requestFunc RequestCallback, receiveFunc ReceiveCallback) (*Manager, error) {
+	requestFunc RequestCallback, receiveFunc ReceiveCallback) (GroupChat, error) {
 
 	// Load the group chat storage or create one if one does not exist
 	gStore, err := gs.NewOrLoadStore(
@@ -92,7 +91,7 @@ func NewManager(services GroupCmix, e2e GroupE2e, receptionId *id.ID,
 	}
 
 	// Define the manager object
-	m := &Manager{
+	m := &manager{
 		e2e:         e2e,
 		rng:         rng,
 		receptionId: receptionId,
@@ -104,7 +103,8 @@ func NewManager(services GroupCmix, e2e GroupE2e, receptionId *id.ID,
 	}
 
 	// Register listener for incoming e2e group chat requests
-	e2e.RegisterListener(&id.ZeroUser, catalog.GroupCreationRequest, &requestListener{m: m})
+	e2e.RegisterListener(
+		&id.ZeroUser, catalog.GroupCreationRequest, &requestListener{m})
 
 	// Register notifications listener for incoming e2e group chat requests
 	err = e2e.AddService(catalog.GroupRq, nil)
@@ -116,11 +116,11 @@ func NewManager(services GroupCmix, e2e GroupE2e, receptionId *id.ID,
 	for _, gId := range m.GetGroups() {
 		g, exists := m.GetGroup(gId)
 		if !exists {
-			jww.WARN.Printf("Unexpected failure to locate GroupID %s", gId.String())
+			jww.WARN.Printf("[GC] Unexpected failure to locate GroupID %s", gId)
 			continue
 		}
 
-		m.joinGroup(g)
+		m.AddService(g, "", nil)
 	}
 
 	return m, nil
@@ -129,28 +129,18 @@ func NewManager(services GroupCmix, e2e GroupE2e, receptionId *id.ID,
 // JoinGroup adds the group to storage, and enables requisite services.
 // An error is returned if the user is already part of the group or if the
 // maximum number of groups have already been joined.
-func (m Manager) JoinGroup(g gs.Group) error {
+func (m *manager) JoinGroup(g gs.Group) error {
 	if err := m.gs.Add(g); err != nil {
 		return errors.Errorf(joinGroupErr, g.ID, err)
 	}
 
-	m.joinGroup(g)
-	jww.DEBUG.Printf("Joined group %q with ID %s.", g.Name, g.ID)
+	m.AddService(g, "", nil)
+	jww.INFO.Printf("[GC] Joined group %q with ID %s.", g.Name, g.ID)
 	return nil
 }
 
-// joinGroup adds the group services
-func (m Manager) joinGroup(g gs.Group) {
-	newService := message.Service{
-		Identifier: g.ID[:],
-		Tag:        catalog.Group,
-		Metadata:   g.ID[:],
-	}
-	m.services.AddService(m.receptionId, newService, &receptionProcessor{m: &m, g: g})
-}
-
 // LeaveGroup removes a group from a list of groups the user is a part of.
-func (m Manager) LeaveGroup(groupID *id.ID) error {
+func (m *manager) LeaveGroup(groupID *id.ID) error {
 	if err := m.gs.Remove(groupID); err != nil {
 		return errors.Errorf(leaveGroupErr, groupID, err)
 	}
@@ -161,24 +151,24 @@ func (m Manager) LeaveGroup(groupID *id.ID) error {
 	}
 	m.services.DeleteService(m.receptionId, delService, nil)
 
-	jww.DEBUG.Printf("Left group with ID %s.", groupID)
+	jww.INFO.Printf("[GC] Left group with ID %s.", groupID)
 	return nil
 }
 
 // GetGroups returns a list of all registered groupChat IDs.
-func (m Manager) GetGroups() []*id.ID {
-	jww.DEBUG.Print("Getting list of all groups.")
+func (m *manager) GetGroups() []*id.ID {
+	jww.DEBUG.Print("[GC] Getting list of all groups.")
 	return m.gs.GroupIDs()
 }
 
 // GetGroup returns the group with the matching ID or returns false if none
 // exist.
-func (m Manager) GetGroup(groupID *id.ID) (gs.Group, bool) {
-	jww.DEBUG.Printf("Getting group with ID %s.", groupID)
+func (m *manager) GetGroup(groupID *id.ID) (gs.Group, bool) {
+	jww.DEBUG.Printf("[GC] Getting group with ID %s.", groupID)
 	return m.gs.Get(groupID)
 }
 
 // NumGroups returns the number of groups the user is a part of.
-func (m Manager) NumGroups() int {
+func (m *manager) NumGroups() int {
 	return m.gs.Len()
 }
diff --git a/groupChat/manager_test.go b/groupChat/manager_test.go
index 95eb620337bc040dd53543c809b2d005211b4490..098bba443c78c9973eb207fd8df09e23b2883e36 100644
--- a/groupChat/manager_test.go
+++ b/groupChat/manager_test.go
@@ -8,6 +8,8 @@
 package groupChat
 
 import (
+	"gitlab.com/elixxir/client/cmix"
+	"gitlab.com/elixxir/client/e2e"
 	gs "gitlab.com/elixxir/client/groupChat/groupStore"
 	"gitlab.com/elixxir/client/storage/versioned"
 	"gitlab.com/elixxir/crypto/group"
@@ -20,8 +22,17 @@ import (
 	"time"
 )
 
-// Unit test of Manager.newManager.
-func Test_newManager(t *testing.T) {
+// Tests that manager adheres to the GroupChat interface.
+var _ GroupChat = (*manager)(nil)
+
+// Tests that GroupCmix adheres to the cmix.Client interface.
+var _ GroupCmix = (cmix.Client)(nil)
+
+// Tests that GroupE2e adheres to the e2e.Handler interface.
+var _ GroupE2e = (e2e.Handler)(nil)
+
+// Unit test of NewManager.
+func TestNewManager(t *testing.T) {
 	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := group.Member{
 		ID:    id.NewIdFromString("userID", id.User, t),
@@ -31,18 +42,21 @@ func Test_newManager(t *testing.T) {
 	requestFunc := func(g gs.Group) { requestChan <- g }
 	receiveChan := make(chan MessageReceive)
 	receiveFunc := func(msg MessageReceive) { receiveChan <- msg }
-	m, err := NewManager(nil, newTestE2eManager(user.DhKey), user.ID, nil, nil, kv, requestFunc, receiveFunc)
+	gcInt, err := NewManager(nil, newTestE2eManager(user.DhKey), user.ID, nil, nil,
+		kv, requestFunc, receiveFunc)
 	if err != nil {
-		t.Errorf("newManager() returned an error: %+v", err)
+		t.Errorf("NewManager returned an error: %+v", err)
 	}
 
+	m := gcInt.(*manager)
+
 	if !m.gs.GetUser().Equal(user) {
-		t.Errorf("newManager() failed to create a store with the correct user."+
+		t.Errorf("NewManager failed to create a store with the correct user."+
 			"\nexpected: %s\nreceived: %s", user, m.gs.GetUser())
 	}
 
 	if m.gs.Len() != 0 {
-		t.Errorf("newManager() failed to create an empty store."+
+		t.Errorf("NewManager failed to create an empty store."+
 			"\nexpected: %d\nreceived: %d", 0, m.gs.Len())
 	}
 
@@ -63,8 +77,8 @@ func Test_newManager(t *testing.T) {
 	}
 }
 
-// Tests that Manager.newManager loads a group storage when it exists.
-func Test_newManager_LoadStorage(t *testing.T) {
+// Tests that NewManager loads a group storage when it exists.
+func TestNewManager_LoadStorage(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := group.Member{
@@ -78,25 +92,29 @@ func Test_newManager_LoadStorage(t *testing.T) {
 	}
 
 	for i := 0; i < 10; i++ {
-		err := gStore.Add(newTestGroup(getGroup(), getGroup().NewInt(42), prng, t))
+		err := gStore.Add(
+			newTestGroup(getGroup(), getGroup().NewInt(42), prng, t))
 		if err != nil {
 			t.Errorf("Failed to add group %d: %+v", i, err)
 		}
 	}
 
-	m, err := NewManager(newTestNetworkManager(0, t), newTestE2eManager(user.DhKey), user.ID, nil, nil, kv, nil, nil)
+	gcInt, err := NewManager(newTestNetworkManager(0, t),
+		newTestE2eManager(user.DhKey), user.ID, nil, nil, kv, nil, nil)
 	if err != nil {
-		t.Errorf("newManager() returned an error: %+v", err)
+		t.Errorf("NewManager returned an error: %+v", err)
 	}
 
+	m := gcInt.(*manager)
+
 	if !reflect.DeepEqual(gStore, m.gs) {
-		t.Errorf("newManager() failed to load the expected storage."+
+		t.Errorf("NewManager failed to load the expected storage."+
 			"\nexpected: %+v\nreceived: %+v", gStore, m.gs)
 	}
 }
 
 // Error path: an error is returned when a group cannot be loaded from storage.
-func Test_newManager_LoadError(t *testing.T) {
+func TestNewManager_LoadError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	kv := versioned.NewKV(ekv.MakeMemstore())
 	user := group.Member{
@@ -120,7 +138,7 @@ func Test_newManager_LoadError(t *testing.T) {
 
 	_, err = NewManager(nil, newTestE2eManager(user.DhKey), user.ID, nil, nil, kv, nil, nil)
 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
-		t.Errorf("newManager() did not return the expected error."+
+		t.Errorf("NewManager did not return the expected error."+
 			"\nexpected: %s\nreceived: %+v", expectedErr, err)
 	}
 }
@@ -129,7 +147,7 @@ func Test_newManager_LoadError(t *testing.T) {
 //  user. To fix this test, they need to use different users, which requires
 //  modifying
 // storage.InitTestingSession.
-// func TestManager_StartProcesses(t *testing.T) {
+// func Test_manager_StartProcesses(t *testing.T) {
 // 	jww.SetLogThreshold(jww.LevelTrace)
 // 	jww.SetStdoutThreshold(jww.LevelTrace)
 // 	prng := rand.New(rand.NewSource(42))
@@ -270,65 +288,65 @@ func Test_newManager_LoadError(t *testing.T) {
 // 	}
 // }
 
-// Unit test of Manager.JoinGroup.
-func TestManager_JoinGroup(t *testing.T) {
+// Unit test of manager.JoinGroup.
+func Test_manager_JoinGroup(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 	g := newTestGroup(m.grp, m.e2e.GetHistoricalDHPubkey(), prng, t)
 
 	err := m.JoinGroup(g)
 	if err != nil {
-		t.Errorf("JoinGroup() returned an error: %+v", err)
+		t.Errorf("JoinGroup returned an error: %+v", err)
 	}
 
 	if _, exists := m.gs.Get(g.ID); !exists {
-		t.Errorf("JoinGroup() failed to add the group %s.", g.ID)
+		t.Errorf("JoinGroup failed to add the group %s.", g.ID)
 	}
 }
 
 // Error path: an error is returned when a group is joined twice.
-func TestManager_JoinGroup_AddErr(t *testing.T) {
+func Test_manager_JoinGroup_AddError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 	expectedErr := strings.SplitN(joinGroupErr, "%", 2)[0]
 
 	err := m.JoinGroup(g)
 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
-		t.Errorf("JoinGroup() failed to return the expected error."+
+		t.Errorf("JoinGroup failed to return the expected error."+
 			"\nexpected: %s\nreceived: %+v", expectedErr, err)
 	}
 }
 
-// Unit test of Manager.LeaveGroup.
-func TestManager_LeaveGroup(t *testing.T) {
+// Unit test of manager.LeaveGroup.
+func Test_manager_LeaveGroup(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 
 	err := m.LeaveGroup(g.ID)
 	if err != nil {
-		t.Errorf("LeaveGroup() returned an error: %+v", err)
+		t.Errorf("LeaveGroup returned an error: %+v", err)
 	}
 
 	if _, exists := m.GetGroup(g.ID); exists {
-		t.Error("LeaveGroup() failed to delete the group.")
+		t.Error("LeaveGroup failed to delete the group.")
 	}
 }
 
-// Error path: an error is returned when no group with the ID exists
-func TestManager_LeaveGroup_NoGroupError(t *testing.T) {
+// Error path: an error is returned when no group with the ID exists.
+func Test_manager_LeaveGroup_NoGroupError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 	expectedErr := strings.SplitN(leaveGroupErr, "%", 2)[0]
 
 	err := m.LeaveGroup(id.NewIdFromString("invalidID", id.Group, t))
 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
-		t.Errorf("LeaveGroup() failed to return the expected error."+
+		t.Errorf("LeaveGroup failed to return the expected error."+
 			"\nexpected: %s\nreceived: %+v", expectedErr, err)
 	}
 }
 
-// Unit test of Manager.GetGroups.
-func TestManager_GetGroups(t *testing.T) {
+// Unit test of manager.GetGroups.
+func Test_manager_GetGroups(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 
@@ -340,36 +358,36 @@ func TestManager_GetGroups(t *testing.T) {
 	}
 
 	if m.gs.Len() != 0 {
-		t.Errorf("GetGroups() returned %d IDs, which is %d less than is in "+
+		t.Errorf("GetGroups returned %d IDs, which is %d less than is in "+
 			"memory.", len(list), m.gs.Len())
 	}
 }
 
-// Unit test of Manager.GetGroup.
-func TestManager_GetGroup(t *testing.T) {
+// Unit test of manager.GetGroup.
+func Test_manager_GetGroup(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 
 	testGrp, exists := m.GetGroup(g.ID)
 	if !exists {
-		t.Error("GetGroup() failed to find a group that should exist.")
+		t.Error("GetGroup failed to find a group that should exist.")
 	}
 
 	if !reflect.DeepEqual(g, testGrp) {
-		t.Errorf("GetGroup() failed to return the expected group."+
+		t.Errorf("GetGroup failed to return the expected group."+
 			"\nexpected: %#v\nreceived: %#v", g, testGrp)
 	}
 
 	testGrp, exists = m.GetGroup(id.NewIdFromString("invalidID", id.Group, t))
 	if exists {
-		t.Errorf("GetGroup() returned a group that should not exist: %#v", testGrp)
+		t.Errorf("GetGroup returned a group that should not exist: %#v", testGrp)
 	}
 }
 
-// Unit test of Manager.NumGroups. First a manager is created with 10 groups
+// Unit test of manager.NumGroups. First a manager is created with 10 groups
 // and the initial number is checked. Then the number of groups is checked after
 // leaving each until the number left is 0.
-func TestManager_NumGroups(t *testing.T) {
+func Test_manager_NumGroups(t *testing.T) {
 	expectedNum := 10
 	m, _ := newTestManagerWithStore(rand.New(rand.NewSource(42)), expectedNum,
 		0, nil, nil, t)
@@ -380,10 +398,9 @@ func TestManager_NumGroups(t *testing.T) {
 		_ = m.LeaveGroup(gid)
 
 		if m.NumGroups() != expectedNum-i {
-			t.Errorf("NumGroups() failed to return the expected number of "+
+			t.Errorf("NumGroups failed to return the expected number of "+
 				"groups (%d).\nexpected: %d\nreceived: %d",
 				i, expectedNum-i, m.NumGroups())
 		}
 	}
-
 }
diff --git a/groupChat/messageReceive.go b/groupChat/messageReceive.go
index e607e7f01fcd1aa2e6bb4cee11356e3ea71814f5..fa883e73e052ea5c54d9ce0d9af5082073debe95 100644
--- a/groupChat/messageReceive.go
+++ b/groupChat/messageReceive.go
@@ -31,7 +31,7 @@ type MessageReceive struct {
 	RoundTimestamp time.Time
 }
 
-// String returns the MessageReceive as readable text. This functions satisfies
+// String returns the MessageReceive as readable text. This functions adheres to
 // the fmt.Stringer interface.
 func (mr MessageReceive) String() string {
 	groupID := "<nil>"
@@ -54,16 +54,17 @@ func (mr MessageReceive) String() string {
 		recipientID = mr.RecipientID.String()
 	}
 
-	str := make([]string, 0, 9)
-	str = append(str, "GroupID:"+groupID)
-	str = append(str, "ID:"+mr.ID.String())
-	str = append(str, "Payload:"+payload)
-	str = append(str, "SenderID:"+senderID)
-	str = append(str, "RecipientID:"+recipientID)
-	str = append(str, "EphemeralID:"+strconv.FormatInt(mr.EphemeralID.Int64(), 10))
-	str = append(str, "Timestamp:"+mr.Timestamp.String())
-	str = append(str, "RoundID:"+strconv.FormatUint(uint64(mr.RoundID), 10))
-	str = append(str, "RoundTimestamp:"+mr.RoundTimestamp.String())
+	str := []string{
+		"GroupID:" + groupID,
+		"ID:" + mr.ID.String(),
+		"Payload:" + payload,
+		"SenderID:" + senderID,
+		"RecipientID:" + recipientID,
+		"EphemeralID:" + strconv.FormatInt(mr.EphemeralID.Int64(), 10),
+		"Timestamp:" + mr.Timestamp.String(),
+		"RoundID:" + strconv.FormatUint(uint64(mr.RoundID), 10),
+		"RoundTimestamp:" + mr.RoundTimestamp.String(),
+	}
 
 	return "{" + strings.Join(str, " ") + "}"
 }
diff --git a/groupChat/processor.go b/groupChat/processor.go
new file mode 100644
index 0000000000000000000000000000000000000000..1047889f6e4e3916748988fa6cc16fd108de4c78
--- /dev/null
+++ b/groupChat/processor.go
@@ -0,0 +1,25 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package groupChat
+
+import (
+	"fmt"
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/primitives/format"
+)
+
+type Processor interface {
+	// Process decrypts and hands off the message to its internal down stream
+	// message processing system.
+	Process(decryptedMsg MessageReceive, msg format.Message,
+		receptionID receptionID.EphemeralIdentity, round rounds.Round)
+
+	// Stringer interface for debugging
+	fmt.Stringer
+}
diff --git a/groupChat/receive.go b/groupChat/receive.go
index 244883cc33dd95d2164196adeaffaf6f03b71833..ac25d9cf6891578e72ebc04cf1860238ec28298b 100644
--- a/groupChat/receive.go
+++ b/groupChat/receive.go
@@ -26,67 +26,82 @@ const (
 	newDecryptKeyErr        = "failed to generate key for decrypting group payload: %+v"
 	unmarshalInternalMsgErr = "failed to unmarshal group internal message: %+v"
 	unmarshalSenderIdErr    = "failed to unmarshal sender ID: %+v"
-	unmarshalPublicMsgErr   = "failed to unmarshal group cMix message contents: %+v"
+	unmarshalPublicMsgErr   = "[GC] Failed to unmarshal group cMix message contents from %d (%s) on round %d: %+v"
+	getDecryptionKeyErr     = "[GC] Unable to get decryption key: %+v"
+	decryptMsgErr           = "[GC] Failed to decrypt group message: %+v"
 	genCryptKeyMacErr       = "failed to generate encryption key for group " +
 		"cMix message because MAC verification failed (epoch %d could be off)"
 )
 
-// Adheres to cmix.Manager interface for reception processing
+// Adheres to message.Processor interface for reception processing.
 type receptionProcessor struct {
-	m *Manager
+	m *manager
 	g gs.Group
+	p Processor
 }
 
-// Process incoming group chat messages
-func (p *receptionProcessor) Process(message format.Message, receptionID receptionID.EphemeralIdentity, round rounds.Round) {
-	jww.TRACE.Print("Group message reception received cMix message.")
-	// Attempt to read the message
-	roundTimestamp := round.Timestamps[states.QUEUED]
+// Process incoming group chat messages.
+func (p *receptionProcessor) Process(message format.Message,
+	receptionID receptionID.EphemeralIdentity, round rounds.Round) {
+	jww.TRACE.Printf("[GC] Received group message from %d (%s) on round %d.",
+		receptionID.EphId.Int64(), receptionID.Source, round.ID)
 
 	// Unmarshal cMix message contents to get public message format
 	pubMsg, err := unmarshalPublicMsg(message.GetContents())
 	if err != nil {
-		jww.WARN.Printf("Failed to unmarshal: %+v", errors.Errorf(unmarshalPublicMsgErr, err))
+		jww.ERROR.Printf(unmarshalPublicMsgErr, receptionID.EphId.Int64(),
+			receptionID.Source, round.ID, err)
+		return
 	}
 
-	// Obtain the cryptKey for the public message
+	// Obtain the decryption key for the public message
 	key, err := getCryptKey(p.g.Key, pubMsg.GetSalt(), message.GetMac(),
-		pubMsg.GetPayload(), p.g.DhKeys, roundTimestamp)
+		pubMsg.GetPayload(), p.g.DhKeys, round.Timestamps[states.QUEUED])
 	if err != nil {
-		jww.WARN.Printf("Unable to getCryptKey: %+v", err)
+		jww.ERROR.Printf(getDecryptionKeyErr, err)
 		return
 	}
 
 	// Decrypt the message payload using the cryptKey
-	result, err := decryptMessage(p.g, message.GetKeyFP(), key, pubMsg.GetPayload())
+	result, err := decryptMessage(
+		p.g, message.GetKeyFP(), key, pubMsg.GetPayload())
 	if err != nil {
-		jww.WARN.Printf("Group message reception failed to read "+
-			"cMix message: %+v", err)
+		jww.ERROR.Printf(decryptMsgErr, err)
 		return
 	}
+
 	// Populate remaining fields from the top level
 	result.GroupID = p.g.ID
 	result.RecipientID = receptionID.Source
 	result.EphemeralID = receptionID.EphId
 	result.RoundID = round.ID
-	result.RoundTimestamp = roundTimestamp
+	result.RoundTimestamp = round.Timestamps[states.QUEUED]
+
+	jww.DEBUG.Printf("[GC] Received group message with ID %s from sender "+
+		"%s in group %q with ID %s at %s.", result.ID, result.SenderID,
+		p.g.Name, p.g.ID, result.Timestamp)
 
-	jww.DEBUG.Printf("Received group message with ID %s from sender "+
-		"%s in group %s with ID %s at %s.", result.ID, result.SenderID, p.g.Name,
-		p.g.ID, result.Timestamp)
+	// Send the received message on the callback
+	go p.m.receiveFunc(result)
 
-	// If the message was read correctly, send it to the callback
-	p.m.receiveFunc(result)
+	// Send the decrypted message and original message to the processor, if one
+	// is registered
+	if p.p != nil {
+		p.p.Process(result, message, receptionID, round)
+	}
 }
 
 func (p *receptionProcessor) String() string {
-	return fmt.Sprintf("GroupChatReception(%s)", p.m.receptionId)
+	if p.p == nil {
+		return fmt.Sprintf("GroupChatReception(%s)", p.m.receptionId)
+	}
+	return fmt.Sprintf("GroupChatReception(%s)-%s", p.m.receptionId, p.p)
 }
 
 // decryptMessage decrypts the group message payload and returns its message ID,
 // timestamp, sender ID, and message contents.
-func decryptMessage(g gs.Group, fingerprint format.Fingerprint, key group.CryptKey, payload []byte) (
-	MessageReceive, error) {
+func decryptMessage(g gs.Group, fingerprint format.Fingerprint,
+	key group.CryptKey, payload []byte) (MessageReceive, error) {
 
 	// Decrypt internal message
 	decryptedPayload := group.Decrypt(key, fingerprint, payload)
diff --git a/groupChat/receiveRequest.go b/groupChat/receiveRequest.go
index 0d2224cbbcba375da4f440224c16b9d03e507eb5..d76744315edf613ca05cb5efbdfba889e7cfadd9 100644
--- a/groupChat/receiveRequest.go
+++ b/groupChat/receiveRequest.go
@@ -27,25 +27,25 @@ const (
 
 // Adheres to receive.Listener interface
 type requestListener struct {
-	m *Manager
+	m *manager
 }
 
 // Hear waits for new group requests to arrive
 func (l *requestListener) Hear(item receive.Message) {
-	jww.DEBUG.Print("Group message request received message.")
+	jww.DEBUG.Print("[GC] Group message request received message.")
 
 	// Generate the group from the request message
 	g, err := l.m.readRequest(item)
 	if err != nil {
-		jww.WARN.Printf("Failed to read message as group request: %+v", err)
+		jww.WARN.Printf(
+			"[GC] Failed to read message as group request: %+v", err)
 		return
 	}
 
-	// Call request callback with the new group if it does not already
-	// exist
+	// Call request callback with the new group if it does not already exist
 	if _, exists := l.m.GetGroup(g.ID); !exists {
-		jww.DEBUG.Printf("Received group request for "+
-			"group %s with ID %s.", g.Name, g.ID)
+		jww.INFO.Printf(
+			"[GC] Received group request for group %s with ID %s.", g.Name, g.ID)
 
 		l.m.requestFunc(g)
 	}
@@ -58,7 +58,7 @@ func (l *requestListener) Name() string {
 
 // readRequest returns the group described in the group request message. An
 // error is returned if the request is of the wrong type or cannot be read.
-func (m *Manager) readRequest(msg receive.Message) (gs.Group, error) {
+func (m *manager) readRequest(msg receive.Message) (gs.Group, error) {
 	// Return an error if the message is not of the right type
 	if msg.MessageType != catalog.GroupCreationRequest {
 		return gs.Group{}, errors.New(sendMessageTypeErr)
diff --git a/groupChat/receiveRequest_test.go b/groupChat/receiveRequest_test.go
index 398de2f495a348b2f34323cadee1d060074834e0..3dc1f7232e5698c526a89363ee88f0aca23661fe 100644
--- a/groupChat/receiveRequest_test.go
+++ b/groupChat/receiveRequest_test.go
@@ -146,7 +146,7 @@ func TestRequestListener_Hear_BadMessageType(t *testing.T) {
 }
 
 // Unit test of readRequest.
-func TestManager_readRequest(t *testing.T) {
+func Test_manager_readRequest(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManager(prng, t)
 
@@ -200,7 +200,7 @@ func TestManager_readRequest(t *testing.T) {
 }
 
 // Error path: an error is returned if the message type is incorrect.
-func TestManager_readRequest_MessageTypeError(t *testing.T) {
+func Test_manager_readRequest_MessageTypeError(t *testing.T) {
 	m, _ := newTestManager(rand.New(rand.NewSource(42)), t)
 	expectedErr := sendMessageTypeErr
 	msg := receive.Message{
@@ -215,7 +215,7 @@ func TestManager_readRequest_MessageTypeError(t *testing.T) {
 }
 
 // Error path: an error is returned if the proto message cannot be unmarshalled.
-func TestManager_readRequest_ProtoUnmarshalError(t *testing.T) {
+func Test_manager_readRequest_ProtoUnmarshalError(t *testing.T) {
 	expectedErr := strings.SplitN(deserializeMembershipErr, "%", 2)[0]
 	m, _ := newTestManager(rand.New(rand.NewSource(42)), t)
 
@@ -239,7 +239,7 @@ func TestManager_readRequest_ProtoUnmarshalError(t *testing.T) {
 }
 
 // Error path: an error is returned if the membership cannot be deserialized.
-func TestManager_readRequest_DeserializeMembershipError(t *testing.T) {
+func Test_manager_readRequest_DeserializeMembershipError(t *testing.T) {
 	m, _ := newTestManager(rand.New(rand.NewSource(42)), t)
 	expectedErr := strings.SplitN(protoUnmarshalErr, "%", 2)[0]
 	msg := receive.Message{
diff --git a/groupChat/send.go b/groupChat/send.go
index 64796dd35111424ff438b5ce2b6d7f721cb2083d..817eba9cc901cdd4f1c9c2415c0350fefbb94125 100644
--- a/groupChat/send.go
+++ b/groupChat/send.go
@@ -10,7 +10,6 @@ package groupChat
 import (
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/client/catalog"
 	"gitlab.com/elixxir/client/cmix"
 	"gitlab.com/elixxir/client/cmix/message"
 	gs "gitlab.com/elixxir/client/groupChat/groupStore"
@@ -36,9 +35,10 @@ const (
 	saltReadLengthErr = "length of generated salt %d != %d required"
 )
 
-// Send sends a message to all group members using Client.SendManyCMIX.
+// Send sends a message to all group members using Client.SendMany.
 // The send fails if the message is too long.
-func (m *Manager) Send(groupID *id.ID, message []byte) (id.Round, time.Time, group.MessageID, error) {
+func (m *manager) Send(groupID *id.ID, tag string, message []byte) (
+	id.Round, time.Time, group.MessageID, error) {
 
 	// Get the relevant group
 	g, exists := m.GetGroup(groupID)
@@ -47,17 +47,19 @@ func (m *Manager) Send(groupID *id.ID, message []byte) (id.Round, time.Time, gro
 			errors.Errorf(newNoGroupErr, groupID)
 	}
 
-	// get the current time stripped of the monotonic clock
+	// Get the current time stripped of the monotonic clock
 	timeNow := netTime.Now().Round(0)
 
 	// Create a cMix message for each group member
-	groupMessages, err := m.newMessages(g, message, timeNow)
+	groupMessages, err := m.newMessages(g, tag, message, timeNow)
 	if err != nil {
-		return 0, time.Time{}, group.MessageID{}, errors.Errorf(newCmixMsgErr, err)
+		return 0, time.Time{}, group.MessageID{},
+			errors.Errorf(newCmixMsgErr, err)
 	}
 
 	// Obtain message ID
-	msgId, err := getGroupMessageId(m.grp, groupID, m.receptionId, timeNow, message)
+	msgId, err := getGroupMessageId(
+		m.grp, groupID, m.receptionId, timeNow, message)
 	if err != nil {
 		return 0, time.Time{}, group.MessageID{}, err
 	}
@@ -71,14 +73,15 @@ func (m *Manager) Send(groupID *id.ID, message []byte) (id.Round, time.Time, gro
 			errors.Errorf(sendManyCmixErr, m.receptionId, groupID, err)
 	}
 
-	jww.DEBUG.Printf("Sent message to %d members in group %s at %s.",
+	jww.DEBUG.Printf("[GC] Sent message to %d members in group %s at %s.",
 		len(groupMessages), groupID, timeNow)
 	return rid, timeNow, msgId, nil
 }
 
-// newMessages quickly builds messages for all group chat members in multiple threads
-func (m *Manager) newMessages(g gs.Group, msg []byte, timestamp time.Time) (
-	[]cmix.TargetedCmixMessage, error) {
+// newMessages quickly builds messages for all group chat members in multiple
+// threads.
+func (m *manager) newMessages(g gs.Group, tag string, msg []byte,
+	timestamp time.Time) ([]cmix.TargetedCmixMessage, error) {
 
 	// Create list of cMix messages
 	messages := make([]cmix.TargetedCmixMessage, 0, len(g.Members))
@@ -93,7 +96,8 @@ func (m *Manager) newMessages(g gs.Group, msg []byte, timestamp time.Time) (
 		}
 
 		// Add cMix message to list
-		cMixMsg, err := newCmixMsg(g, msg, timestamp, member, rng, m.receptionId, m.services.GetMaxMessageLength())
+		cMixMsg, err := newCmixMsg(g, tag, msg, timestamp, member, rng,
+			m.receptionId, m.services.GetMaxMessageLength())
 		if err != nil {
 			return nil, err
 		}
@@ -104,15 +108,16 @@ func (m *Manager) newMessages(g gs.Group, msg []byte, timestamp time.Time) (
 }
 
 // newCmixMsg generates a new cMix message to be sent to a group member.
-func newCmixMsg(g gs.Group, msg []byte, timestamp time.Time,
-	mem group.Member, rng io.Reader, senderId *id.ID, maxCmixMessageSize int) (cmix.TargetedCmixMessage, error) {
+func newCmixMsg(g gs.Group, tag string, msg []byte, timestamp time.Time,
+	mem group.Member, rng io.Reader, senderId *id.ID, maxCmixMessageSize int) (
+	cmix.TargetedCmixMessage, error) {
 
 	// Initialize targeted message
 	cmixMsg := cmix.TargetedCmixMessage{
 		Recipient: mem.ID,
 		Service: message.Service{
 			Identifier: g.ID[:],
-			Tag:        catalog.Group,
+			Tag:        makeServiceTag(tag),
 			Metadata:   g.ID[:],
 		},
 	}
@@ -160,13 +165,16 @@ func newCmixMsg(g gs.Group, msg []byte, timestamp time.Time,
 }
 
 // Build the group message ID
-func getGroupMessageId(grp *cyclic.Group, groupId, senderId *id.ID, timestamp time.Time, msg []byte) (group.MessageID, error) {
+func getGroupMessageId(grp *cyclic.Group, groupId, senderId *id.ID,
+	timestamp time.Time, msg []byte) (group.MessageID, error) {
 	cmixMsg := format.NewMessage(grp.GetP().ByteLen())
 	_, intlMsg, err := newMessageParts(cmixMsg.ContentsSize())
 	if err != nil {
-		return group.MessageID{}, errors.WithMessage(err, "Failed to make message parts for message ID")
+		return group.MessageID{}, errors.WithMessage(err,
+			"Failed to make message parts for message ID")
 	}
-	return group.NewMessageID(groupId, setInternalPayload(intlMsg, timestamp, senderId, msg)), nil
+	return group.NewMessageID(groupId,
+		setInternalPayload(intlMsg, timestamp, senderId, msg)), nil
 }
 
 // newMessageParts generates a public payload message and the internal payload
diff --git a/groupChat/sendRequests.go b/groupChat/sendRequests.go
index dba1231cde58c38d7159daadce4f7589369c2a0a..dca54c2c951523d6b8af49064f78352d5116ab8b 100644
--- a/groupChat/sendRequests.go
+++ b/groupChat/sendRequests.go
@@ -30,20 +30,20 @@ const (
 )
 
 // ResendRequest allows a groupChat request to be sent again.
-func (m Manager) ResendRequest(groupID *id.ID) ([]id.Round, RequestStatus, error) {
+func (m *manager) ResendRequest(groupID *id.ID) ([]id.Round, RequestStatus, error) {
 	g, exists := m.GetGroup(groupID)
 	if !exists {
 		return nil, NotSent, errors.Errorf(resendGroupIdErr, groupID)
 	}
 
-	jww.DEBUG.Printf("Resending group requests for group %s.", groupID)
+	jww.INFO.Printf("[GC] Resending group requests for group %s.", groupID)
 
 	return m.sendRequests(g)
 }
 
 // sendRequests sends group requests to each member in the group except for the
 // leader/sender
-func (m Manager) sendRequests(g gs.Group) ([]id.Round, RequestStatus, error) {
+func (m *manager) sendRequests(g gs.Group) ([]id.Round, RequestStatus, error) {
 	// Build request message
 	requestMarshaled, err := proto.Marshal(&Request{
 		Name:        g.Name,
@@ -106,7 +106,8 @@ func (m Manager) sendRequests(g gs.Group) ([]id.Round, RequestStatus, error) {
 				strings.Join(errs, "\n"))
 	}
 
-	jww.DEBUG.Printf("Sent group request to %d members in group %q with ID %s.",
+	jww.DEBUG.Printf(
+		"[GC] Sent group request to %d members in group %q with ID %s.",
 		len(g.Members), g.Name, g.ID)
 
 	// If all sends succeeded, return a list of roundIDs
@@ -114,12 +115,13 @@ func (m Manager) sendRequests(g gs.Group) ([]id.Round, RequestStatus, error) {
 }
 
 // sendRequest sends the group request to the user via E2E.
-func (m Manager) sendRequest(memberID *id.ID, request []byte) ([]id.Round, error) {
+func (m *manager) sendRequest(memberID *id.ID, request []byte) ([]id.Round, error) {
 	p := e2e.GetDefaultParams()
 	p.LastServiceTag = catalog.GroupRq
 	p.DebugTag = "group.Request"
 
-	rounds, _, _, err := m.e2e.SendE2E(catalog.GroupCreationRequest, memberID, request, p)
+	rounds, _, _, err := m.e2e.SendE2E(
+		catalog.GroupCreationRequest, memberID, request, p)
 	if err != nil {
 		return nil, errors.Errorf(sendE2eErr, memberID, err)
 	}
diff --git a/groupChat/sendRequests_test.go b/groupChat/sendRequests_test.go
index 6951f49b12ee258a59a120bdfadd507300bd109b..07d2ee5459db766450f63cc7f61195a8c9616643 100644
--- a/groupChat/sendRequests_test.go
+++ b/groupChat/sendRequests_test.go
@@ -23,8 +23,8 @@ import (
 	"testing"
 )
 
-// Tests that Manager.ResendRequest sends all expected requests successfully.
-func TestManager_ResendRequest(t *testing.T) {
+// Tests that manager.ResendRequest sends all expected requests successfully.
+func Test_manager_ResendRequest(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 
@@ -102,7 +102,7 @@ func TestManager_ResendRequest(t *testing.T) {
 
 // Error path: an error is returned when no group with the corresponding group
 // ID exists.
-func TestManager_ResendRequest_GetGroupError(t *testing.T) {
+func Test_manager_ResendRequest_GetGroupError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 	expectedErr := strings.SplitN(resendGroupIdErr, "%", 2)[0]
@@ -119,8 +119,8 @@ func TestManager_ResendRequest_GetGroupError(t *testing.T) {
 	}
 }
 
-// Tests that Manager.sendRequests sends all expected requests successfully.
-func TestManager_sendRequests(t *testing.T) {
+// Tests that manager.sendRequests sends all expected requests successfully.
+func Test_manager_sendRequests(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 
@@ -196,9 +196,9 @@ func TestManager_sendRequests(t *testing.T) {
 	}
 }
 
-// Tests that Manager.sendRequests returns the correct status when all sends
+// Tests that manager.sendRequests returns the correct status when all sends
 // fail.
-func TestManager_sendRequests_SendAllFail(t *testing.T) {
+func Test_manager_sendRequests_SendAllFail(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 1, nil, nil, t)
 	expectedErr := fmt.Sprintf(sendRequestAllErr, len(g.Members)-1, "")
@@ -225,9 +225,9 @@ func TestManager_sendRequests_SendAllFail(t *testing.T) {
 	}
 }
 
-// Tests that Manager.sendRequests returns the correct status when some sends
+// Tests that manager.sendRequests returns the correct status when some sends
 // fail.
-func TestManager_sendRequests_SendPartialSent(t *testing.T) {
+func Test_manager_sendRequests_SendPartialSent(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 2, nil, nil, t)
 	expectedErr := fmt.Sprintf(sendRequestPartialErr, (len(g.Members)-1)/2,
@@ -267,8 +267,8 @@ func TestManager_sendRequests_SendPartialSent(t *testing.T) {
 	}
 }
 
-// Unit test of Manager.sendRequest.
-func TestManager_sendRequest(t *testing.T) {
+// Unit test of manager.sendRequest.
+func Test_manager_sendRequest(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 
@@ -307,7 +307,7 @@ func TestManager_sendRequest(t *testing.T) {
 }
 
 // Error path: an error is returned when SendE2E fails
-func TestManager_sendRequest_SendE2eError(t *testing.T) {
+func Test_manager_sendRequest_SendE2eError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 1, nil, nil, t)
 	expectedErr := strings.SplitN(sendE2eErr, "%", 2)[0]
diff --git a/groupChat/send_test.go b/groupChat/send_test.go
index 72d98232909c7d10f401e7805b0be4ef52393765..85eeeb245a02d70ca6782219d7f16dd00aed9c63 100644
--- a/groupChat/send_test.go
+++ b/groupChat/send_test.go
@@ -17,6 +17,7 @@ import (
 	"gitlab.com/elixxir/primitives/format"
 	"gitlab.com/elixxir/primitives/states"
 	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
 	"gitlab.com/xx_network/primitives/netTime"
 	"math/rand"
 	"strings"
@@ -24,11 +25,12 @@ import (
 	"time"
 )
 
-func TestManager_Send(t *testing.T) {
+func Test_manager_Send(t *testing.T) {
 	receiveChan := make(chan MessageReceive, 100)
 	receiveFunc := func(msg MessageReceive) {
 		receiveChan <- msg
 	}
+	msgChan := make(chan MessageReceive, 10)
 
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 1, 0, nil, receiveFunc, t)
@@ -36,6 +38,7 @@ func TestManager_Send(t *testing.T) {
 	reception := &receptionProcessor{
 		m: m,
 		g: g,
+		p: &testProcessor{msgChan},
 	}
 
 	roundId, _, msgId, err := m.Send(g.ID, messageBytes)
@@ -54,9 +57,12 @@ func TestManager_Send(t *testing.T) {
 	timestamps := make(map[states.Round]time.Time)
 	timestamps[states.QUEUED] = netTime.Now().Round(0)
 	for _, msg := range messages {
-		reception.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{ID: roundId, Timestamps: timestamps})
+		reception.Process(msg, receptionID.EphemeralIdentity{
+			EphId: ephemeral.Id{1, 2, 3}, Source: &id.ID{4, 5, 6},
+		},
+			rounds.Round{ID: roundId, Timestamps: timestamps})
 		select {
-		case result := <-receiveChan:
+		case result := <-msgChan:
 			if !result.SenderID.Cmp(m.receptionId) {
 				t.Errorf("Sender mismatch")
 			}
@@ -90,7 +96,7 @@ func TestGroup_newCmixMsg_SaltReaderError(t *testing.T) {
 func TestGroup_newCmixMsg_InternalMsgSizeError(t *testing.T) {
 	expectedErr := strings.SplitN(messageLenErr, "%", 2)[0]
 
-	// Create new test Manager and Group
+	// Create new test manager and Group
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 
@@ -261,3 +267,14 @@ func Test_setPublicPayload(t *testing.T) {
 			encryptedPayload, unmarshalled.GetPayload())
 	}
 }
+
+type testProcessor struct {
+	msgChan chan MessageReceive
+}
+
+func (tp *testProcessor) Process(decryptedMsg MessageReceive, _ format.Message,
+	_ receptionID.EphemeralIdentity, _ rounds.Round) {
+	tp.msgChan <- decryptedMsg
+}
+
+func (tp *testProcessor) String() string { return "testProcessor" }
diff --git a/groupChat/service.go b/groupChat/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..63fb600e476fa7eb07c09c46f95bb260eee38e1b
--- /dev/null
+++ b/groupChat/service.go
@@ -0,0 +1,35 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package groupChat
+
+import (
+	"gitlab.com/elixxir/client/catalog"
+	"gitlab.com/elixxir/client/cmix/message"
+	gs "gitlab.com/elixxir/client/groupChat/groupStore"
+)
+
+func (m *manager) AddService(g gs.Group, tag string, p Processor) {
+	newService := message.Service{
+		Identifier: g.ID[:],
+		Tag:        makeServiceTag(tag),
+		Metadata:   g.ID[:],
+	}
+	m.services.AddService(m.receptionId, newService, &receptionProcessor{m, g, p})
+}
+
+func (m *manager) RemoveService(g gs.Group, tag string, p Processor) {
+	toDelete := message.Service{
+		Identifier: g.ID[:],
+		Tag:        makeServiceTag(tag),
+	}
+	m.services.DeleteService(g.ID, toDelete, &receptionProcessor{m, g, p})
+}
+
+func makeServiceTag(tag string) string {
+	return catalog.Group + "-" + tag
+}
diff --git a/groupChat/utils_test.go b/groupChat/utils_test.go
index 371d03ba3d87adbbbab9be9b64d12be99006ef7c..34b3ed5b42fa561682dab2d673e05fbb0bebf015 100644
--- a/groupChat/utils_test.go
+++ b/groupChat/utils_test.go
@@ -41,9 +41,9 @@ import (
 	"gitlab.com/xx_network/primitives/netTime"
 )
 
-// newTestManager creates a new Manager for testing.
-func newTestManager(rng *rand.Rand, t *testing.T) (*Manager, gs.Group) {
-	m := &Manager{
+// newTestManager creates a new manager for testing.
+func newTestManager(rng *rand.Rand, t *testing.T) (*manager, gs.Group) {
+	m := &manager{
 		receptionId: id.NewIdFromString("test", id.User, t),
 		rng:         fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG),
 		grp:         getGroup(),
@@ -66,13 +66,13 @@ func newTestManager(rng *rand.Rand, t *testing.T) (*Manager, gs.Group) {
 	return m, g
 }
 
-// newTestManager creates a new Manager that has groups stored for testing. One
+// newTestManager creates a new manager that has groups stored for testing. One
 // of the groups in the list is also returned.
 func newTestManagerWithStore(rng *rand.Rand, numGroups int, sendErr int,
 	requestFunc RequestCallback, receiveFunc ReceiveCallback,
-	t *testing.T) (*Manager, gs.Group) {
+	t *testing.T) (*manager, gs.Group) {
 
-	m := &Manager{
+	m := &manager{
 		receptionId: id.NewIdFromString("test", id.User, t),
 		rng:         fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG),
 		grp:         getGroup(),
diff --git a/interfaces/networkManager.go b/interfaces/networkManager.go
index 1315c7d710571d694f6ca77d7b0222e2e50aa0f9..2139e9597c6a4cdc51714e47308d6aa91d3481c5 100644
--- a/interfaces/networkManager.go
+++ b/interfaces/networkManager.go
@@ -50,7 +50,7 @@ type NetworkManager interface {
 		id.Round, []ephemeral.Id, error)
 
 	/*===Message Reception================================================*/
-	/* Identities are all network identites which the client is currently
+	/* Identities are all network identities which the client is currently
 	trying to pick up message on. An identity must be added
 	to receive messages, fake ones will be used to poll the network
 	if none are present. On creation of the network handler, the identity in
@@ -132,7 +132,7 @@ type NetworkManager interface {
 	// the given identity
 	DeleteClientTriggers(identity *id.ID)
 
-	// TrackTriggers - Registers a callback which will get called
+	// TrackServices - Registers a callback which will get called
 	// every time triggers change.
 	// It will receive the triggers list every time it is modified.
 	// Will only get callbacks while the Network Follower is running.
@@ -163,7 +163,8 @@ type NetworkManager interface {
 	// relationship with
 	NumRegisteredNodes() int
 
-	// Triggers the generation of a keying relationship with a given node
+	// TriggerNodeRegistration triggers the generation of a keying
+	// relationship with a given node
 	TriggerNodeRegistration(nid *id.ID)
 
 	/*===Historical Rounds================================================*/
@@ -176,7 +177,7 @@ type NetworkManager interface {
 	// network
 	LookupHistoricalRound(rid id.Round,
 		callback func(info *mixmessages.RoundInfo,
-			success bool)) error
+		success bool)) error
 
 	/*===Sender===========================================================*/
 	/* The sender handles sending comms to the network. It tracks
diff --git a/single/request.go b/single/request.go
index 6f2d71ae8183a3f45f0f6dde7edaff80d104fc29..6cb079652cca7e72a82c2c1f623802a5ff19f64a 100644
--- a/single/request.go
+++ b/single/request.go
@@ -212,8 +212,6 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte,
 			errors.Errorf(errSendRequest, tag, recipient, err)
 	}
 
-	// todo: this is jono's work but there's a send above it,
-	//  probably just WIP code, talk to jono and resolve once tests work
 	roundIDs := make([]id.Round, len(parts)+1)
 	roundIDs[0] = rid
 	for i, part := range parts {
diff --git a/storage/user/cryptographic.go b/storage/user/cryptographic.go
index 592fc9c2322d3f8552d3e0ebc5f721cd292ed384..fd4b73047008f6f0f78ad9a2f1b067372932aae6 100644
--- a/storage/user/cryptographic.go
+++ b/storage/user/cryptographic.go
@@ -41,7 +41,9 @@ type ciDisk struct {
 	IsPrecanned        bool
 }
 
-func newCryptographicIdentity(transmissionID, receptionID *id.ID, transmissionSalt, receptionSalt []byte, transmissionRsa, receptionRsa *rsa.PrivateKey,
+func newCryptographicIdentity(transmissionID, receptionID *id.ID,
+	transmissionSalt, receptionSalt []byte,
+	transmissionRsa, receptionRsa *rsa.PrivateKey,
 	isPrecanned bool, kv *versioned.KV) *CryptographicIdentity {
 
 	ci := &CryptographicIdentity{
diff --git a/ud/interfaces.go b/ud/interfaces.go
index 6042ae755fd34b770a3dcf503768d76346ebf50c..ba5bff8b7eb20cd55da1a1f9c7cf91d3478d8df1 100644
--- a/ud/interfaces.go
+++ b/ud/interfaces.go
@@ -24,6 +24,10 @@ type E2E interface {
 
 	// GetReceptionID returns the default IDs
 	GetReceptionID() *id.ID
+
+	// GetHistoricalDHPubkey returns the user's Historical DH
+	// Public Key
+	GetHistoricalDHPubkey() *cyclic.Int
 }
 
 // UserInfo is a sub-interface for the user.User object in storage.
diff --git a/ud/manager.go b/ud/manager.go
index 53e6a37f62630e24f0754ac4b731bddf01e37c0c..9d60515e5995059eb4926e6ef33384a69e6a0347 100644
--- a/ud/manager.go
+++ b/ud/manager.go
@@ -17,6 +17,12 @@ import (
 	"time"
 )
 
+const (
+	IsRegisteredErr = "NewManager is already registered. " +
+		"NewManager is meant for the first instantiation. Use LoadManager " +
+		"for all other calls"
+)
+
 // Manager is the control structure for the contacting the user discovery service.
 type Manager struct {
 	// Network is a sub-interface of the cmix.Client interface. It
@@ -85,6 +91,10 @@ func NewManager(services CMix, e2e E2E,
 		kv:      kv,
 	}
 
+	if m.isRegistered() {
+		return nil, errors.Errorf(IsRegisteredErr)
+	}
+
 	// Initialize store
 	var err error
 	m.store, err = store.NewOrLoadStore(kv)
diff --git a/ud/register.go b/ud/register.go
index caae813dc950dbe260a07b19ce5be12b6a5db2e7..3df8edd643216d8629e7c074152cd23f7ef2cc04 100644
--- a/ud/register.go
+++ b/ud/register.go
@@ -25,7 +25,7 @@ func (m *Manager) register(username string, rng csprng.Source,
 		RSAPublicPem:           string(rsa.CreatePublicKeyPem(cryptoUser.ReceptionRSA.GetPublic())),
 		IdentityRegistration: &pb.Identity{
 			Username: username,
-			DhPubKey: cryptoUser.E2eDhPublicKey.Bytes(),
+			DhPubKey: m.e2e.GetHistoricalDHPubkey().Bytes(),
 			Salt:     cryptoUser.ReceptionSalt,
 		},
 		UID:       cryptoUser.ReceptionID.Marshal(),
diff --git a/ud/search.go b/ud/search.go
index d432f47561b079ffecb335c9b999d07561a1cb7e..7061f4cc3a5c0b1c463b665a06f7cc9b3d70bdaf 100644
--- a/ud/search.go
+++ b/ud/search.go
@@ -54,7 +54,8 @@ func Search(services CMix, events event.Reporter,
 		factMap:  factMap,
 	}
 
-	rndId, ephId, err := single.TransmitRequest(udContact, SearchTag, requestMarshaled,
+	rndId, ephId, err := single.TransmitRequest(udContact, SearchTag,
+		requestMarshaled,
 		response, params, services, rng, grp)
 	if err != nil {
 		return []id.Round{}, receptionID.EphemeralIdentity{},
diff --git a/xxmutils/restoreContacts.go b/xxmutils/restoreContacts.go
index 6b8afad7073c5d9ff7b4ad56e19515bdd596440c..69a09d2e6258479cfdc3a08aaeca050442fdc256 100644
--- a/xxmutils/restoreContacts.go
+++ b/xxmutils/restoreContacts.go
@@ -11,6 +11,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"gitlab.com/elixxir/client/single"
 	"gitlab.com/xx_network/primitives/netTime"
 	"math"
 	"strings"
@@ -41,6 +42,11 @@ func RestoreContactsFromBackup(backupPartnerIDs []byte, client *api.Client,
 	updatesCb interfaces.RestoreContactsUpdater) ([]*id.ID, []*id.ID,
 	[]error, error) {
 
+	udContact, err := udManager.GetContact()
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
 	var restored, failed []*id.ID
 	var errs []error
 
@@ -93,7 +99,7 @@ func RestoreContactsFromBackup(backupPartnerIDs []byte, client *api.Client,
 	rsWg := &sync.WaitGroup{}
 	rsWg.Add(numRoutines)
 	for i := 0; i < numRoutines; i++ {
-		go LookupContacts(lookupCh, foundCh, failCh, udManager, lcWg)
+		go LookupContacts(lookupCh, foundCh, failCh, client, udContact, lcWg)
 		go ResetSessions(resetContactCh, restoredCh, failCh, *client,
 			rsWg)
 	}
@@ -125,7 +131,6 @@ func RestoreContactsFromBackup(backupPartnerIDs []byte, client *api.Client,
 
 	// Event Processing
 	done := false
-	var err error = nil
 	for !done {
 		// NOTE: Timer is reset every loop
 		timeoutTimer := time.NewTimer(restoreTimeout)
@@ -173,13 +178,13 @@ func RestoreContactsFromBackup(backupPartnerIDs []byte, client *api.Client,
 // the mobile phone apps and are not intended to be part of the xxDK. It
 // should be treated as internal functions specific to the phone apps.
 func LookupContacts(in chan *id.ID, out chan *contact.Contact,
-	failCh chan failure, udManager *ud.Manager,
+	failCh chan failure, client *api.Client, udContact contact.Contact,
 	wg *sync.WaitGroup) {
 	defer wg.Done()
 	// Start looking up contacts with user discovery and feed this
 	// contacts channel.
 	for lookupID := range in {
-		c, err := LookupContact(lookupID, udManager)
+		c, err := LookupContact(lookupID, client, udContact)
 		if err == nil {
 			out <- c
 			continue
@@ -221,7 +226,7 @@ func ResetSessions(in, out chan *contact.Contact, failCh chan failure,
 // xxDK users should not use this function. This function is used by
 // the mobile phone apps and are not intended to be part of the xxDK. It
 // should be treated as internal functions specific to the phone apps.
-func LookupContact(userID *id.ID, udManager *ud.Manager) (
+func LookupContact(userID *id.ID, client *api.Client, udContact contact.Contact) (
 	*contact.Contact, error) {
 	// This is a little wonky, but wait until we get called then
 	// set the result to the contact objects details if there is
@@ -240,8 +245,10 @@ func LookupContact(userID *id.ID, udManager *ud.Manager) (
 	waiter.Lock()
 
 	// in MS, so 90 seconds
-	timeout := time.Duration(90 * time.Second)
-	udManager.Lookup(userID, lookupCB, timeout)
+	stream := client.GetRng().GetStream()
+	defer stream.Close()
+	_, _, err = ud.Lookup(client.GetNetworkInterface(), stream, client.GetE2EHandler().GetGroup(),
+		udContact, lookupCB, userID, single.GetDefaultRequestParams())
 
 	// Now force a wait for callback to exit
 	waiter.Lock()
@@ -272,7 +279,7 @@ type failure struct {
 const stateStoreFmt = "restoreContactsFromBackup/v1/%s"
 
 type stateStore struct {
-	apiStore *storage.Session
+	apiStore storage.Session
 	// TODO: We could put a syncmap or something here instead of
 	// 1-key-per-id
 }